mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-10-30 13:11:08 -04:00 
			
		
		
		
	v5.0.0 update
This commit is contained in:
		
							
								
								
									
										480
									
								
								resources/scripts/views/invoices/Index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										480
									
								
								resources/scripts/views/invoices/Index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,480 @@ | ||||
| <template> | ||||
|   <BasePage> | ||||
|     <SendInvoiceModal /> | ||||
|     <BasePageHeader :title="$t('invoices.title')"> | ||||
|       <BaseBreadcrumb> | ||||
|         <BaseBreadcrumbItem :title="$t('general.home')" to="dashboard" /> | ||||
|         <BaseBreadcrumbItem :title="$tc('invoices.invoice', 2)" to="#" active /> | ||||
|       </BaseBreadcrumb> | ||||
|  | ||||
|       <template #actions> | ||||
|         <BaseButton | ||||
|           v-show="invoiceStore.invoiceTotalCount" | ||||
|           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> | ||||
|  | ||||
|         <router-link | ||||
|           v-if="userStore.hasAbilities(abilities.CREATE_INVOICE)" | ||||
|           to="invoices/create" | ||||
|         > | ||||
|           <BaseButton variant="primary" class="ml-4"> | ||||
|             <template #left="slotProps"> | ||||
|               <BaseIcon name="PlusIcon" :class="slotProps.class" /> | ||||
|             </template> | ||||
|             {{ $t('invoices.new_invoice') }} | ||||
|           </BaseButton> | ||||
|         </router-link> | ||||
|       </template> | ||||
|     </BasePageHeader> | ||||
|  | ||||
|     <BaseFilterWrapper v-show="showFilters" @clear="clearFilter"> | ||||
|       <BaseInputGroup :label="$tc('customers.customer', 1)"> | ||||
|         <BaseCustomerSelectInput | ||||
|           v-model="filters.customer_id" | ||||
|           :placeholder="$t('customers.type_or_click')" | ||||
|           value-prop="id" | ||||
|           label="name" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup :label="$t('general.from')"> | ||||
|         <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('general.to')" class="mt-2"> | ||||
|         <BaseDatePicker | ||||
|           v-model="filters.to_date" | ||||
|           :calendar-button="true" | ||||
|           calendar-button-icon="calendar" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup :label="$t('invoices.invoice_number')"> | ||||
|         <BaseInput v-model="filters.invoice_number"> | ||||
|           <template #left="slotProps"> | ||||
|             <BaseIcon name="HashtagIcon" :class="slotProps.class" /> | ||||
|           </template> | ||||
|         </BaseInput> | ||||
|       </BaseInputGroup> | ||||
|     </BaseFilterWrapper> | ||||
|  | ||||
|     <BaseEmptyPlaceholder | ||||
|       v-show="showEmptyScreen" | ||||
|       :title="$t('invoices.no_invoices')" | ||||
|       :description="$t('invoices.list_of_invoices')" | ||||
|     > | ||||
|       <MoonwalkerIcon class="mt-5 mb-4" /> | ||||
|       <template | ||||
|         v-if="userStore.hasAbilities(abilities.CREATE_INVOICE)" | ||||
|         #actions | ||||
|       > | ||||
|         <BaseButton | ||||
|           variant="primary-outline" | ||||
|           @click="$router.push('/admin/invoices/create')" | ||||
|         > | ||||
|           <template #left="slotProps"> | ||||
|             <BaseIcon name="PlusIcon" :class="slotProps.class" /> | ||||
|           </template> | ||||
|           {{ $t('invoices.add_new_invoice') }} | ||||
|         </BaseButton> | ||||
|       </template> | ||||
|     </BaseEmptyPlaceholder> | ||||
|  | ||||
|     <div v-show="!showEmptyScreen" class="relative table-container"> | ||||
|       <div | ||||
|         class=" | ||||
|           relative | ||||
|           flex | ||||
|           items-center | ||||
|           justify-between | ||||
|           h-10 | ||||
|           mt-5 | ||||
|           list-none | ||||
|           border-b-2 border-gray-200 border-solid | ||||
|         " | ||||
|       > | ||||
|         <!-- Tabs --> | ||||
|         <BaseTabGroup class="-mb-5" @change="setStatusFilter"> | ||||
|           <BaseTab :title="$t('general.draft')" filter="DRAFT" /> | ||||
|           <BaseTab :title="$t('general.sent')" filter="SENT" /> | ||||
|           <BaseTab :title="$t('general.all')" filter="" /> | ||||
|         </BaseTabGroup> | ||||
|  | ||||
|         <BaseDropdown | ||||
|           v-if=" | ||||
|             invoiceStore.selectedInvoices.length && | ||||
|             userStore.hasAbilities(abilities.DELETE_INVOICE) | ||||
|           " | ||||
|           class="absolute float-right" | ||||
|         > | ||||
|           <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 @click="removeMultipleInvoices"> | ||||
|             <BaseIcon name="TrashIcon" class="mr-3 text-gray-600" /> | ||||
|             {{ $t('general.delete') }} | ||||
|           </BaseDropdownItem> | ||||
|         </BaseDropdown> | ||||
|       </div> | ||||
|  | ||||
|       <BaseTable | ||||
|         ref="table" | ||||
|         :data="fetchData" | ||||
|         :columns="invoiceColumns" | ||||
|         :placeholder-count="invoiceStore.invoiceTotalCount >= 20 ? 10 : 5" | ||||
|         class="mt-10" | ||||
|       > | ||||
|         <!-- Select All Checkbox --> | ||||
|         <template #header> | ||||
|           <div class="absolute items-center left-6 top-2.5 select-none"> | ||||
|             <BaseCheckbox | ||||
|               v-model="invoiceStore.selectAllField" | ||||
|               variant="primary" | ||||
|               @change="invoiceStore.selectAllInvoices" | ||||
|             /> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-checkbox="{ row }"> | ||||
|           <div class="relative block"> | ||||
|             <BaseCheckbox | ||||
|               :id="row.id" | ||||
|               v-model="selectField" | ||||
|               :value="row.data.id" | ||||
|             /> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-name="{ row }"> | ||||
|           {{ row.data.customer.name }} | ||||
|         </template> | ||||
|  | ||||
|         <!-- Invoice Number  --> | ||||
|         <template #cell-invoice_number="{ row }"> | ||||
|           <router-link | ||||
|             :to="{ path: `invoices/${row.data.id}/view` }" | ||||
|             class="font-medium text-primary-500" | ||||
|           > | ||||
|             {{ row.data.invoice_number }} | ||||
|           </router-link> | ||||
|         </template> | ||||
|  | ||||
|         <!-- Invoice date  --> | ||||
|         <template #cell-invoice_date="{ row }"> | ||||
|             {{ row.data.formatted_invoice_date }} | ||||
|         </template> | ||||
|  | ||||
|         <!-- Invoice Total  --> | ||||
|         <template #cell-total="{ row }"> | ||||
|           <BaseFormatMoney | ||||
|             :amount="row.data.total" | ||||
|             :currency="row.data.customer.currency" | ||||
|           /> | ||||
|         </template> | ||||
|  | ||||
|         <!-- Invoice status  --> | ||||
|         <template #cell-status="{ row }"> | ||||
|           <BaseInvoiceStatusBadge :status="row.data.status" class="px-3 py-1"> | ||||
|             {{ row.data.status }} | ||||
|           </BaseInvoiceStatusBadge> | ||||
|         </template> | ||||
|  | ||||
|         <!-- Due Amount + Paid Status  --> | ||||
|         <template #cell-due_amount="{ row }"> | ||||
|           <div class="flex justify-between"> | ||||
|             <BaseFormatMoney | ||||
|               :amount="row.data.due_amount" | ||||
|               :currency="row.data.currency" | ||||
|             /> | ||||
|  | ||||
|             <BasePaidStatusBadge | ||||
|               :status="row.data.paid_status" | ||||
|               class="px-1 py-0.5 ml-2" | ||||
|             > | ||||
|               {{ row.data.paid_status }} | ||||
|             </BasePaidStatusBadge> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|         <!-- Actions --> | ||||
|         <template v-if="hasAtleastOneAbility()" #cell-actions="{ row }"> | ||||
|           <InvoiceDropdown :row="row.data" :table="table" /> | ||||
|         </template> | ||||
|       </BaseTable> | ||||
|     </div> | ||||
|   </BasePage> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { computed, onUnmounted, reactive, ref, watch, inject } from 'vue' | ||||
| import { useRouter } from 'vue-router' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { useInvoiceStore } from '@/scripts/stores/invoice' | ||||
| import { useNotificationStore } from '@/scripts/stores/notification' | ||||
| import { useDialogStore } from '@/scripts/stores/dialog' | ||||
| import { useUserStore } from '@/scripts/stores/user' | ||||
| import abilities from '@/scripts/stub/abilities' | ||||
| import { debouncedWatch } from '@vueuse/core' | ||||
|  | ||||
| import MoonwalkerIcon from '@/scripts/components/icons/empty/MoonwalkerIcon.vue' | ||||
| import InvoiceDropdown from '@/scripts/components/dropdowns/InvoiceIndexDropdown.vue' | ||||
| import SendInvoiceModal from '@/scripts/components/modal-components/SendInvoiceModal.vue' | ||||
| // Stores | ||||
| const invoiceStore = useInvoiceStore() | ||||
| const dialogStore = useDialogStore() | ||||
| const notificationStore = useNotificationStore() | ||||
|  | ||||
| const { t } = useI18n() | ||||
|  | ||||
| // Local State | ||||
| const utils = inject('$utils') | ||||
| const table = ref(null) | ||||
| const showFilters = ref(false) | ||||
|  | ||||
| const status = ref([ | ||||
|   'DRAFT', | ||||
|   'SENT', | ||||
|   'VIEWED', | ||||
|   'EXPIRED', | ||||
|   'ACCEPTED', | ||||
|   'REJECTED', | ||||
| ]) | ||||
| const isRequestOngoing = ref(true) | ||||
| const activeTab = ref('general.draft') | ||||
| const router = useRouter() | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| let filters = reactive({ | ||||
|   customer_id: '', | ||||
|   status: 'DRAFT', | ||||
|   from_date: '', | ||||
|   to_date: '', | ||||
|   invoice_number: '', | ||||
| }) | ||||
|  | ||||
| const showEmptyScreen = computed( | ||||
|   () => !invoiceStore.invoiceTotalCount && !isRequestOngoing.value | ||||
| ) | ||||
|  | ||||
| const selectField = computed({ | ||||
|   get: () => invoiceStore.selectedInvoices, | ||||
|   set: (value) => { | ||||
|     return invoiceStore.selectInvoice(value) | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| const invoiceColumns = computed(() => { | ||||
|   return [ | ||||
|     { | ||||
|       key: 'checkbox', | ||||
|       thClass: 'extra w-10', | ||||
|       tdClass: 'font-medium text-gray-900', | ||||
|       placeholderClass: 'w-10', | ||||
|       sortable: false, | ||||
|     }, | ||||
|     { | ||||
|       key: 'invoice_date', | ||||
|       label: t('invoices.date'), | ||||
|       thClass: 'extra', | ||||
|       tdClass: 'font-medium', | ||||
|     }, | ||||
|     { key: 'invoice_number', label: t('invoices.number') }, | ||||
|     { key: 'name', label: t('invoices.customer') }, | ||||
|     { key: 'status', label: t('invoices.status') }, | ||||
|     { | ||||
|       key: 'due_amount', | ||||
|       label: t('dashboard.recent_invoices_card.amount_due'), | ||||
|     }, | ||||
|     { | ||||
|       key: 'total', | ||||
|       label: t('invoices.total'), | ||||
|       tdClass: 'font-medium text-gray-900', | ||||
|     }, | ||||
|  | ||||
|     { | ||||
|       key: 'actions', | ||||
|       label: t('invoices.action'), | ||||
|       tdClass: 'text-right text-sm font-medium', | ||||
|       thClass: 'text-right', | ||||
|       sortable: false, | ||||
|     }, | ||||
|   ] | ||||
| }) | ||||
|  | ||||
| debouncedWatch( | ||||
|   filters, | ||||
|   () => { | ||||
|     setFilters() | ||||
|   }, | ||||
|   { debounce: 500 } | ||||
| ) | ||||
|  | ||||
| onUnmounted(() => { | ||||
|   if (invoiceStore.selectAllField) { | ||||
|     invoiceStore.selectAllInvoices() | ||||
|   } | ||||
| }) | ||||
|  | ||||
| function hasAtleastOneAbility() { | ||||
|   return userStore.hasAbilities([ | ||||
|     abilities.DELETE_INVOICE, | ||||
|     abilities.EDIT_INVOICE, | ||||
|     abilities.VIEW_INVOICE, | ||||
|     abilities.SEND_INVOICE, | ||||
|   ]) | ||||
| } | ||||
|  | ||||
| function refreshTable() { | ||||
|   table.value && table.value.refresh() | ||||
| } | ||||
|  | ||||
| async function fetchData({ page, filter, sort }) { | ||||
|   let data = { | ||||
|     customer_id: filters.customer_id, | ||||
|     status: filters.status, | ||||
|     from_date: filters.from_date, | ||||
|     to_date: filters.to_date, | ||||
|     invoice_number: filters.invoice_number, | ||||
|     orderByField: sort.fieldName || 'created_at', | ||||
|     orderBy: sort.order || 'desc', | ||||
|     page, | ||||
|   } | ||||
|  | ||||
|   isRequestOngoing.value = true | ||||
|  | ||||
|   let response = await invoiceStore.fetchInvoices(data) | ||||
|  | ||||
|   isRequestOngoing.value = false | ||||
|  | ||||
|   return { | ||||
|     data: response.data.data, | ||||
|     pagination: { | ||||
|       totalPages: response.data.meta.last_page, | ||||
|       currentPage: page, | ||||
|       totalCount: response.data.meta.total, | ||||
|       limit: 10, | ||||
|     }, | ||||
|   } | ||||
| } | ||||
|  | ||||
| function setStatusFilter(val) { | ||||
|   if (activeTab.value == val.title) { | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   activeTab.value = val.title | ||||
|  | ||||
|   switch (val.title) { | ||||
|     case t('general.draft'): | ||||
|       filters.status = 'DRAFT' | ||||
|       break | ||||
|     case t('general.sent'): | ||||
|       filters.status = 'SENT' | ||||
|       break | ||||
|     default: | ||||
|       filters.status = '' | ||||
|       break | ||||
|   } | ||||
| } | ||||
|  | ||||
| function setFilters() { | ||||
|   invoiceStore.$patch((state) => { | ||||
|     state.selectedInvoices = [] | ||||
|     state.selectAllField = false | ||||
|   }) | ||||
|  | ||||
|   refreshTable() | ||||
| } | ||||
|  | ||||
| function clearFilter() { | ||||
|   filters.customer_id = '' | ||||
|   filters.status = '' | ||||
|   filters.from_date = '' | ||||
|   filters.to_date = '' | ||||
|   filters.invoice_number = '' | ||||
|  | ||||
|   activeTab.value = t('general.all') | ||||
| } | ||||
|  | ||||
| async function removeMultipleInvoices() { | ||||
|   dialogStore | ||||
|     .openDialog({ | ||||
|       title: t('general.are_you_sure'), | ||||
|       message: t('invoices.confirm_delete'), | ||||
|       yesLabel: t('general.ok'), | ||||
|       noLabel: t('general.cancel'), | ||||
|       variant: 'danger', | ||||
|       hideNoButton: false, | ||||
|       size: 'lg', | ||||
|     }) | ||||
|     .then(async (res) => { | ||||
|       if (res) { | ||||
|         await invoiceStore.deleteMultipleInvoices().then((res) => { | ||||
|           if (res.data.success) { | ||||
|             refreshTable() | ||||
|  | ||||
|             invoiceStore.$patch((state) => { | ||||
|               state.selectedInvoices = [] | ||||
|               state.selectAllField = false | ||||
|             }) | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| function toggleFilter() { | ||||
|   if (showFilters.value) { | ||||
|     clearFilter() | ||||
|   } | ||||
|  | ||||
|   showFilters.value = !showFilters.value | ||||
| } | ||||
|  | ||||
| function setActiveTab(val) { | ||||
|   switch (val) { | ||||
|     case 'DRAFT': | ||||
|       activeTab.value = t('general.draft') | ||||
|       break | ||||
|     case 'SENT': | ||||
|       activeTab.value = t('general.sent') | ||||
|       break | ||||
|     default: | ||||
|       activeTab.value = t('general.all') | ||||
|       break | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										481
									
								
								resources/scripts/views/invoices/View.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										481
									
								
								resources/scripts/views/invoices/View.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,481 @@ | ||||
| <script setup> | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { computed, reactive, ref, watch, inject } from 'vue' | ||||
| import InvoiceDropdown from '@/scripts/components/dropdowns/InvoiceIndexDropdown.vue' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import { debounce } from 'lodash' | ||||
| import { useInvoiceStore } from '@/scripts/stores/invoice' | ||||
| import { useModalStore } from '@/scripts/stores/modal' | ||||
| import { useNotificationStore } from '@/scripts/stores/notification' | ||||
| import { useUserStore } from '@/scripts/stores/user' | ||||
| import { useDialogStore } from '@/scripts/stores/dialog' | ||||
| import SendInvoiceModal from '@/scripts/components/modal-components/SendInvoiceModal.vue' | ||||
| import LoadingIcon from '@/scripts/components/icons/LoadingIcon.vue' | ||||
| import abilities from '@/scripts/stub/abilities' | ||||
|  | ||||
| const modalStore = useModalStore() | ||||
| const invoiceStore = useInvoiceStore() | ||||
| const notificationStore = useNotificationStore() | ||||
| const userStore = useUserStore() | ||||
| const dialogStore = useDialogStore() | ||||
|  | ||||
| const { t } = useI18n() | ||||
| const utils = inject('$utils') | ||||
| const id = ref(null) | ||||
| const count = ref(null) | ||||
| const invoiceData = ref(null) | ||||
| const currency = ref(null) | ||||
| const route = useRoute() | ||||
| const router = useRouter() | ||||
| const status = ref([ | ||||
|   'DRAFT', | ||||
|   'SENT', | ||||
|   'VIEWED', | ||||
|   'EXPIRED', | ||||
|   'ACCEPTED', | ||||
|   'REJECTED', | ||||
| ]) | ||||
| const isMarkAsSent = ref(false) | ||||
| const isSendingEmail = ref(false) | ||||
| const isRequestOnGoing = ref(false) | ||||
| const isSearching = ref(false) | ||||
| const isLoading = ref(false) | ||||
|  | ||||
| const searchData = reactive({ | ||||
|   orderBy: null, | ||||
|   orderByField: null, | ||||
|   searchText: null, | ||||
| }) | ||||
|  | ||||
| const pageTitle = computed(() => invoiceData.value.invoice_number) | ||||
|  | ||||
| const getOrderBy = computed(() => { | ||||
|   if (searchData.orderBy === 'asc' || searchData.orderBy == null) { | ||||
|     return true | ||||
|   } | ||||
|   return false | ||||
| }) | ||||
|  | ||||
| const getOrderName = computed(() => { | ||||
|   if (getOrderBy.value) { | ||||
|     return t('general.ascending') | ||||
|   } | ||||
|   return t('general.descending') | ||||
| }) | ||||
|  | ||||
| const shareableLink = computed(() => { | ||||
|   return `/invoices/pdf/${invoiceData.value.unique_hash}` | ||||
| }) | ||||
|  | ||||
| const getCurrentInvoiceId = computed(() => { | ||||
|   if (invoiceData.value && invoiceData.value.id) { | ||||
|     return invoice.value.id | ||||
|   } | ||||
|   return null | ||||
| }) | ||||
|  | ||||
| watch(route, (to, from) => { | ||||
|   if (to.name === 'invoices.view') { | ||||
|     loadInvoice() | ||||
|   } | ||||
| }) | ||||
|  | ||||
| async function onMarkAsSent() { | ||||
|   dialogStore | ||||
|     .openDialog({ | ||||
|       title: t('general.are_you_sure'), | ||||
|       message: t('invoices.invoice_mark_as_sent'), | ||||
|       yesLabel: t('general.ok'), | ||||
|       noLabel: t('general.cancel'), | ||||
|       variant: 'primary', | ||||
|       hideNoButton: false, | ||||
|       size: 'lg', | ||||
|     }) | ||||
|     .then(async (response) => { | ||||
|       isMarkAsSent.value = false | ||||
|       if (response) { | ||||
|         await invoiceStore.markAsSent({ | ||||
|           id: invoiceData.value.id, | ||||
|           status: 'SENT', | ||||
|         }) | ||||
|         invoiceData.value.status = 'SENT' | ||||
|         isMarkAsSent.value = true | ||||
|       } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| async function onSendInvoice(id) { | ||||
|   modalStore.openModal({ | ||||
|     title: t('invoices.send_invoice'), | ||||
|     componentName: 'SendInvoiceModal', | ||||
|     id: invoiceData.value.id, | ||||
|     data: invoiceData.value, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function hasActiveUrl(id) { | ||||
|   return route.params.id == id | ||||
| } | ||||
|  | ||||
| async function loadInvoices() { | ||||
|   isLoading.value = true | ||||
|   await invoiceStore.fetchInvoices() | ||||
|   isLoading.value = false | ||||
|  | ||||
|   setTimeout(() => { | ||||
|     scrollToInvoice() | ||||
|   }, 500) | ||||
| } | ||||
|  | ||||
| function scrollToInvoice() { | ||||
|   const el = document.getElementById(`invoice-${route.params.id}`) | ||||
|   if (el) { | ||||
|     el.scrollIntoView({ behavior: 'smooth' }) | ||||
|     el.classList.add('shake') | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function loadInvoice() { | ||||
|   let response = await invoiceStore.fetchInvoice(route.params.id) | ||||
|   if (response.data) { | ||||
|     invoiceData.value = { ...response.data.data } | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function onSearched() { | ||||
|   let data = '' | ||||
|   if ( | ||||
|     searchData.searchText !== '' && | ||||
|     searchData.searchText !== null && | ||||
|     searchData.searchText !== undefined | ||||
|   ) { | ||||
|     data += `search=${searchData.searchText}&` | ||||
|   } | ||||
|  | ||||
|   if (searchData.orderBy !== null && searchData.orderBy !== undefined) { | ||||
|     data += `orderBy=${searchData.orderBy}&` | ||||
|   } | ||||
|   if ( | ||||
|     searchData.orderByField !== null && | ||||
|     searchData.orderByField !== undefined | ||||
|   ) { | ||||
|     data += `orderByField=${searchData.orderByField}` | ||||
|   } | ||||
|   isSearching.value = true | ||||
|   let response = await invoiceStore.searchInvoice(data) | ||||
|   isSearching.value = false | ||||
|   if (response.data) { | ||||
|     invoiceStore.invoices = response.data.data | ||||
|   } | ||||
| } | ||||
|  | ||||
| function sortData() { | ||||
|   if (searchData.orderBy === 'asc') { | ||||
|     searchData.orderBy = 'desc' | ||||
|     onSearched() | ||||
|     return true | ||||
|   } | ||||
|   searchData.orderBy = 'asc' | ||||
|   onSearched() | ||||
|   return true | ||||
| } | ||||
|  | ||||
| loadInvoices() | ||||
| loadInvoice() | ||||
| onSearched = debounce(onSearched, 500) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <SendInvoiceModal /> | ||||
|   <BasePage v-if="invoiceData" class="xl:pl-96 xl:ml-8"> | ||||
|     <BasePageHeader :title="pageTitle"> | ||||
|       <template #actions> | ||||
|         <div class="text-sm mr-3"> | ||||
|           <BaseButton | ||||
|             v-if=" | ||||
|               invoiceData.status === 'DRAFT' && | ||||
|               userStore.hasAbilities(abilities.EDIT_INVOICE) | ||||
|             " | ||||
|             :disabled="isMarkAsSent" | ||||
|             variant="primary-outline" | ||||
|             @click="onMarkAsSent" | ||||
|           > | ||||
|             {{ $t('invoices.mark_as_sent') }} | ||||
|           </BaseButton> | ||||
|         </div> | ||||
|  | ||||
|         <BaseButton | ||||
|           v-if=" | ||||
|             invoiceData.status === 'DRAFT' && | ||||
|             userStore.hasAbilities(abilities.SEND_INVOICE) | ||||
|           " | ||||
|           :disabled="isSendingEmail" | ||||
|           variant="primary" | ||||
|           class="text-sm" | ||||
|           @click="onSendInvoice" | ||||
|         > | ||||
|           {{ $t('invoices.send_invoice') }} | ||||
|         </BaseButton> | ||||
|  | ||||
|         <!-- Record Payment  --> | ||||
|         <router-link | ||||
|           v-if="userStore.hasAbilities(abilities.CREATE_PAYMENT)" | ||||
|           :to="`/admin/payments/${$route.params.id}/create`" | ||||
|         > | ||||
|           <BaseButton | ||||
|             v-if=" | ||||
|               invoiceData.status === 'SENT' || | ||||
|               invoiceData.status === 'OVERDUE' || | ||||
|               invoiceData.status === 'VIEWED' | ||||
|             " | ||||
|             variant="primary" | ||||
|           > | ||||
|             {{ $t('invoices.record_payment') }} | ||||
|           </BaseButton> | ||||
|         </router-link> | ||||
|  | ||||
|         <!-- Invoice Dropdown  --> | ||||
|         <InvoiceDropdown | ||||
|           class="ml-3" | ||||
|           :row="invoiceData" | ||||
|           :load-data="loadInvoices" | ||||
|         /> | ||||
|       </template> | ||||
|     </BasePageHeader> | ||||
|  | ||||
|     <!-- sidebar --> | ||||
|     <div | ||||
|       class=" | ||||
|         fixed | ||||
|         top-0 | ||||
|         left-0 | ||||
|         hidden | ||||
|         h-full | ||||
|         pt-16 | ||||
|         pb-4 | ||||
|         ml-56 | ||||
|         bg-white | ||||
|         xl:ml-64 | ||||
|         w-88 | ||||
|         xl:block | ||||
|       " | ||||
|     > | ||||
|       <div | ||||
|         class=" | ||||
|           flex | ||||
|           items-center | ||||
|           justify-between | ||||
|           px-4 | ||||
|           pt-8 | ||||
|           pb-2 | ||||
|           border border-gray-200 border-solid | ||||
|           height-full | ||||
|         " | ||||
|       > | ||||
|         <div class="mb-6"> | ||||
|           <BaseInput | ||||
|             v-model="searchData.searchText" | ||||
|             :placeholder="$t('general.search')" | ||||
|             type="text" | ||||
|             variant="gray" | ||||
|             @input="onSearched()" | ||||
|           > | ||||
|             <template #right> | ||||
|               <BaseIcon name="SearchIcon" class="h-5 text-gray-400" /> | ||||
|             </template> | ||||
|           </BaseInput> | ||||
|         </div> | ||||
|  | ||||
|         <div class="flex mb-6 ml-3" role="group" aria-label="First group"> | ||||
|           <BaseDropdown class="ml-3" position="bottom-start"> | ||||
|             <template #activator> | ||||
|               <BaseButton size="md" variant="gray"> | ||||
|                 <BaseIcon name="FilterIcon" /> | ||||
|               </BaseButton> | ||||
|             </template> | ||||
|             <div | ||||
|               class=" | ||||
|                 px-2 | ||||
|                 py-1 | ||||
|                 pb-2 | ||||
|                 mb-1 mb-2 | ||||
|                 text-sm | ||||
|                 border-b border-gray-200 border-solid | ||||
|               " | ||||
|             > | ||||
|               {{ $t('general.sort_by') }} | ||||
|             </div> | ||||
|  | ||||
|             <BaseDropdownItem class="flex px-1 py-2 cursor-pointer"> | ||||
|               <BaseInputGroup class="-mt-3 font-normal"> | ||||
|                 <BaseRadio | ||||
|                   id="filter_invoice_date" | ||||
|                   v-model="searchData.orderByField" | ||||
|                   :label="$t('reports.invoices.invoice_date')" | ||||
|                   size="sm" | ||||
|                   name="filter" | ||||
|                   value="invoice_date" | ||||
|                   @update:modelValue="onSearched" | ||||
|                 /> | ||||
|               </BaseInputGroup> | ||||
|             </BaseDropdownItem> | ||||
|  | ||||
|             <BaseDropdownItem class="flex px-1 py-2 cursor-pointer"> | ||||
|               <BaseInputGroup class="-mt-3 font-normal"> | ||||
|                 <BaseRadio | ||||
|                   id="filter_due_date" | ||||
|                   v-model="searchData.orderByField" | ||||
|                   :label="$t('invoices.due_date')" | ||||
|                   value="due_date" | ||||
|                   size="sm" | ||||
|                   name="filter" | ||||
|                   @update:modelValue="onSearched" | ||||
|                 /> | ||||
|               </BaseInputGroup> | ||||
|             </BaseDropdownItem> | ||||
|  | ||||
|             <BaseDropdownItem class="flex px-1 py-2 cursor-pointer"> | ||||
|               <BaseInputGroup class="-mt-3 font-normal"> | ||||
|                 <BaseRadio | ||||
|                   id="filter_invoice_number" | ||||
|                   v-model="searchData.orderByField" | ||||
|                   :label="$t('invoices.invoice_number')" | ||||
|                   value="invoice_number" | ||||
|                   size="sm" | ||||
|                   name="filter" | ||||
|                   @update:modelValue="onSearched" | ||||
|                 /> | ||||
|               </BaseInputGroup> | ||||
|             </BaseDropdownItem> | ||||
|           </BaseDropdown> | ||||
|  | ||||
|           <BaseButton class="ml-1" size="md" variant="gray" @click="sortData"> | ||||
|             <BaseIcon v-if="getOrderBy" name="SortAscendingIcon" /> | ||||
|             <BaseIcon v-else name="SortDescendingIcon" /> | ||||
|           </BaseButton> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div | ||||
|         v-if="invoiceStore && invoiceStore.invoices" | ||||
|         class=" | ||||
|           h-full | ||||
|           pb-32 | ||||
|           overflow-y-scroll | ||||
|           border-l border-gray-200 border-solid | ||||
|           base-scroll | ||||
|         " | ||||
|       > | ||||
|         <div v-for="(invoice, index) in invoiceStore.invoices" :key="index"> | ||||
|           <router-link | ||||
|             v-if="invoice && !isLoading" | ||||
|             :id="'invoice-' + invoice.id" | ||||
|             :to="`/admin/invoices/${invoice.id}/view`" | ||||
|             :class="[ | ||||
|               'flex justify-between side-invoice p-4 cursor-pointer hover:bg-gray-100 items-center border-l-4 border-transparent', | ||||
|               { | ||||
|                 'bg-gray-100 border-l-4 border-primary-500 border-solid': | ||||
|                   hasActiveUrl(invoice.id), | ||||
|               }, | ||||
|             ]" | ||||
|             style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)" | ||||
|           > | ||||
|             <div class="flex-2"> | ||||
|               <div | ||||
|                 class=" | ||||
|                   pr-2 | ||||
|                   mb-2 | ||||
|                   text-sm | ||||
|                   not-italic | ||||
|                   font-normal | ||||
|                   leading-5 | ||||
|                   text-black | ||||
|                   capitalize | ||||
|                   truncate | ||||
|                 " | ||||
|               > | ||||
|                 {{ invoice.customer.name }} | ||||
|               </div> | ||||
|  | ||||
|               <div | ||||
|                 class=" | ||||
|                   mt-1 | ||||
|                   mb-2 | ||||
|                   text-xs | ||||
|                   not-italic | ||||
|                   font-medium | ||||
|                   leading-5 | ||||
|                   text-gray-600 | ||||
|                 " | ||||
|               > | ||||
|                 {{ invoice.invoice_number }} | ||||
|               </div> | ||||
|               <BaseEstimateStatusBadge | ||||
|                 :status="invoice.status" | ||||
|                 class="px-1 text-xs" | ||||
|               > | ||||
|                 {{ invoice.status }} | ||||
|               </BaseEstimateStatusBadge> | ||||
|             </div> | ||||
|  | ||||
|             <div class="flex-1 whitespace-nowrap right"> | ||||
|               <BaseFormatMoney | ||||
|                 class=" | ||||
|                   mb-2 | ||||
|                   text-xl | ||||
|                   not-italic | ||||
|                   font-semibold | ||||
|                   leading-8 | ||||
|                   text-right text-gray-900 | ||||
|                   block | ||||
|                 " | ||||
|                 :amount="invoice.total" | ||||
|                 :currency="invoice.customer.currency" | ||||
|               /> | ||||
|               <div | ||||
|                 class=" | ||||
|                   text-sm | ||||
|                   not-italic | ||||
|                   font-normal | ||||
|                   leading-5 | ||||
|                   text-right text-gray-600 | ||||
|                   est-date | ||||
|                 " | ||||
|               > | ||||
|                 {{ invoice.formatted_invoice_date }} | ||||
|               </div> | ||||
|             </div> | ||||
|           </router-link> | ||||
|         </div> | ||||
|         <div class="flex justify-center p-4 items-center"> | ||||
|           <LoadingIcon | ||||
|             v-if="isLoading" | ||||
|             class="h-6 m-1 animate-spin text-primary-400" | ||||
|           /> | ||||
|         </div> | ||||
|         <p | ||||
|           v-if="!invoiceStore.invoices.length && !isLoading" | ||||
|           class="flex justify-center px-4 mt-5 text-sm text-gray-600" | ||||
|         > | ||||
|           {{ $t('invoices.no_matching_invoices') }} | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div | ||||
|       class="flex flex-col min-h-0 mt-8 overflow-hidden" | ||||
|       style="height: 75vh" | ||||
|     > | ||||
|       <iframe | ||||
|         :src="`${shareableLink}`" | ||||
|         class=" | ||||
|           flex-1 | ||||
|           border border-gray-400 border-solid | ||||
|           bg-white | ||||
|           rounded-md | ||||
|           frame-style | ||||
|         " | ||||
|       /> | ||||
|     </div> | ||||
|   </BasePage> | ||||
| </template> | ||||
							
								
								
									
										266
									
								
								resources/scripts/views/invoices/create/InvoiceCreate.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								resources/scripts/views/invoices/create/InvoiceCreate.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,266 @@ | ||||
| <template> | ||||
|   <SelectTemplateModal /> | ||||
|   <ItemModal /> | ||||
|   <TaxTypeModal /> | ||||
|  | ||||
|   <BasePage class="relative invoice-create-page"> | ||||
|     <form @submit.prevent="submitForm"> | ||||
|       <BasePageHeader :title="pageTitle"> | ||||
|         <BaseBreadcrumb> | ||||
|           <BaseBreadcrumbItem | ||||
|             :title="$t('general.home')" | ||||
|             to="/admin/dashboard" | ||||
|           /> | ||||
|           <BaseBreadcrumbItem | ||||
|             :title="$tc('invoices.invoice', 2)" | ||||
|             to="/admin/invoices" | ||||
|           /> | ||||
|           <BaseBreadcrumbItem | ||||
|             v-if="$route.name === 'invoices.edit'" | ||||
|             :title="$t('invoices.edit_invoice')" | ||||
|             to="#" | ||||
|             active | ||||
|           /> | ||||
|           <BaseBreadcrumbItem | ||||
|             v-else | ||||
|             :title="$t('invoices.new_invoice')" | ||||
|             to="#" | ||||
|             active | ||||
|           /> | ||||
|         </BaseBreadcrumb> | ||||
|  | ||||
|         <template #actions> | ||||
|           <router-link | ||||
|             v-if="$route.name === 'invoices.edit'" | ||||
|             :to="`/invoices/pdf/${invoiceStore.newInvoice.unique_hash}`" | ||||
|             target="_blank" | ||||
|           > | ||||
|             <BaseButton class="mr-3" variant="primary-outline" type="button"> | ||||
|               <span class="flex"> | ||||
|                 {{ $t('general.view_pdf') }} | ||||
|               </span> | ||||
|             </BaseButton> | ||||
|           </router-link> | ||||
|  | ||||
|           <BaseButton | ||||
|             :loading="isSaving" | ||||
|             :disabled="isSaving" | ||||
|             variant="primary" | ||||
|             type="submit" | ||||
|           > | ||||
|             <template #left="slotProps"> | ||||
|               <BaseIcon | ||||
|                 v-if="!isSaving" | ||||
|                 name="SaveIcon" | ||||
|                 :class="slotProps.class" | ||||
|               /> | ||||
|             </template> | ||||
|             {{ $t('invoices.save_invoice') }} | ||||
|           </BaseButton> | ||||
|         </template> | ||||
|       </BasePageHeader> | ||||
|  | ||||
|       <!-- Select Customer & Basic Fields  --> | ||||
|       <InvoiceBasicFields | ||||
|         :v="v$" | ||||
|         :is-loading="isLoadingContent" | ||||
|         :is-edit="isEdit" | ||||
|       /> | ||||
|  | ||||
|       <BaseScrollPane> | ||||
|         <!-- Invoice Items --> | ||||
|         <InvoiceItems | ||||
|           :currency="invoiceStore.newInvoice.selectedCurrency" | ||||
|           :is-loading="isLoadingContent" | ||||
|           :item-validation-scope="invoiceValidationScope" | ||||
|           :store="invoiceStore" | ||||
|           store-prop="newInvoice" | ||||
|         /> | ||||
|  | ||||
|         <!-- Invoice Footer Section --> | ||||
|         <div | ||||
|           class=" | ||||
|             block | ||||
|             mt-10 | ||||
|             invoice-foot | ||||
|             lg:flex lg:justify-between lg:items-start | ||||
|           " | ||||
|         > | ||||
|           <div class="relative w-full lg:w-1/2 lg:mr-4"> | ||||
|             <!-- Invoice Custom Notes --> | ||||
|             <NoteFields | ||||
|               :store="invoiceStore" | ||||
|               store-prop="newInvoice" | ||||
|               :fields="invoiceNoteFieldList" | ||||
|               type="Invoice" | ||||
|             /> | ||||
|  | ||||
|             <!-- Invoice Custom Fields --> | ||||
|             <InvoiceCustomFields | ||||
|               type="Invoice" | ||||
|               :is-edit="isEdit" | ||||
|               :is-loading="isLoadingContent" | ||||
|               :store="invoiceStore" | ||||
|               store-prop="newInvoice" | ||||
|               :custom-field-scope="invoiceValidationScope" | ||||
|               class="mb-6" | ||||
|             /> | ||||
|  | ||||
|             <!-- Invoice Template Button--> | ||||
|             <SelectTemplate | ||||
|               :store="invoiceStore" | ||||
|               store-prop="newInvoice" | ||||
|               component-name="InvoiceTemplate" | ||||
|             /> | ||||
|           </div> | ||||
|  | ||||
|           <InvoiceTotal | ||||
|             :currency="invoiceStore.newInvoice.selectedCurrency" | ||||
|             :is-loading="isLoadingContent" | ||||
|             :store="invoiceStore" | ||||
|             store-prop="newInvoice" | ||||
|             tax-popup-type="invoice" | ||||
|           /> | ||||
|         </div> | ||||
|       </BaseScrollPane> | ||||
|     </form> | ||||
|   </BasePage> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { computed, ref, watch } from 'vue' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { | ||||
|   required, | ||||
|   maxLength, | ||||
|   helpers, | ||||
|   requiredIf, | ||||
|   decimal, | ||||
| } from '@vuelidate/validators' | ||||
| import useVuelidate from '@vuelidate/core' | ||||
|  | ||||
| import { useInvoiceStore } from '@/scripts/stores/invoice' | ||||
| import { useCompanyStore } from '@/scripts/stores/company' | ||||
| import { useCustomFieldStore } from '@/scripts/stores/custom-field' | ||||
|  | ||||
| import InvoiceItems from '@/scripts/components/estimate-invoice-common/CreateItems.vue' | ||||
| import InvoiceTotal from '@/scripts/components/estimate-invoice-common/CreateTotal.vue' | ||||
| import SelectTemplate from '@/scripts/components/estimate-invoice-common/SelectTemplateButton.vue' | ||||
| import InvoiceBasicFields from './InvoiceCreateBasicFields.vue' | ||||
| import InvoiceCustomFields from '@/scripts/components/custom-fields/CreateCustomFields.vue' | ||||
| import NoteFields from '@/scripts/components/estimate-invoice-common/CreateNotesField.vue' | ||||
| import SelectTemplateModal from '@/scripts/components/modal-components/SelectTemplateModal.vue' | ||||
| import TaxTypeModal from '@/scripts/components/modal-components/TaxTypeModal.vue' | ||||
| import ItemModal from '@/scripts/components/modal-components/ItemModal.vue' | ||||
|  | ||||
| const invoiceStore = useInvoiceStore() | ||||
| const companyStore = useCompanyStore() | ||||
| const customFieldStore = useCustomFieldStore() | ||||
| const { t } = useI18n() | ||||
| let route = useRoute() | ||||
| let router = useRouter() | ||||
|  | ||||
| const invoiceValidationScope = 'newInvoice' | ||||
| let isSaving = ref(false) | ||||
|  | ||||
| const invoiceNoteFieldList = ref([ | ||||
|   'customer', | ||||
|   'company', | ||||
|   'customerCustom', | ||||
|   'invoice', | ||||
|   'invoiceCustom', | ||||
| ]) | ||||
|  | ||||
| let isLoadingContent = computed( | ||||
|   () => invoiceStore.isFetchingInvoice || invoiceStore.isFetchingInitialSettings | ||||
| ) | ||||
|  | ||||
| let pageTitle = computed(() => | ||||
|   isEdit.value ? t('invoices.edit_invoice') : t('invoices.new_invoice') | ||||
| ) | ||||
|  | ||||
| let isEdit = computed(() => route.name === 'invoices.edit') | ||||
|  | ||||
| const rules = { | ||||
|   invoice_date: { | ||||
|     required: helpers.withMessage(t('validation.required'), required), | ||||
|   }, | ||||
|   due_date: { | ||||
|     required: helpers.withMessage(t('validation.required'), required), | ||||
|   }, | ||||
|   reference_number: { | ||||
|     maxLength: helpers.withMessage( | ||||
|       t('validation.price_maxlength'), | ||||
|       maxLength(255) | ||||
|     ), | ||||
|   }, | ||||
|   customer_id: { | ||||
|     required: helpers.withMessage(t('validation.required'), required), | ||||
|   }, | ||||
|   invoice_number: { | ||||
|     required: helpers.withMessage(t('validation.required'), required), | ||||
|   }, | ||||
|   exchange_rate: { | ||||
|     required: requiredIf(function () { | ||||
|       helpers.withMessage(t('validation.required'), required) | ||||
|       return invoiceStore.showExchangeRate | ||||
|     }), | ||||
|     decimal: helpers.withMessage(t('validation.valid_exchange_rate'), decimal), | ||||
|   }, | ||||
| } | ||||
|  | ||||
| const v$ = useVuelidate( | ||||
|   rules, | ||||
|   computed(() => invoiceStore.newInvoice), | ||||
|   { $scope: invoiceValidationScope } | ||||
| ) | ||||
|  | ||||
| customFieldStore.resetCustomFields() | ||||
| v$.value.$reset | ||||
| invoiceStore.resetCurrentInvoice() | ||||
| invoiceStore.fetchInvoiceInitialSettings(isEdit.value) | ||||
|  | ||||
| watch( | ||||
|   () => invoiceStore.newInvoice.customer, | ||||
|   (newVal) => { | ||||
|     if (newVal && newVal.currency) { | ||||
|       invoiceStore.newInvoice.selectedCurrency = newVal.currency | ||||
|     } else { | ||||
|       invoiceStore.newInvoice.selectedCurrency = | ||||
|         companyStore.selectedCompanyCurrency | ||||
|     } | ||||
|   } | ||||
| ) | ||||
|  | ||||
| async function submitForm() { | ||||
|   v$.value.$touch() | ||||
|  | ||||
|   if (v$.value.$invalid) { | ||||
|     return false | ||||
|   } | ||||
|  | ||||
|   isSaving.value = true | ||||
|  | ||||
|   let data = { | ||||
|     ...invoiceStore.newInvoice, | ||||
|     sub_total: invoiceStore.getSubTotal, | ||||
|     total: invoiceStore.getTotal, | ||||
|     tax: invoiceStore.getTotalTax, | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     const action = isEdit.value | ||||
|       ? invoiceStore.updateInvoice | ||||
|       : invoiceStore.addInvoice | ||||
|  | ||||
|     const response = await action(data) | ||||
|  | ||||
|     router.push(`/admin/invoices/${response.data.data.id}/view`) | ||||
|   } catch (err) { | ||||
|     console.error(err) | ||||
|   } | ||||
|  | ||||
|   isSaving.value = false | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,85 @@ | ||||
| <template> | ||||
|   <div class="grid grid-cols-12 gap-8 mt-6 mb-8"> | ||||
|     <BaseCustomerSelectPopup | ||||
|       v-model="invoiceStore.newInvoice.customer" | ||||
|       :valid="v.customer_id" | ||||
|       :content-loading="isLoading" | ||||
|       type="invoice" | ||||
|       class="col-span-12 lg:col-span-5 pr-0" | ||||
|     /> | ||||
|  | ||||
|     <BaseInputGrid class="col-span-12 lg:col-span-7"> | ||||
|       <BaseInputGroup | ||||
|         :label="$t('invoices.invoice_date')" | ||||
|         :content-loading="isLoading" | ||||
|         required | ||||
|         :error="v.invoice_date.$error && v.invoice_date.$errors[0].$message" | ||||
|       > | ||||
|         <BaseDatePicker | ||||
|           v-model="invoiceStore.newInvoice.invoice_date" | ||||
|           :content-loading="isLoading" | ||||
|           :calendar-button="true" | ||||
|           calendar-button-icon="calendar" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup | ||||
|         :label="$t('invoices.due_date')" | ||||
|         :content-loading="isLoading" | ||||
|         required | ||||
|         :error="v.due_date.$error && v.due_date.$errors[0].$message" | ||||
|       > | ||||
|         <BaseDatePicker | ||||
|           v-model="invoiceStore.newInvoice.due_date" | ||||
|           :content-loading="isLoading" | ||||
|           :calendar-button="true" | ||||
|           calendar-button-icon="calendar" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup | ||||
|         :label="$t('invoices.invoice_number')" | ||||
|         :content-loading="isLoading" | ||||
|         :error="v.invoice_number.$error && v.invoice_number.$errors[0].$message" | ||||
|         required | ||||
|       > | ||||
|         <BaseInput | ||||
|           v-model="invoiceStore.newInvoice.invoice_number" | ||||
|           :content-loading="isLoading" | ||||
|           @input="v.invoice_number.$touch()" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <ExchangeRateConverter | ||||
|         :store="invoiceStore" | ||||
|         store-prop="newInvoice" | ||||
|         :v="v" | ||||
|         :is-loading="isLoading" | ||||
|         :is-edit="isEdit" | ||||
|         :customer-currency="invoiceStore.newInvoice.currency_id" | ||||
|       /> | ||||
|     </BaseInputGrid> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import ExchangeRateConverter from '@/scripts/components/estimate-invoice-common/ExchangeRateConverter.vue' | ||||
| import { useInvoiceStore } from '@/scripts/stores/invoice' | ||||
|  | ||||
| const props = defineProps({ | ||||
|   v: { | ||||
|     type: Object, | ||||
|     default: null, | ||||
|   }, | ||||
|   isLoading: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
|   isEdit: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| const invoiceStore = useInvoiceStore() | ||||
| </script> | ||||
		Reference in New Issue
	
	Block a user