mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-10-30 21:21:09 -04:00 
			
		
		
		
	v5.0.0 update
This commit is contained in:
		
							
								
								
									
										468
									
								
								resources/scripts/views/expenses/Create.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										468
									
								
								resources/scripts/views/expenses/Create.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,468 @@ | ||||
| <template> | ||||
|   <CategoryModal /> | ||||
|  | ||||
|   <BasePage class="relative"> | ||||
|     <form action="" @submit.prevent="submitForm"> | ||||
|       <!-- Page Header --> | ||||
|       <BasePageHeader :title="pageTitle" class="mb-5"> | ||||
|         <BaseBreadcrumb> | ||||
|           <BaseBreadcrumbItem | ||||
|             :title="$t('general.home')" | ||||
|             to="/admin/dashboard" | ||||
|           /> | ||||
|  | ||||
|           <BaseBreadcrumbItem | ||||
|             :title="$tc('expenses.expense', 2)" | ||||
|             to="/admin/expenses" | ||||
|           /> | ||||
|  | ||||
|           <BaseBreadcrumbItem :title="pageTitle" to="#" active /> | ||||
|         </BaseBreadcrumb> | ||||
|  | ||||
|         <template #actions> | ||||
|           <BaseButton | ||||
|             v-if="isEdit && expenseStore.currentExpense.attachment_receipt" | ||||
|             :href="receiptDownloadUrl" | ||||
|             tag="a" | ||||
|             variant="primary-outline" | ||||
|             type="button" | ||||
|             class="mr-2" | ||||
|           > | ||||
|             <template #left="slotProps"> | ||||
|               <BaseIcon name="DownloadIcon" :class="slotProps.class" /> | ||||
|             </template> | ||||
|             {{ $t('expenses.download_receipt') }} | ||||
|           </BaseButton> | ||||
|  | ||||
|           <div class="hidden md:block"> | ||||
|             <BaseButton | ||||
|               :loading="isSaving" | ||||
|               :content-loading="isFetchingInitialData" | ||||
|               :disabled="isSaving" | ||||
|               variant="primary" | ||||
|               type="submit" | ||||
|             > | ||||
|               <template #left="slotProps"> | ||||
|                 <BaseIcon | ||||
|                   v-if="!isSaving" | ||||
|                   name="SaveIcon" | ||||
|                   :class="slotProps.class" | ||||
|                 /> | ||||
|               </template> | ||||
|               {{ | ||||
|                 isEdit | ||||
|                   ? $t('expenses.update_expense') | ||||
|                   : $t('expenses.save_expense') | ||||
|               }} | ||||
|             </BaseButton> | ||||
|           </div> | ||||
|         </template> | ||||
|       </BasePageHeader> | ||||
|  | ||||
|       <BaseCard> | ||||
|         <BaseInputGrid> | ||||
|           <BaseInputGroup | ||||
|             :label="$t('expenses.category')" | ||||
|             :error=" | ||||
|               v$.currentExpense.expense_category_id.$error && | ||||
|               v$.currentExpense.expense_category_id.$errors[0].$message | ||||
|             " | ||||
|             :content-loading="isFetchingInitialData" | ||||
|             required | ||||
|           > | ||||
|             <BaseMultiselect | ||||
|               v-model="expenseStore.currentExpense.expense_category_id" | ||||
|               :content-loading="isFetchingInitialData" | ||||
|               value-prop="id" | ||||
|               label="name" | ||||
|               track-by="id" | ||||
|               :options="searchCategory" | ||||
|               :filter-results="false" | ||||
|               resolve-on-load | ||||
|               :delay="500" | ||||
|               searchable | ||||
|               :invalid="v$.currentExpense.expense_category_id.$error" | ||||
|               :placeholder="$t('expenses.categories.select_a_category')" | ||||
|               @input="v$.currentExpense.expense_category_id.$touch()" | ||||
|             > | ||||
|               <template #action> | ||||
|                 <BaseSelectAction @click="openCategoryModal"> | ||||
|                   <BaseIcon | ||||
|                     name="PlusIcon" | ||||
|                     class="h-4 mr-2 -ml-2 text-center text-primary-400" | ||||
|                   /> | ||||
|                   {{ $t('settings.expense_category.add_new_category') }} | ||||
|                 </BaseSelectAction> | ||||
|               </template> | ||||
|             </BaseMultiselect> | ||||
|           </BaseInputGroup> | ||||
|  | ||||
|           <BaseInputGroup | ||||
|             :label="$t('expenses.expense_date')" | ||||
|             :error=" | ||||
|               v$.currentExpense.expense_date.$error && | ||||
|               v$.currentExpense.expense_date.$errors[0].$message | ||||
|             " | ||||
|             :content-loading="isFetchingInitialData" | ||||
|             required | ||||
|           > | ||||
|             <BaseDatePicker | ||||
|               v-model="expenseStore.currentExpense.expense_date" | ||||
|               :content-loading="isFetchingInitialData" | ||||
|               :calendar-button="true" | ||||
|               :invalid="v$.currentExpense.expense_date.$error" | ||||
|               @input="v$.currentExpense.expense_date.$touch()" | ||||
|             /> | ||||
|           </BaseInputGroup> | ||||
|  | ||||
|           <BaseInputGroup | ||||
|             :label="$t('expenses.amount')" | ||||
|             :error=" | ||||
|               v$.currentExpense.amount.$error && | ||||
|               v$.currentExpense.amount.$errors[0].$message | ||||
|             " | ||||
|             :content-loading="isFetchingInitialData" | ||||
|             required | ||||
|           > | ||||
|             <BaseMoney | ||||
|               :key="expenseStore.currentExpense.selectedCurrency" | ||||
|               v-model="amountData" | ||||
|               class="focus:border focus:border-solid focus:border-primary-500" | ||||
|               :invalid="v$.currentExpense.amount.$error" | ||||
|               :currency="expenseStore.currentExpense.selectedCurrency" | ||||
|               @input="v$.currentExpense.amount.$touch()" | ||||
|             /> | ||||
|           </BaseInputGroup> | ||||
|           <BaseInputGroup | ||||
|             :label="$t('expenses.currency')" | ||||
|             :content-loading="isFetchingInitialData" | ||||
|             :error=" | ||||
|               v$.currentExpense.currency_id.$error && | ||||
|               v$.currentExpense.currency_id.$errors[0].$message | ||||
|             " | ||||
|             required | ||||
|           > | ||||
|             <BaseMultiselect | ||||
|               v-model="expenseStore.currentExpense.currency_id" | ||||
|               value-prop="id" | ||||
|               label="name" | ||||
|               track-by="name" | ||||
|               :content-loading="isFetchingInitialData" | ||||
|               :options="globalStore.currencies" | ||||
|               searchable | ||||
|               :can-deselect="false" | ||||
|               :placeholder="$t('customers.select_currency')" | ||||
|               :invalid="v$.currentExpense.currency_id.$error" | ||||
|               class="w-full" | ||||
|               @update:modelValue="onCurrencyChange" | ||||
|             > | ||||
|             </BaseMultiselect> | ||||
|           </BaseInputGroup> | ||||
|  | ||||
|           <!-- Exchange rate converter --> | ||||
|           <ExchangeRateConverter | ||||
|             :store="expenseStore" | ||||
|             store-prop="currentExpense" | ||||
|             :v="v$.currentExpense" | ||||
|             :is-loading="isFetchingInitialData" | ||||
|             :is-edit="isEdit" | ||||
|             :customer-currency="expenseStore.currentExpense.currency_id" | ||||
|           /> | ||||
|  | ||||
|           <BaseInputGroup | ||||
|             :content-loading="isFetchingInitialData" | ||||
|             :label="$t('expenses.customer')" | ||||
|           > | ||||
|             <BaseMultiselect | ||||
|               v-model="expenseStore.currentExpense.customer_id" | ||||
|               :content-loading="isFetchingInitialData" | ||||
|               value-prop="id" | ||||
|               label="name" | ||||
|               track-by="id" | ||||
|               :options="searchCustomer" | ||||
|               :filter-results="false" | ||||
|               resolve-on-load | ||||
|               :delay="500" | ||||
|               searchable | ||||
|               :placeholder="$t('customers.select_a_customer')" | ||||
|             /> | ||||
|           </BaseInputGroup> | ||||
|  | ||||
|           <BaseInputGroup | ||||
|             :content-loading="isFetchingInitialData" | ||||
|             :label="$t('payments.payment_mode')" | ||||
|           > | ||||
|             <BaseMultiselect | ||||
|               v-model="expenseStore.currentExpense.payment_method_id" | ||||
|               :content-loading="isFetchingInitialData" | ||||
|               label="name" | ||||
|               value-prop="id" | ||||
|               track-by="name" | ||||
|               :options="expenseStore.paymentModes" | ||||
|               :placeholder="$t('payments.select_payment_mode')" | ||||
|               searchable | ||||
|             > | ||||
|               <!-- <template #action> | ||||
|                 <BaseSelectAction @click="addPaymentMode"> | ||||
|                   <BaseIcon | ||||
|                     name="PlusIcon" | ||||
|                     class="h-4 mr-2 -ml-2 text-center text-primary-400" | ||||
|                   /> | ||||
|                   {{ $t('settings.payment_modes.add_payment_mode') }} | ||||
|                 </BaseSelectAction> | ||||
|               </template> --> | ||||
|             </BaseMultiselect> | ||||
|           </BaseInputGroup> | ||||
|  | ||||
|           <BaseInputGroup | ||||
|             :content-loading="isFetchingInitialData" | ||||
|             :label="$t('expenses.note')" | ||||
|             :error=" | ||||
|               v$.currentExpense.notes.$error && | ||||
|               v$.currentExpense.notes.$errors[0].$message | ||||
|             " | ||||
|           > | ||||
|             <BaseTextarea | ||||
|               v-model="expenseStore.currentExpense.notes" | ||||
|               :content-loading="isFetchingInitialData" | ||||
|               :row="4" | ||||
|               rows="4" | ||||
|               @input="v$.currentExpense.notes.$touch()" | ||||
|             /> | ||||
|           </BaseInputGroup> | ||||
|  | ||||
|           <BaseInputGroup :label="$t('expenses.receipt')"> | ||||
|             <BaseFileUploader | ||||
|               v-model="expenseStore.currentExpense.receiptFiles" | ||||
|               accept="image/*,.doc,.docx,.pdf,.csv,.xlsx,.xls" | ||||
|               @change="onFileInputChange" | ||||
|               @remove="onFileInputRemove" | ||||
|             /> | ||||
|           </BaseInputGroup> | ||||
|  | ||||
|           <!-- Expense Custom Fields --> | ||||
|           <ExpenseCustomFields | ||||
|             :is-edit="isEdit" | ||||
|             class="col-span-2" | ||||
|             :is-loading="isFetchingInitialData" | ||||
|             type="Expense" | ||||
|             :store="expenseStore" | ||||
|             store-prop="currentExpense" | ||||
|             :custom-field-scope="expenseValidationScope" | ||||
|           /> | ||||
|  | ||||
|           <div class="block md:hidden"> | ||||
|             <BaseButton | ||||
|               :loading="isSaving" | ||||
|               :tabindex="6" | ||||
|               variant="primary" | ||||
|               type="submit" | ||||
|               class="flex justify-center w-full" | ||||
|             > | ||||
|               <template #left="slotProps"> | ||||
|                 <BaseIcon | ||||
|                   v-if="!isSaving" | ||||
|                   name="SaveIcon" | ||||
|                   :class="slotProps.class" | ||||
|                 /> | ||||
|               </template> | ||||
|               {{ | ||||
|                 isEdit | ||||
|                   ? $t('expenses.update_expense') | ||||
|                   : $t('expenses.save_expense') | ||||
|               }} | ||||
|             </BaseButton> | ||||
|           </div> | ||||
|         </BaseInputGrid> | ||||
|       </BaseCard> | ||||
|     </form> | ||||
|   </BasePage> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { ref, computed, onMounted } from 'vue' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { | ||||
|   required, | ||||
|   minValue, | ||||
|   maxLength, | ||||
|   helpers, | ||||
|   requiredIf, | ||||
|   decimal, | ||||
| } from '@vuelidate/validators' | ||||
| import useVuelidate from '@vuelidate/core' | ||||
| import { useExpenseStore } from '@/scripts/stores/expense' | ||||
| import { useCategoryStore } from '@/scripts/stores/category' | ||||
| import { useCompanyStore } from '@/scripts/stores/company' | ||||
| import { useCustomerStore } from '@/scripts/stores/customer' | ||||
| import { useCustomFieldStore } from '@/scripts/stores/custom-field' | ||||
| import { useModalStore } from '@/scripts/stores/modal' | ||||
| import ExpenseCustomFields from '@/scripts/components/custom-fields/CreateCustomFields.vue' | ||||
| import CategoryModal from '@/scripts/components/modal-components/CategoryModal.vue' | ||||
| import ExchangeRateConverter from '@/scripts/components/estimate-invoice-common/ExchangeRateConverter.vue' | ||||
| import { useGlobalStore } from '@/scripts/stores/global' | ||||
|  | ||||
| const customerStore = useCustomerStore() | ||||
| const companyStore = useCompanyStore() | ||||
| const expenseStore = useExpenseStore() | ||||
| const categoryStore = useCategoryStore() | ||||
| const customFieldStore = useCustomFieldStore() | ||||
| const modalStore = useModalStore() | ||||
| const route = useRoute() | ||||
| const router = useRouter() | ||||
| const { t } = useI18n() | ||||
| const globalStore = useGlobalStore() | ||||
|  | ||||
| let isSaving = ref(false) | ||||
| let isFetchingInitialData = ref(false) | ||||
| const expenseValidationScope = 'newExpense' | ||||
|  | ||||
| const rules = computed(() => { | ||||
|   return { | ||||
|     currentExpense: { | ||||
|       expense_category_id: { | ||||
|         required: helpers.withMessage(t('validation.required'), required), | ||||
|       }, | ||||
|       expense_date: { | ||||
|         required: helpers.withMessage(t('validation.required'), required), | ||||
|       }, | ||||
|  | ||||
|       amount: { | ||||
|         required: helpers.withMessage(t('validation.required'), required), | ||||
|         minValue: helpers.withMessage( | ||||
|           t('validation.price_minvalue'), | ||||
|           minValue(0.1) | ||||
|         ), | ||||
|         maxLength: helpers.withMessage( | ||||
|           t('validation.price_maxlength'), | ||||
|           maxLength(20) | ||||
|         ), | ||||
|       }, | ||||
|  | ||||
|       notes: { | ||||
|         maxLength: helpers.withMessage( | ||||
|           t('validation.description_maxlength'), | ||||
|           maxLength(65000) | ||||
|         ), | ||||
|       }, | ||||
|       currency_id: { | ||||
|         required: helpers.withMessage(t('validation.required'), required), | ||||
|       }, | ||||
|       exchange_rate: { | ||||
|         required: requiredIf(function () { | ||||
|           helpers.withMessage(t('validation.required'), required) | ||||
|           return expenseStore.showExchangeRate | ||||
|         }), | ||||
|         decimal: helpers.withMessage( | ||||
|           t('validation.valid_exchange_rate'), | ||||
|           decimal | ||||
|         ), | ||||
|       }, | ||||
|     }, | ||||
|   } | ||||
| }) | ||||
|  | ||||
| const v$ = useVuelidate(rules, expenseStore, { | ||||
|   $scope: expenseValidationScope, | ||||
| }) | ||||
|  | ||||
| const amountData = computed({ | ||||
|   get: () => expenseStore.currentExpense.amount / 100, | ||||
|   set: (value) => { | ||||
|     expenseStore.currentExpense.amount = Math.round(value * 100) | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| const isEdit = computed(() => route.name === 'expenses.edit') | ||||
|  | ||||
| const pageTitle = computed(() => | ||||
|   isEdit.value ? t('expenses.edit_expense') : t('expenses.new_expense') | ||||
| ) | ||||
|  | ||||
| const receiptDownloadUrl = computed(() => | ||||
|   isEdit.value ? `/expenses/${route.params.id}/download-receipt` : '' | ||||
| ) | ||||
|  | ||||
| expenseStore.resetCurrentExpenseData() | ||||
| customFieldStore.resetCustomFields() | ||||
|  | ||||
| loadData() | ||||
|  | ||||
| function onFileInputChange(fileName, file) { | ||||
|   expenseStore.currentExpense.attachment_receipt = file | ||||
| } | ||||
|  | ||||
| function onFileInputRemove() { | ||||
|   expenseStore.currentExpense.attachment_receipt = null | ||||
| } | ||||
|  | ||||
| function openCategoryModal() { | ||||
|   modalStore.openModal({ | ||||
|     title: t('settings.expense_category.add_category'), | ||||
|     componentName: 'CategoryModal', | ||||
|     size: 'sm', | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function onCurrencyChange(v) { | ||||
|   expenseStore.currentExpense.selectedCurrency = globalStore.currencies.find( | ||||
|     (c) => c.id === v | ||||
|   ) | ||||
| } | ||||
|  | ||||
| async function searchCategory(search) { | ||||
|   let res = await categoryStore.fetchCategories({ search }) | ||||
|   return res.data.data | ||||
| } | ||||
|  | ||||
| async function searchCustomer(search) { | ||||
|   let res = await customerStore.fetchCustomers({ search }) | ||||
|   return res.data.data | ||||
| } | ||||
|  | ||||
| async function loadData() { | ||||
|   expenseStore.currentExpense.currency_id = companyStore.selectedCompanyCurrency.id | ||||
|   expenseStore.currentExpense.selectedCurrency = companyStore.selectedCompanyCurrency | ||||
|  | ||||
|   isFetchingInitialData.value = true | ||||
|   await expenseStore.fetchPaymentModes({ limit: 'all' }) | ||||
|  | ||||
|   if (isEdit.value) { | ||||
|     await expenseStore.fetchExpense(route.params.id) | ||||
|   } else if (route.query.customer) { | ||||
|     expenseStore.currentExpense.customer_id = route.query.customer | ||||
|   } | ||||
|  | ||||
|   isFetchingInitialData.value = false | ||||
| } | ||||
|  | ||||
| async function submitForm() { | ||||
|   v$.value.$touch() | ||||
|  | ||||
|   if (v$.value.$invalid) { | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   isSaving.value = true | ||||
|  | ||||
|   let formData = expenseStore.currentExpense | ||||
|  | ||||
|   try { | ||||
|     if (isEdit.value) { | ||||
|       await expenseStore.updateExpense({ | ||||
|         id: route.params.id, | ||||
|         data: formData, | ||||
|       }) | ||||
|     } else { | ||||
|       await expenseStore.addExpense(formData) | ||||
|     } | ||||
|     isSaving.value = false | ||||
|     router.push('/admin/expenses') | ||||
|   } catch (err) { | ||||
|     console.error(err) | ||||
|     isSaving.value = false | ||||
|     return | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										399
									
								
								resources/scripts/views/expenses/Index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								resources/scripts/views/expenses/Index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,399 @@ | ||||
| <template> | ||||
|   <BasePage> | ||||
|     <!-- Page Header --> | ||||
|     <BasePageHeader :title="$t('expenses.title')"> | ||||
|       <BaseBreadcrumb> | ||||
|         <BaseBreadcrumbItem :title="$t('general.home')" to="dashboard" /> | ||||
|         <BaseBreadcrumbItem :title="$tc('expenses.expense', 2)" to="#" active /> | ||||
|       </BaseBreadcrumb> | ||||
|  | ||||
|       <template #actions> | ||||
|         <BaseButton | ||||
|           v-show="expenseStore.totalExpenses" | ||||
|           variant="primary-outline" | ||||
|           @click="toggleFilter" | ||||
|         > | ||||
|           {{ $t('general.filter') }} | ||||
|           <template #right="slotProps"> | ||||
|             <BaseIcon | ||||
|               v-if="!showFilters" | ||||
|               name="FilterIcon" | ||||
|               :class="slotProps.class" | ||||
|             /> | ||||
|             <BaseIcon v-else name="XIcon" :class="slotProps.class" /> | ||||
|           </template> | ||||
|         </BaseButton> | ||||
|  | ||||
|         <BaseButton | ||||
|           v-if="userStore.hasAbilities(abilities.CREATE_EXPENSE)" | ||||
|           class="ml-4" | ||||
|           variant="primary" | ||||
|           @click="$router.push('expenses/create')" | ||||
|         > | ||||
|           <template #left="slotProps"> | ||||
|             <BaseIcon name="PlusIcon" :class="slotProps.class" /> | ||||
|           </template> | ||||
|           {{ $t('expenses.add_expense') }} | ||||
|         </BaseButton> | ||||
|       </template> | ||||
|     </BasePageHeader> | ||||
|  | ||||
|     <BaseFilterWrapper :show="showFilters" class="mt-5" @clear="clearFilter"> | ||||
|       <BaseInputGroup :label="$t('expenses.customer')"> | ||||
|         <BaseCustomerSelectInput | ||||
|           v-model="filters.customer_id" | ||||
|           :placeholder="$t('customers.type_or_click')" | ||||
|           value-prop="id" | ||||
|           label="name" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup :label="$t('expenses.category')"> | ||||
|         <BaseMultiselect | ||||
|           v-model="filters.expense_category_id" | ||||
|           value-prop="id" | ||||
|           label="name" | ||||
|           track-by="name" | ||||
|           :filter-results="false" | ||||
|           resolve-on-load | ||||
|           :delay="500" | ||||
|           :options="searchCategory" | ||||
|           searchable | ||||
|           :placeholder="$t('expenses.categories.select_a_category')" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup :label="$t('expenses.from_date')"> | ||||
|         <BaseDatePicker | ||||
|           v-model="filters.from_date" | ||||
|           :calendar-button="true" | ||||
|           calendar-button-icon="calendar" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|       <div | ||||
|         class="hidden w-8 h-0 mx-4 border border-gray-400 border-solid xl:block" | ||||
|         style="margin-top: 1.5rem" | ||||
|       /> | ||||
|  | ||||
|       <BaseInputGroup :label="$t('expenses.to_date')"> | ||||
|         <BaseDatePicker | ||||
|           v-model="filters.to_date" | ||||
|           :calendar-button="true" | ||||
|           calendar-button-icon="calendar" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|     </BaseFilterWrapper> | ||||
|  | ||||
|     <!-- Empty Table Placeholder --> | ||||
|     <BaseEmptyPlaceholder | ||||
|       v-show="showEmptyScreen" | ||||
|       :title="$t('expenses.no_expenses')" | ||||
|       :description="$t('expenses.list_of_expenses')" | ||||
|     > | ||||
|       <UFOIcon class="mt-5 mb-4" /> | ||||
|  | ||||
|       <template | ||||
|         v-if="userStore.hasAbilities(abilities.CREATE_EXPENSE)" | ||||
|         #actions | ||||
|       > | ||||
|         <BaseButton | ||||
|           variant="primary-outline" | ||||
|           @click="$router.push('/admin/expenses/create')" | ||||
|         > | ||||
|           <template #left="slotProps"> | ||||
|             <BaseIcon name="PlusIcon" :class="slotProps.class" /> | ||||
|           </template> | ||||
|           {{ $t('expenses.add_new_expense') }} | ||||
|         </BaseButton> | ||||
|       </template> | ||||
|     </BaseEmptyPlaceholder> | ||||
|  | ||||
|     <div v-show="!showEmptyScreen" class="relative table-container"> | ||||
|       <div class="relative flex items-center justify-end h-5"> | ||||
|         <BaseDropdown | ||||
|           v-if=" | ||||
|             expenseStore.selectedExpenses.length && | ||||
|             userStore.hasAbilities(abilities.DELETE_EXPENSE) | ||||
|           " | ||||
|         > | ||||
|           <template #activator> | ||||
|             <span | ||||
|               class=" | ||||
|                 flex | ||||
|                 text-sm | ||||
|                 font-medium | ||||
|                 cursor-pointer | ||||
|                 select-none | ||||
|                 text-primary-400 | ||||
|               " | ||||
|             > | ||||
|               {{ $t('general.actions') }} | ||||
|               <BaseIcon name="ChevronDownIcon" /> | ||||
|             </span> | ||||
|           </template> | ||||
|  | ||||
|           <BaseDropdownItem | ||||
|             v-if="userStore.hasAbilities(abilities.DELETE_EXPENSE)" | ||||
|             @click="removeMultipleExpenses" | ||||
|           > | ||||
|             <BaseIcon name="TrashIcon" class="h-5 mr-3 text-gray-600" /> | ||||
|             {{ $t('general.delete') }} | ||||
|           </BaseDropdownItem> | ||||
|         </BaseDropdown> | ||||
|       </div> | ||||
|  | ||||
|       <BaseTable | ||||
|         ref="tableComponent" | ||||
|         :data="fetchData" | ||||
|         :columns="expenseColumns" | ||||
|         class="mt-3" | ||||
|       > | ||||
|         <!-- Select All Checkbox --> | ||||
|         <template #header> | ||||
|           <div class="absolute items-center left-6 top-2.5 select-none"> | ||||
|             <BaseCheckbox | ||||
|               v-model="selectAllFieldStatus" | ||||
|               variant="primary" | ||||
|               @change="expenseStore.selectAllExpenses" | ||||
|             /> | ||||
|           </div> | ||||
|         </template> | ||||
|         <template #cell-status="{ row }"> | ||||
|           <div class="relative block"> | ||||
|             <BaseCheckbox | ||||
|               :id="row.id" | ||||
|               v-model="selectField" | ||||
|               :value="row.data.id" | ||||
|               variant="primary" | ||||
|             /> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-name="{ row }"> | ||||
|           <router-link | ||||
|             :to="{ path: `expenses/${row.data.id}/edit` }" | ||||
|             class="font-medium text-primary-500" | ||||
|           > | ||||
|             {{ row.data.expense_category.name }} | ||||
|           </router-link> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-amount="{ row }"> | ||||
|           <BaseFormatMoney | ||||
|             :amount="row.data.amount" | ||||
|             :currency="companyStore.selectedCompanyCurrency" | ||||
|           /> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-user_name="{ row }"> | ||||
|           {{ row.data.customer ? row.data.customer.name : '-' }} | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-notes="{ row }"> | ||||
|           <div class="notes"> | ||||
|             <div class="truncate note w-60"> | ||||
|               {{ row.data.notes ? row.data.notes : '-' }} | ||||
|             </div> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|         <template v-if="hasAbilities()" #cell-actions="{ row }"> | ||||
|           <ExpenseDropdown | ||||
|             :row="row.data" | ||||
|             :table="tableComponent" | ||||
|             :load-data="refreshTable" | ||||
|           /> | ||||
|         </template> | ||||
|       </BaseTable> | ||||
|     </div> | ||||
|   </BasePage> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { ref, onMounted, computed, reactive, onUnmounted } from 'vue' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { useExpenseStore } from '@/scripts/stores/expense' | ||||
| import { useCategoryStore } from '@/scripts/stores/category' | ||||
| import { useDialogStore } from '@/scripts/stores/dialog' | ||||
| import { useCompanyStore } from '@/scripts/stores/company' | ||||
| import { debouncedWatch } from '@vueuse/core' | ||||
| import { useUserStore } from '@/scripts/stores/user' | ||||
|  | ||||
| import abilities from '@/scripts/stub/abilities' | ||||
|  | ||||
| import UFOIcon from '@/scripts/components/icons/empty/UFOIcon.vue' | ||||
| import ExpenseDropdown from '@/scripts/components/dropdowns/ExpenseIndexDropdown.vue' | ||||
|  | ||||
| const companyStore = useCompanyStore() | ||||
| const expenseStore = useExpenseStore() | ||||
| const dialogStore = useDialogStore() | ||||
| const categoryStore = useCategoryStore() | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| let isFetchingInitialData = ref(true) | ||||
| let showFilters = ref(null) | ||||
|  | ||||
| const filters = reactive({ | ||||
|   expense_category_id: '', | ||||
|   from_date: '', | ||||
|   to_date: '', | ||||
|   customer_id: '', | ||||
| }) | ||||
|  | ||||
| const { t } = useI18n() | ||||
| let tableComponent = ref(null) | ||||
|  | ||||
| const showEmptyScreen = computed(() => { | ||||
|   return !expenseStore.totalExpenses && !isFetchingInitialData.value | ||||
| }) | ||||
|  | ||||
| const selectField = computed({ | ||||
|   get: () => expenseStore.selectedExpenses, | ||||
|   set: (value) => { | ||||
|     return expenseStore.selectExpense(value) | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| const selectAllFieldStatus = computed({ | ||||
|   get: () => expenseStore.selectAllField, | ||||
|   set: (value) => { | ||||
|     return expenseStore.setSelectAllState(value) | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| const expenseColumns = computed(() => { | ||||
|   return [ | ||||
|     { | ||||
|       key: 'status', | ||||
|       thClass: 'extra w-10', | ||||
|       tdClass: 'font-medium text-gray-900', | ||||
|       placeholderClass: 'w-10', | ||||
|       sortable: false, | ||||
|     }, | ||||
|     { | ||||
|       key: 'expense_date', | ||||
|       label: 'Date', | ||||
|       thClass: 'extra', | ||||
|       tdClass: 'font-medium text-gray-900', | ||||
|     }, | ||||
|     { | ||||
|       key: 'name', | ||||
|       label: 'Category', | ||||
|       thClass: 'extra', | ||||
|       tdClass: 'cursor-pointer font-medium text-primary-500', | ||||
|     }, | ||||
|     { key: 'user_name', label: 'Customer' }, | ||||
|     { key: 'notes', label: 'Note' }, | ||||
|     { key: 'amount', label: 'Amount' }, | ||||
|     { | ||||
|       key: 'actions', | ||||
|       sortable: false, | ||||
|       tdClass: 'text-right text-sm font-medium', | ||||
|     }, | ||||
|   ] | ||||
| }) | ||||
|  | ||||
| debouncedWatch( | ||||
|   filters, | ||||
|   () => { | ||||
|     setFilters() | ||||
|   }, | ||||
|   { debounce: 500 } | ||||
| ) | ||||
|  | ||||
| onUnmounted(() => { | ||||
|   if (expenseStore.selectAllField) { | ||||
|     expenseStore.selectAllExpenses() | ||||
|   } | ||||
| }) | ||||
|  | ||||
| onMounted(() => { | ||||
|   categoryStore.fetchCategories({ limit: 'all' }) | ||||
| }) | ||||
|  | ||||
| async function searchCategory(search) { | ||||
|   let res = await categoryStore.fetchCategories({ search }) | ||||
|   return res.data.data | ||||
| } | ||||
|  | ||||
| async function fetchData({ page, filter, sort }) { | ||||
|   let data = { | ||||
|     ...filters, | ||||
|  | ||||
|     orderByField: sort.fieldName || 'created_at', | ||||
|  | ||||
|     orderBy: sort.order || 'desc', | ||||
|  | ||||
|     page, | ||||
|   } | ||||
|  | ||||
|   isFetchingInitialData.value = true | ||||
|  | ||||
|   let response = await expenseStore.fetchExpenses(data) | ||||
|   isFetchingInitialData.value = false | ||||
|  | ||||
|   return { | ||||
|     data: response.data.data, | ||||
|  | ||||
|     pagination: { | ||||
|       data: response.data.data, | ||||
|       totalPages: response.data.meta.last_page, | ||||
|       currentPage: page, | ||||
|       totalCount: response.data.meta.total, | ||||
|       limit: 10, | ||||
|     }, | ||||
|   } | ||||
| } | ||||
|  | ||||
| function refreshTable() { | ||||
|   tableComponent.value && tableComponent.value.refresh() | ||||
| } | ||||
|  | ||||
| function setFilters() { | ||||
|   refreshTable() | ||||
| } | ||||
|  | ||||
| function clearFilter() { | ||||
|   filters.expense_category_id = '' | ||||
|   filters.from_date = '' | ||||
|   filters.to_date = '' | ||||
|   filters.customer_id = '' | ||||
| } | ||||
|  | ||||
| function toggleFilter() { | ||||
|   if (showFilters.value) { | ||||
|     clearFilter() | ||||
|   } | ||||
|  | ||||
|   showFilters.value = !showFilters.value | ||||
| } | ||||
|  | ||||
| function hasAbilities() { | ||||
|   return userStore.hasAbilities([ | ||||
|     abilities.DELETE_EXPENSE, | ||||
|     abilities.EDIT_EXPENSE, | ||||
|   ]) | ||||
| } | ||||
|  | ||||
| function removeMultipleExpenses() { | ||||
|   dialogStore | ||||
|     .openDialog({ | ||||
|       title: t('general.are_you_sure'), | ||||
|       message: t('expenses.confirm_delete', 2), | ||||
|       yesLabel: t('general.ok'), | ||||
|       noLabel: t('general.cancel'), | ||||
|       variant: 'danger', | ||||
|       size: 'lg', | ||||
|       hideNoButton: false, | ||||
|     }) | ||||
|     .then((res) => { | ||||
|       if (res) { | ||||
|         expenseStore.deleteMultipleExpenses().then((response) => { | ||||
|           if (response.data) { | ||||
|             refreshTable() | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
| } | ||||
| </script> | ||||
		Reference in New Issue
	
	Block a user