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:
		
							
								
								
									
										450
									
								
								resources/scripts/views/estimates/Index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										450
									
								
								resources/scripts/views/estimates/Index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,450 @@ | ||||
| <template> | ||||
|   <BasePage> | ||||
|     <SendEstimateModal /> | ||||
|  | ||||
|     <BasePageHeader :title="$t('estimates.title')"> | ||||
|       <BaseBreadcrumb> | ||||
|         <BaseBreadcrumbItem :title="$t('general.home')" to="dashboard" /> | ||||
|  | ||||
|         <BaseBreadcrumbItem | ||||
|           :title="$tc('estimates.estimate', 2)" | ||||
|           to="#" | ||||
|           active | ||||
|         /> | ||||
|       </BaseBreadcrumb> | ||||
|  | ||||
|       <template #actions> | ||||
|         <BaseButton | ||||
|           v-show="estimateStore.totalEstimateCount" | ||||
|           variant="primary-outline" | ||||
|           @click="toggleFilter" | ||||
|         > | ||||
|           {{ $t('general.filter') }} | ||||
|           <template #right="slotProps"> | ||||
|             <BaseIcon | ||||
|               v-if="!showFilters" | ||||
|               :class="slotProps.class" | ||||
|               name="FilterIcon" | ||||
|             /> | ||||
|             <BaseIcon v-else name="XIcon" :class="slotProps.class" /> | ||||
|           </template> | ||||
|         </BaseButton> | ||||
|  | ||||
|         <router-link | ||||
|           v-if="userStore.hasAbilities(abilities.CREATE_ESTIMATE)" | ||||
|           to="estimates/create" | ||||
|         > | ||||
|           <BaseButton variant="primary" class="ml-4"> | ||||
|             <template #left="slotProps"> | ||||
|               <BaseIcon name="PlusIcon" :class="slotProps.class" /> | ||||
|             </template> | ||||
|             {{ $t('estimates.new_estimate') }} | ||||
|           </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')"> | ||||
|         <BaseDatePicker | ||||
|           v-model="filters.to_date" | ||||
|           :calendar-button="true" | ||||
|           calendar-button-icon="calendar" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup :label="$t('estimates.estimate_number')"> | ||||
|         <BaseInput v-model="filters.estimate_number"> | ||||
|           <template #left="slotProps"> | ||||
|             <BaseIcon name="HashtagIcon" :class="slotProps.class" /> | ||||
|           </template> | ||||
|         </BaseInput> | ||||
|       </BaseInputGroup> | ||||
|     </BaseFilterWrapper> | ||||
|  | ||||
|     <BaseEmptyPlaceholder | ||||
|       v-show="showEmptyScreen" | ||||
|       :title="$t('estimates.no_estimates')" | ||||
|       :description="$t('estimates.list_of_estimates')" | ||||
|     > | ||||
|       <ObservatoryIcon class="mt-5 mb-4" /> | ||||
|  | ||||
|       <template #actions> | ||||
|         <BaseButton | ||||
|           v-if="userStore.hasAbilities(abilities.CREATE_ESTIMATE)" | ||||
|           variant="primary-outline" | ||||
|           @click="$router.push('/admin/estimates/create')" | ||||
|         > | ||||
|           <template #left="slotProps"> | ||||
|             <BaseIcon name="PlusIcon" :class="slotProps.class" /> | ||||
|           </template> | ||||
|           {{ $t('estimates.add_new_estimate') }} | ||||
|         </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=" | ||||
|             estimateStore.selectedEstimates.length && | ||||
|             userStore.hasAbilities(abilities.DELETE_ESTIMATE) | ||||
|           " | ||||
|           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="removeMultipleEstimates"> | ||||
|             <BaseIcon name="TrashIcon" class="mr-3 text-gray-600" /> | ||||
|             {{ $t('general.delete') }} | ||||
|           </BaseDropdownItem> | ||||
|         </BaseDropdown> | ||||
|       </div> | ||||
|  | ||||
|       <BaseTable | ||||
|         ref="tableComponent" | ||||
|         :data="fetchData" | ||||
|         :columns="estimateColumns" | ||||
|         :placeholder-count="estimateStore.totalEstimateCount >= 20 ? 10 : 5" | ||||
|         class="mt-10" | ||||
|       > | ||||
|         <template #header> | ||||
|           <div class="absolute items-center left-6 top-2.5 select-none"> | ||||
|             <BaseCheckbox | ||||
|               v-model="estimateStore.selectAllField" | ||||
|               variant="primary" | ||||
|               @change="estimateStore.selectAllEstimates" | ||||
|             /> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-checkbox="{ row }"> | ||||
|           <div class="relative block"> | ||||
|             <BaseCheckbox | ||||
|               :id="row.id" | ||||
|               v-model="selectField" | ||||
|               :value="row.data.id" | ||||
|             /> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|         <!-- Estimate date  --> | ||||
|         <template #cell-estimate_date="{ row }"> | ||||
|             {{ row.data.formatted_estimate_date }} | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-estimate_number="{ row }"> | ||||
|           <router-link | ||||
|             :to="{ path: `estimates/${row.data.id}/view` }" | ||||
|             class="font-medium text-primary-500" | ||||
|           > | ||||
|             {{ row.data.estimate_number }} | ||||
|           </router-link> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-name="{ row }"> | ||||
|           <div> | ||||
|             {{ row.data.customer.name }} | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-status="{ row }"> | ||||
|           <BaseEstimateStatusBadge :status="row.data.status" class="px-3 py-1"> | ||||
|             {{ row.data.status }} | ||||
|           </BaseEstimateStatusBadge> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-total="{ row }"> | ||||
|           <BaseFormatMoney :amount="row.data.total" /> | ||||
|         </template> | ||||
|  | ||||
|         <!-- Actions --> | ||||
|         <template v-if="hasAtleastOneAbility()" #cell-actions="{ row }"> | ||||
|           <EstimateDropDown :row="row.data" :table="tableComponent" /> | ||||
|         </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 { useEstimateStore } from '@/scripts/stores/estimate' | ||||
| import { useDialogStore } from '@/scripts/stores/dialog' | ||||
| import { useUserStore } from '@/scripts/stores/user' | ||||
| import { debouncedWatch } from '@vueuse/core' | ||||
| import abilities from '@/scripts/stub/abilities' | ||||
|  | ||||
| import ObservatoryIcon from '@/scripts/components/icons/empty/ObservatoryIcon.vue' | ||||
| import EstimateDropDown from '@/scripts/components/dropdowns/EstimateIndexDropdown.vue' | ||||
| import SendEstimateModal from '@/scripts/components/modal-components/SendEstimateModal.vue' | ||||
|  | ||||
| const estimateStore = useEstimateStore() | ||||
| const dialogStore = useDialogStore() | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const tableComponent = ref(null) | ||||
| const { t } = useI18n() | ||||
| 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() | ||||
|  | ||||
| let filters = reactive({ | ||||
|   customer_id: '', | ||||
|   status: 'DRAFT', | ||||
|   from_date: '', | ||||
|   to_date: '', | ||||
|   estimate_number: '', | ||||
| }) | ||||
|  | ||||
| const showEmptyScreen = computed( | ||||
|   () => !estimateStore.totalEstimateCount && !isRequestOngoing.value | ||||
| ) | ||||
|  | ||||
| const selectField = computed({ | ||||
|   get: () => estimateStore.selectedEstimates, | ||||
|   set: (val) => { | ||||
|     estimateStore.selectEstimate(val) | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| const estimateColumns = computed(() => { | ||||
|   return [ | ||||
|     { | ||||
|       key: 'checkbox', | ||||
|       thClass: 'extra w-10 pr-0', | ||||
|       sortable: false, | ||||
|       tdClass: 'font-medium text-gray-900 pr-0', | ||||
|     }, | ||||
|     { | ||||
|       key: 'estimate_date', | ||||
|       label: t('estimates.date'), | ||||
|       thClass: 'extra', | ||||
|       tdClass: 'font-medium text-gray-500', | ||||
|     }, | ||||
|     { key: 'estimate_number', label: t('estimates.number', 2) }, | ||||
|     { key: 'name', label: t('estimates.customer') }, | ||||
|     { key: 'status', label: t('estimates.status') }, | ||||
|     { | ||||
|       key: 'total', | ||||
|       label: t('estimates.total'), | ||||
|       tdClass: 'font-medium text-gray-900', | ||||
|     }, | ||||
|     { | ||||
|       key: 'actions', | ||||
|       tdClass: 'text-right text-sm font-medium pl-0', | ||||
|       thClass: 'text-right pl-0', | ||||
|       sortable: false, | ||||
|     }, | ||||
|   ] | ||||
| }) | ||||
|  | ||||
| debouncedWatch( | ||||
|   filters, | ||||
|   () => { | ||||
|     setFilters() | ||||
|   }, | ||||
|   { debounce: 500 } | ||||
| ) | ||||
|  | ||||
| onUnmounted(() => { | ||||
|   if (estimateStore.selectAllField) { | ||||
|     estimateStore.selectAllEstimates() | ||||
|   } | ||||
| }) | ||||
|  | ||||
| function hasAtleastOneAbility() { | ||||
|   return userStore.hasAbilities([ | ||||
|     abilities.CREATE_ESTIMATE, | ||||
|     abilities.EDIT_ESTIMATE, | ||||
|     abilities.VIEW_ESTIMATE, | ||||
|     abilities.SEND_ESTIMATE, | ||||
|   ]) | ||||
| } | ||||
|  | ||||
| function refreshTable() { | ||||
|   tableComponent.value && tableComponent.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, | ||||
|     estimate_number: filters.estimate_number, | ||||
|     orderByField: sort.fieldName || 'created_at', | ||||
|     orderBy: sort.order || 'desc', | ||||
|     page, | ||||
|   } | ||||
|  | ||||
|   isRequestOngoing.value = true | ||||
|  | ||||
|   let response = await estimateStore.fetchEstimates(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() { | ||||
|   estimateStore.$patch((state) => { | ||||
|     state.selectedEstimates = [] | ||||
|     state.selectAllField = false | ||||
|   }) | ||||
|  | ||||
|   refreshTable() | ||||
| } | ||||
|  | ||||
| function clearFilter() { | ||||
|   filters.customer_id = '' | ||||
|   filters.status = '' | ||||
|   filters.from_date = '' | ||||
|   filters.to_date = '' | ||||
|   filters.estimate_number = '' | ||||
|  | ||||
|   activeTab.value = t('general.all') | ||||
| } | ||||
|  | ||||
| function toggleFilter() { | ||||
|   if (showFilters.value) { | ||||
|     clearFilter() | ||||
|   } | ||||
|  | ||||
|   showFilters.value = !showFilters.value | ||||
| } | ||||
|  | ||||
| async function removeMultipleEstimates() { | ||||
|   dialogStore | ||||
|     .openDialog({ | ||||
|       title: t('general.are_you_sure'), | ||||
|       message: t('estimates.confirm_delete'), | ||||
|       yesLabel: t('general.ok'), | ||||
|       noLabel: t('general.cancel'), | ||||
|       variant: 'danger', | ||||
|       hideNoButton: false, | ||||
|       size: 'lg', | ||||
|     }) | ||||
|     .then((res) => { | ||||
|       if (res) { | ||||
|         estimateStore.deleteMultipleEstimates().then((res) => { | ||||
|           refreshTable() | ||||
|           if (res.data) { | ||||
|             estimateStore.$patch((state) => { | ||||
|               state.selectedEstimates = [] | ||||
|               state.selectAllField = false | ||||
|             }) | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| 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> | ||||
							
								
								
									
										500
									
								
								resources/scripts/views/estimates/View.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										500
									
								
								resources/scripts/views/estimates/View.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,500 @@ | ||||
| <template> | ||||
|   <SendEstimateModal /> | ||||
|   <BasePage v-if="estimateData" class="xl:pl-96 xl:ml-8"> | ||||
|     <BasePageHeader :title="pageTitle"> | ||||
|       <template #actions> | ||||
|         <div class="mr-3 text-sm"> | ||||
|           <BaseButton | ||||
|             v-if=" | ||||
|               estimateData.status === 'DRAFT' && | ||||
|               userStore.hasAbilities(abilities.EDIT_ESTIMATE) | ||||
|             " | ||||
|             :disabled="isMarkAsSent" | ||||
|             :content-loading="isLoadingEstimate" | ||||
|             variant="primary-outline" | ||||
|             @click="onMarkAsSent" | ||||
|           > | ||||
|             {{ $t('estimates.mark_as_sent') }} | ||||
|           </BaseButton> | ||||
|         </div> | ||||
|  | ||||
|         <BaseButton | ||||
|           v-if=" | ||||
|             estimateData.status === 'DRAFT' && | ||||
|             userStore.hasAbilities(abilities.SEND_ESTIMATE) | ||||
|           " | ||||
|           :disabled="isSendingEmail" | ||||
|           :content-loading="isLoadingEstimate" | ||||
|           variant="primary" | ||||
|           class="text-sm" | ||||
|           @click="onSendEstimate" | ||||
|         > | ||||
|           {{ $t('estimates.send_estimate') }} | ||||
|         </BaseButton> | ||||
|  | ||||
|         <EstimateDropDown class="ml-3" :row="estimateData" /> | ||||
|       </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="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" | ||||
|             width-class="w-45" | ||||
|             position-class="left-0" | ||||
|           > | ||||
|             <template #activator> | ||||
|               <BaseButton size="md" variant="gray"> | ||||
|                 <BaseIcon name="FilterIcon" /> | ||||
|               </BaseButton> | ||||
|             </template> | ||||
|  | ||||
|             <div | ||||
|               class=" | ||||
|                 px-4 | ||||
|                 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-4 py-2 cursor-pointer"> | ||||
|               <BaseInputGroup class="-mt-3 font-normal"> | ||||
|                 <BaseRadio | ||||
|                   id="filter_estimate_date" | ||||
|                   v-model="searchData.orderByField" | ||||
|                   :label="$t('reports.estimates.estimate_date')" | ||||
|                   size="sm" | ||||
|                   name="filter" | ||||
|                   value="estimate_date" | ||||
|                   @update:modelValue="onSearched" | ||||
|                 /> | ||||
|               </BaseInputGroup> | ||||
|             </BaseDropdownItem> | ||||
|  | ||||
|             <BaseDropdownItem class="flex px-4 py-2 cursor-pointer"> | ||||
|               <BaseInputGroup class="-mt-3 font-normal"> | ||||
|                 <BaseRadio | ||||
|                   id="filter_due_date" | ||||
|                   v-model="searchData.orderByField" | ||||
|                   :label="$t('estimates.due_date')" | ||||
|                   value="expiry_date" | ||||
|                   size="sm" | ||||
|                   name="filter" | ||||
|                   @update:modelValue="onSearched" | ||||
|                 /> | ||||
|               </BaseInputGroup> | ||||
|             </BaseDropdownItem> | ||||
|  | ||||
|             <BaseDropdownItem class="flex px-4 py-2 cursor-pointer"> | ||||
|               <BaseInputGroup class="-mt-3 font-normal"> | ||||
|                 <BaseRadio | ||||
|                   id="filter_estimate_number" | ||||
|                   v-model="searchData.orderByField" | ||||
|                   :label="$t('estimates.estimate_number')" | ||||
|                   value="estimate_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="estimateStore && estimateStore.estimates" | ||||
|         class=" | ||||
|           h-full | ||||
|           pb-32 | ||||
|           overflow-y-scroll | ||||
|           border-l border-gray-200 border-solid | ||||
|           base-scroll | ||||
|         " | ||||
|       > | ||||
|         <div v-for="(estimate, index) in estimateStore.estimates" :key="index"> | ||||
|           <router-link | ||||
|             v-if="estimate && !isLoading" | ||||
|             :id="'estimate-' + estimate.id" | ||||
|             :to="`/admin/estimates/${estimate.id}/view`" | ||||
|             :class="[ | ||||
|               'flex justify-between side-estimate 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(estimate.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 | ||||
|                 " | ||||
|               > | ||||
|                 {{ estimate.customer.name }} | ||||
|               </div> | ||||
|  | ||||
|               <div | ||||
|                 class=" | ||||
|                   mt-1 | ||||
|                   mb-2 | ||||
|                   text-xs | ||||
|                   not-italic | ||||
|                   font-medium | ||||
|                   leading-5 | ||||
|                   text-gray-600 | ||||
|                 " | ||||
|               > | ||||
|                 {{ estimate.estimate_number }} | ||||
|               </div> | ||||
|  | ||||
|               <BaseEstimateStatusBadge | ||||
|                 :status="estimate.status" | ||||
|                 class="px-1 text-xs" | ||||
|               > | ||||
|                 {{ estimate.status }} | ||||
|               </BaseEstimateStatusBadge> | ||||
|             </div> | ||||
|  | ||||
|             <div class="flex-1 whitespace-nowrap right"> | ||||
|               <BaseFormatMoney | ||||
|                 :amount="estimate.total" | ||||
|                 :currency="estimate.customer.currency" | ||||
|                 class=" | ||||
|                   block | ||||
|                   mb-2 | ||||
|                   text-xl | ||||
|                   not-italic | ||||
|                   font-semibold | ||||
|                   leading-8 | ||||
|                   text-right text-gray-900 | ||||
|                 " | ||||
|               /> | ||||
|  | ||||
|               <div | ||||
|                 class=" | ||||
|                   text-sm | ||||
|                   not-italic | ||||
|                   font-normal | ||||
|                   leading-5 | ||||
|                   text-right text-gray-600 | ||||
|                   est-date | ||||
|                 " | ||||
|               > | ||||
|                 {{ estimate.formatted_estimate_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="!estimateStore.estimates.length && !isLoading" | ||||
|           class="flex justify-center px-4 mt-5 text-sm text-gray-600" | ||||
|         > | ||||
|           {{ $t('estimates.no_matching_estimates') }} | ||||
|         </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 | ||||
|           rounded-md | ||||
|           bg-white | ||||
|           frame-style | ||||
|         " | ||||
|       /> | ||||
|     </div> | ||||
|   </BasePage> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { computed, reactive, ref, watch, inject } from 'vue' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import EstimateDropDown from '@/scripts/components/dropdowns/EstimateIndexDropdown.vue' | ||||
| import { debounce } from 'lodash' | ||||
| import { useEstimateStore } from '@/scripts/stores/estimate' | ||||
| import { useModalStore } from '@/scripts/stores/modal' | ||||
| import { useNotificationStore } from '@/scripts/stores/notification' | ||||
| import { useDialogStore } from '@/scripts/stores/dialog' | ||||
| import { useUserStore } from '@/scripts/stores/user' | ||||
| import SendEstimateModal from '@/scripts/components/modal-components/SendEstimateModal.vue' | ||||
| import LoadingIcon from '@/scripts/components/icons/LoadingIcon.vue' | ||||
| import abilities from '@/scripts/stub/abilities' | ||||
|  | ||||
| const modalStore = useModalStore() | ||||
| const estimateStore = useEstimateStore() | ||||
| const notificationStore = useNotificationStore() | ||||
| const dialogStore = useDialogStore() | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const { t } = useI18n() | ||||
| const utils = inject('$utils') | ||||
| const id = ref(null) | ||||
| const count = ref(null) | ||||
| const estimateData = 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 isLoadingEstimate = ref(false) | ||||
|  | ||||
| const searchData = reactive({ | ||||
|   orderBy: null, | ||||
|   orderByField: null, | ||||
|   searchText: null, | ||||
| }) | ||||
|  | ||||
| const pageTitle = computed(() => estimateData.value.estimate_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 `/estimates/pdf/${estimateData.value.unique_hash}` | ||||
| }) | ||||
|  | ||||
| const getCurrentEstimateId = computed(() => { | ||||
|   if (estimateData.value && estimateData.value.id) { | ||||
|     return estimate.value.id | ||||
|   } | ||||
|   return null | ||||
| }) | ||||
|  | ||||
| watch(route, (to, from) => { | ||||
|   if (to.name === 'estimates.view') { | ||||
|     loadEstimate() | ||||
|   } | ||||
| }) | ||||
|  | ||||
| loadEstimates() | ||||
| loadEstimate() | ||||
|  | ||||
| onSearched = debounce(onSearched, 500) | ||||
|  | ||||
| function hasActiveUrl(id) { | ||||
|   return route.params.id == id | ||||
| } | ||||
|  | ||||
| async function loadEstimates() { | ||||
|   isLoading.value = true | ||||
|   await estimateStore.fetchEstimates(route.params.id) | ||||
|   isLoading.value = false | ||||
|  | ||||
|   setTimeout(() => { | ||||
|     scrollToEstimate() | ||||
|   }, 500) | ||||
| } | ||||
|  | ||||
| function scrollToEstimate() { | ||||
|   const el = document.getElementById(`estimate-${route.params.id}`) | ||||
|   if (el) { | ||||
|     el.scrollIntoView({ behavior: 'smooth' }) | ||||
|     el.classList.add('shake') | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function loadEstimate() { | ||||
|   isLoadingEstimate.value = true | ||||
|   let response = await estimateStore.fetchEstimate(route.params.id) | ||||
|  | ||||
|   if (response.data) { | ||||
|     isLoadingEstimate.value = false | ||||
|     estimateData.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 estimateStore.searchEstimate(data) | ||||
|   isSearching.value = false | ||||
|   if (response.data) { | ||||
|     estimateStore.estimates = response.data.data | ||||
|   } | ||||
| } | ||||
|  | ||||
| function sortData() { | ||||
|   if (searchData.orderBy === 'asc') { | ||||
|     searchData.orderBy = 'desc' | ||||
|     onSearched() | ||||
|     return true | ||||
|   } | ||||
|   searchData.orderBy = 'asc' | ||||
|   onSearched() | ||||
|   return true | ||||
| } | ||||
|  | ||||
| async function onMarkAsSent() { | ||||
|   dialogStore | ||||
|     .openDialog({ | ||||
|       title: t('general.are_you_sure'), | ||||
|       message: t('estimates.confirm_mark_as_sent'), | ||||
|       yesLabel: t('general.ok'), | ||||
|       noLabel: t('general.cancel'), | ||||
|       variant: 'primary', | ||||
|       hideNoButton: false, | ||||
|       size: 'lg', | ||||
|     }) | ||||
|     .then((response) => { | ||||
|       isMarkAsSent.value = false | ||||
|       if (response) { | ||||
|         estimateStore.markAsSent({ | ||||
|           id: estimateData.value.id, | ||||
|           status: 'SENT', | ||||
|         }) | ||||
|         estimateData.value.status = 'SENT' | ||||
|         isMarkAsSent.value = true | ||||
|       } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| async function onSendEstimate(id) { | ||||
|   modalStore.openModal({ | ||||
|     title: t('estimates.send_estimate'), | ||||
|     componentName: 'SendEstimateModal', | ||||
|     id: estimateData.value.id, | ||||
|     data: estimateData.value, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| async function removeEstimate(id) { | ||||
|   dialogStore | ||||
|     .openDialog({ | ||||
|       title: t('general.are_you_sure'), | ||||
|       message: t('estimates.confirm_delete'), | ||||
|       yesLabel: t('general.ok'), | ||||
|       noLabel: t('general.cancel'), | ||||
|       variant: 'danger', | ||||
|       hideNoButton: false, | ||||
|       size: 'lg', | ||||
|     }) | ||||
|     .then((res) => { | ||||
|       if (res) { | ||||
|         estimateStore | ||||
|           .deleteEstimate({ ids: [id] }) | ||||
|           .then(() => { | ||||
|             router.push('/admin/estimates') | ||||
|           }) | ||||
|           .catch((err) => { | ||||
|             console.error(err) | ||||
|           }) | ||||
|       } | ||||
|     }) | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										267
									
								
								resources/scripts/views/estimates/create/EstimateCreate.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										267
									
								
								resources/scripts/views/estimates/create/EstimateCreate.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,267 @@ | ||||
| <template> | ||||
|   <SelectTemplateModal /> | ||||
|   <ItemModal /> | ||||
|   <TaxTypeModal /> | ||||
|  | ||||
|   <BasePage class="relative estimate-create-page"> | ||||
|     <form @submit.prevent="submitForm"> | ||||
|       <BasePageHeader :title="pageTitle"> | ||||
|         <BaseBreadcrumb> | ||||
|           <BaseBreadcrumbItem | ||||
|             :title="$t('general.home')" | ||||
|             to="/admin/dashboard" | ||||
|           /> | ||||
|           <BaseBreadcrumbItem | ||||
|             :title="$tc('estimates.estimate', 2)" | ||||
|             to="/admin/estimates" | ||||
|           /> | ||||
|           <BaseBreadcrumbItem | ||||
|             v-if="$route.name === 'estimates.edit'" | ||||
|             :title="$t('estimates.edit_estimate')" | ||||
|             to="#" | ||||
|             active | ||||
|           /> | ||||
|           <BaseBreadcrumbItem | ||||
|             v-else | ||||
|             :title="$t('estimates.new_estimate')" | ||||
|             to="#" | ||||
|             active | ||||
|           /> | ||||
|         </BaseBreadcrumb> | ||||
|  | ||||
|         <template #actions> | ||||
|           <router-link | ||||
|             v-if="$route.name === 'estimates.edit'" | ||||
|             :to="`/estimates/pdf/${estimateStore.newEstimate.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" | ||||
|             :content-loading="isLoadingContent" | ||||
|             variant="primary" | ||||
|             type="submit" | ||||
|           > | ||||
|             <template #left="slotProps"> | ||||
|               <BaseIcon | ||||
|                 v-if="!isSaving" | ||||
|                 :class="slotProps.class" | ||||
|                 name="SaveIcon" | ||||
|               /> | ||||
|             </template> | ||||
|             {{ $t('estimates.save_estimate') }} | ||||
|           </BaseButton> | ||||
|         </template> | ||||
|       </BasePageHeader> | ||||
|  | ||||
|       <!-- Select Customer & Basic Fields  --> | ||||
|       <EstimateBasicFields | ||||
|         :v="v$" | ||||
|         :is-loading="isLoadingContent" | ||||
|         :is-edit="isEdit" | ||||
|       /> | ||||
|  | ||||
|       <BaseScrollPane> | ||||
|         <!-- Estimate Items --> | ||||
|         <Items | ||||
|           :currency="estimateStore.newEstimate.selectedCurrency" | ||||
|           :is-loading="isLoadingContent" | ||||
|           :item-validation-scope="estimateValidationScope" | ||||
|           :store="estimateStore" | ||||
|           store-prop="newEstimate" | ||||
|         /> | ||||
|  | ||||
|         <!-- Estimate Footer Section --> | ||||
|         <div | ||||
|           class=" | ||||
|             block | ||||
|             mt-10 | ||||
|             estimate-foot | ||||
|             lg:flex lg:justify-between lg:items-start | ||||
|           " | ||||
|         > | ||||
|           <div class="relative w-full lg:w-1/2"> | ||||
|             <!-- Estimate Custom Notes --> | ||||
|             <NoteFields | ||||
|               :store="estimateStore" | ||||
|               store-prop="newEstimate" | ||||
|               :fields="estimateNoteFieldList" | ||||
|               type="Estimate" | ||||
|             /> | ||||
|  | ||||
|             <!-- Estimate Custom Fields --> | ||||
|             <EstimateCustomFields | ||||
|               type="Estimate" | ||||
|               :is-edit="isEdit" | ||||
|               :is-loading="isLoadingContent" | ||||
|               :store="estimateStore" | ||||
|               store-prop="newEstimate" | ||||
|               :custom-field-scope="estimateValidationScope" | ||||
|               class="mb-6" | ||||
|             /> | ||||
|  | ||||
|             <!-- Estimate Template Button--> | ||||
|             <SelectTemplate | ||||
|               :store="estimateStore" | ||||
|               component-name="EstimateTemplate" | ||||
|               store-prop="newEstimate" | ||||
|             /> | ||||
|           </div> | ||||
|  | ||||
|           <Total | ||||
|             :currency="estimateStore.newEstimate.selectedCurrency" | ||||
|             :is-loading="isLoadingContent" | ||||
|             :store="estimateStore" | ||||
|             store-prop="newEstimate" | ||||
|             tax-popup-type="estimate" | ||||
|           /> | ||||
|         </div> | ||||
|       </BaseScrollPane> | ||||
|     </form> | ||||
|   </BasePage> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { computed, ref, watch, onMounted } 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 { useEstimateStore } from '@/scripts/stores/estimate' | ||||
| import { useCompanyStore } from '@/scripts/stores/company' | ||||
| import { useCustomFieldStore } from '@/scripts/stores/custom-field' | ||||
|  | ||||
| import Items from '@/scripts/components/estimate-invoice-common/CreateItems.vue' | ||||
| import Total from '@/scripts/components/estimate-invoice-common/CreateTotal.vue' | ||||
| import SelectTemplate from '@/scripts/components/estimate-invoice-common/SelectTemplateButton.vue' | ||||
| import EstimateCustomFields from '@/scripts/components/custom-fields/CreateCustomFields.vue' | ||||
| import NoteFields from '@/scripts/components/estimate-invoice-common/CreateNotesField.vue' | ||||
| import EstimateBasicFields from './EstimateCreateBasicFields.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 estimateStore = useEstimateStore() | ||||
| const companyStore = useCompanyStore() | ||||
| const customFieldStore = useCustomFieldStore() | ||||
| const { t } = useI18n() | ||||
|  | ||||
| const estimateValidationScope = 'newEstimate' | ||||
| let isSaving = ref(false) | ||||
|  | ||||
| const estimateNoteFieldList = ref([ | ||||
|   'customer', | ||||
|   'company', | ||||
|   'customerCustom', | ||||
|   'estimate', | ||||
|   'estimateCustom', | ||||
| ]) | ||||
|  | ||||
| let route = useRoute() | ||||
| let router = useRouter() | ||||
|  | ||||
| let isLoadingContent = computed(() => estimateStore.isFetchingInitialSettings) | ||||
|  | ||||
| let pageTitle = computed(() => | ||||
|   isEdit.value ? t('estimates.edit_estimate') : t('estimates.new_estimate') | ||||
| ) | ||||
|  | ||||
| let isEdit = computed(() => route.name === 'estimates.edit') | ||||
|  | ||||
| const rules = { | ||||
|   estimate_date: { | ||||
|     required: helpers.withMessage(t('validation.required'), required), | ||||
|   }, | ||||
|   expiry_date: { | ||||
|     required: helpers.withMessage(t('validation.required'), required), | ||||
|   }, | ||||
|   estimate_number: { | ||||
|     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), | ||||
|   }, | ||||
|   exchange_rate: { | ||||
|     required: requiredIf(function () { | ||||
|       helpers.withMessage(t('validation.required'), required) | ||||
|       return estimateStore.showExchangeRate | ||||
|     }), | ||||
|     decimal: helpers.withMessage(t('validation.valid_exchange_rate'), decimal), | ||||
|   }, | ||||
| } | ||||
|  | ||||
| const v$ = useVuelidate( | ||||
|   rules, | ||||
|   computed(() => estimateStore.newEstimate), | ||||
|   { $scope: estimateValidationScope } | ||||
| ) | ||||
|  | ||||
| watch( | ||||
|   () => estimateStore.newEstimate.customer, | ||||
|   (newVal) => { | ||||
|     if (newVal && newVal.currency) { | ||||
|       estimateStore.newEstimate.selectedCurrency = newVal.currency | ||||
|     } else { | ||||
|       estimateStore.newEstimate.selectedCurrency = | ||||
|         companyStore.selectedCompanyCurrency | ||||
|     } | ||||
|   } | ||||
| ) | ||||
|  | ||||
| estimateStore.resetCurrentEstimate() | ||||
| customFieldStore.resetCustomFields() | ||||
| v$.value.$reset | ||||
| estimateStore.fetchEstimateInitialSettings(isEdit.value) | ||||
|  | ||||
| async function submitForm() { | ||||
|   v$.value.$touch() | ||||
|  | ||||
|   if (v$.value.$invalid) { | ||||
|     return false | ||||
|   } | ||||
|  | ||||
|   isSaving.value = true | ||||
|  | ||||
|   let data = { | ||||
|     ...estimateStore.newEstimate, | ||||
|     sub_total: estimateStore.getSubTotal, | ||||
|     total: estimateStore.getTotal, | ||||
|     tax: estimateStore.getTotalTax, | ||||
|   } | ||||
|  | ||||
|   const action = isEdit.value | ||||
|     ? estimateStore.updateEstimate | ||||
|     : estimateStore.addEstimate | ||||
|  | ||||
|   try { | ||||
|     let res = await action(data) | ||||
|  | ||||
|     if (res.data.data) { | ||||
|       router.push(`/admin/estimates/${res.data.data.id}/view`) | ||||
|     } | ||||
|   } catch (err) { | ||||
|     console.error(err) | ||||
|   } | ||||
|  | ||||
|   isSaving.value = false | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,102 @@ | ||||
| <template> | ||||
|   <div class="md:grid-cols-12 grid-cols-1 md:gap-x-6 mt-6 mb-8 grid gap-y-5"> | ||||
|     <BaseCustomerSelectPopup | ||||
|       v-model="estimateStore.newEstimate.customer" | ||||
|       :valid="v.customer_id" | ||||
|       :content-loading="isLoading" | ||||
|       type="estimate" | ||||
|       class="col-span-5 pr-0" | ||||
|     /> | ||||
|  | ||||
|     <BaseInputGrid class="col-span-7"> | ||||
|       <BaseInputGroup | ||||
|         :label="$t('reports.estimates.estimate_date')" | ||||
|         :content-loading="isLoading" | ||||
|         required | ||||
|         :error="v.estimate_date.$error && v.estimate_date.$errors[0].$message" | ||||
|       > | ||||
|         <BaseDatePicker | ||||
|           v-model="estimateStore.newEstimate.estimate_date" | ||||
|           :content-loading="isLoading" | ||||
|           :calendar-button="true" | ||||
|           calendar-button-icon="calendar" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup | ||||
|         :label="$t('estimates.expiry_date')" | ||||
|         :content-loading="isLoading" | ||||
|         required | ||||
|         :error="v.expiry_date.$error && v.expiry_date.$errors[0].$message" | ||||
|       > | ||||
|         <BaseDatePicker | ||||
|           v-model="estimateStore.newEstimate.expiry_date" | ||||
|           :content-loading="isLoading" | ||||
|           :calendar-button="true" | ||||
|           calendar-button-icon="calendar" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup | ||||
|         :label="$t('estimates.estimate_number')" | ||||
|         :content-loading="isLoading" | ||||
|         required | ||||
|         :error="v.estimate_number.$error && v.estimate_number.$errors[0].$message" | ||||
|       > | ||||
|         <BaseInput | ||||
|           v-model="estimateStore.newEstimate.estimate_number" | ||||
|           :content-loading="isLoading" | ||||
|         > | ||||
|         </BaseInput> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <!-- <BaseInputGroup | ||||
|         :label="$t('estimates.ref_number')" | ||||
|         :content-loading="isLoading" | ||||
|         :error=" | ||||
|           v.reference_number.$error && v.reference_number.$errors[0].$message | ||||
|         " | ||||
|       > | ||||
|         <BaseInput | ||||
|           v-model="estimateStore.newEstimate.reference_number" | ||||
|           :content-loading="isLoading" | ||||
|           @input="v.reference_number.$touch()" | ||||
|         > | ||||
|           <template #left="slotProps"> | ||||
|             <BaseIcon name="HashtagIcon" :class="slotProps.class" /> | ||||
|           </template> | ||||
|         </BaseInput> | ||||
|       </BaseInputGroup> --> | ||||
|       <ExchangeRateConverter | ||||
|         :store="estimateStore" | ||||
|         store-prop="newEstimate" | ||||
|         :v="v" | ||||
|         :is-loading="isLoading" | ||||
|         :is-edit="isEdit" | ||||
|         :customer-currency="estimateStore.newEstimate.currency_id" | ||||
|       /> | ||||
|     </BaseInputGrid> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { useEstimateStore } from '@/scripts/stores/estimate' | ||||
| import ExchangeRateConverter from '@/scripts/components/estimate-invoice-common/ExchangeRateConverter.vue' | ||||
|  | ||||
| const props = defineProps({ | ||||
|   v: { | ||||
|     type: Object, | ||||
|     default: null, | ||||
|   }, | ||||
|   isLoading: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
|   isEdit: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| const estimateStore = useEstimateStore() | ||||
| </script> | ||||
		Reference in New Issue
	
	Block a user