mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-10-30 21:21:09 -04:00 
			
		
		
		
	v6 update
This commit is contained in:
		
							
								
								
									
										259
									
								
								resources/scripts/customer/views/payments/Index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								resources/scripts/customer/views/payments/Index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,259 @@ | ||||
| <template> | ||||
|   <BasePage> | ||||
|     <BasePageHeader :title="$t('payments.title')"> | ||||
|       <BaseBreadcrumb slot="breadcrumbs"> | ||||
|         <BaseBreadcrumbItem | ||||
|           :title="$t('general.home')" | ||||
|           :to="`/${globalStore.companySlug}/customer/dashboard`" | ||||
|         /> | ||||
|  | ||||
|         <BaseBreadcrumbItem :title="$tc('payments.payment', 2)" to="#" active /> | ||||
|       </BaseBreadcrumb> | ||||
|  | ||||
|       <template #actions> | ||||
|         <BaseButton | ||||
|           v-show="paymentStore.totalPayments" | ||||
|           variant="primary-outline" | ||||
|           @click="toggleFilter" | ||||
|         > | ||||
|           {{ $t('general.filter') }} | ||||
|           <template #right="slotProps"> | ||||
|             <BaseIcon | ||||
|               v-if="!showFilters" | ||||
|               :class="slotProps.class" | ||||
|               name="FilterIcon" | ||||
|             /> | ||||
|             <BaseIcon v-else :class="slotProps.class" name="XIcon" /> | ||||
|           </template> | ||||
|         </BaseButton> | ||||
|       </template> | ||||
|     </BasePageHeader> | ||||
|  | ||||
|     <BaseFilterWrapper v-show="showFilters" @clear="clearFilter"> | ||||
|       <BaseInputGroup :label="$t('payments.payment_number')" class="px-3"> | ||||
|         <BaseInput | ||||
|           v-model="filters.payment_number" | ||||
|           :placeholder="$t('payments.payment_number')" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|  | ||||
|       <BaseInputGroup :label="$t('payments.payment_mode')" class="px-3"> | ||||
|         <BaseMultiselect | ||||
|           v-model="filters.payment_mode" | ||||
|           value-prop="id" | ||||
|           track-by="name" | ||||
|           :filter-results="false" | ||||
|           label="name" | ||||
|           resolve-on-load | ||||
|           :delay="100" | ||||
|           searchable | ||||
|           :options="searchPayment" | ||||
|           :placeholder="$t('payments.payment_mode')" | ||||
|         /> | ||||
|       </BaseInputGroup> | ||||
|     </BaseFilterWrapper> | ||||
|  | ||||
|     <BaseEmptyPlaceholder | ||||
|       v-if="showEmptyScreen" | ||||
|       :title="$t('payments.no_payments')" | ||||
|       :description="$t('payments.list_of_payments')" | ||||
|     > | ||||
|       <CapsuleIcon class="mt-5 mb-4" /> | ||||
|     </BaseEmptyPlaceholder> | ||||
|  | ||||
|     <div v-show="!showEmptyScreen" class="relative table-container"> | ||||
|       <BaseTable | ||||
|         ref="table" | ||||
|         :data="fetchData" | ||||
|         :columns="paymentColumns" | ||||
|         :placeholder-count="paymentStore.totalPayments >= 20 ? 10 : 5" | ||||
|         class="mt-10" | ||||
|       > | ||||
|         <template #cell-payment_date="{ row }"> | ||||
|           {{ row.data.formatted_payment_date }} | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-payment_number="{ row }"> | ||||
|           <router-link | ||||
|             :to="{ | ||||
|               path: `payments/${row.data.id}/view`, | ||||
|             }" | ||||
|             class="font-medium text-primary-500" | ||||
|           > | ||||
|             {{ row.data.payment_number }} | ||||
|           </router-link> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-payment_mode="{ row }"> | ||||
|           <span> | ||||
|             {{ | ||||
|               row.data.payment_method | ||||
|                 ? row.data.payment_method.name | ||||
|                 : $t('payments.not_selected') | ||||
|             }} | ||||
|           </span> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-invoice_number="{ row }"> | ||||
|           <span> | ||||
|             {{ | ||||
|               row.data.invoice?.invoice_number | ||||
|                 ? row.data.invoice?.invoice_number | ||||
|                 : $t('payments.no_invoice') | ||||
|             }} | ||||
|           </span> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-amount="{ row }"> | ||||
|           <div v-html="utils.formatMoney(row.data.amount, currency)" /> | ||||
|         </template> | ||||
|  | ||||
|         <template #cell-actions="{ row }"> | ||||
|           <BaseDropdown> | ||||
|             <template #activator> | ||||
|               <BaseIcon name="DotsHorizontalIcon" class="w-5 text-gray-500" /> | ||||
|             </template> | ||||
|             <router-link :to="`payments/${row.data.id}/view`"> | ||||
|               <BaseDropdownItem> | ||||
|                 <BaseIcon name="EyeIcon" class="h-5 mr-3 text-gray-600" /> | ||||
|                 {{ $t('general.view') }} | ||||
|               </BaseDropdownItem> | ||||
|             </router-link> | ||||
|           </BaseDropdown> | ||||
|         </template> | ||||
|       </BaseTable> | ||||
|     </div> | ||||
|   </BasePage> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { debouncedWatch } from '@vueuse/core' | ||||
| import BaseTable from '@/scripts/components/base/base-table/BaseTable.vue' | ||||
| import CapsuleIcon from '@/scripts/components/icons/empty/CapsuleIcon.vue' | ||||
| import { ref, reactive, inject, computed } from 'vue' | ||||
| import BaseDropdownItem from '@/scripts/components/base/BaseDropdownItem.vue' | ||||
| import BaseDropdown from '@/scripts/components/base/BaseDropdown.vue' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { usePaymentStore } from '@/scripts/customer/stores/payment' | ||||
| import { useGlobalStore } from '@/scripts/customer/stores/global' | ||||
| import { useRoute } from 'vue-router' | ||||
|  | ||||
| const { tm, t } = useI18n() | ||||
| let showFilters = ref(false) | ||||
| let sortedBy = ref('created_at') | ||||
| let isFetchingInitialData = ref(true) | ||||
| let table = ref(null) | ||||
| const filters = reactive({ | ||||
|   payment_mode: '', | ||||
|   payment_number: '', | ||||
| }) | ||||
|  | ||||
| //Utils | ||||
| const utils = inject('utils') | ||||
|  | ||||
| //Store | ||||
| const route = useRoute() | ||||
| const paymentStore = usePaymentStore() | ||||
| const globalStore = useGlobalStore() | ||||
|  | ||||
| // Computed Props | ||||
|  | ||||
| const showEmptyScreen = computed(() => { | ||||
|   return !paymentStore.totalPayments && !isFetchingInitialData.value | ||||
| }) | ||||
|  | ||||
| const currency = computed(() => { | ||||
|   return globalStore.currency | ||||
| }) | ||||
|  | ||||
| // Payment Table columns Data | ||||
|  | ||||
| const paymentColumns = computed(() => { | ||||
|   return [ | ||||
|     { | ||||
|       key: 'payment_date', | ||||
|       label: t('payments.date'), | ||||
|       thClass: 'extra', | ||||
|       tdClass: 'font-medium text-gray-900', | ||||
|     }, | ||||
|     { key: 'payment_number', label: t('payments.payment_number') }, | ||||
|     { key: 'payment_mode', label: t('payments.payment_mode') }, | ||||
|     { key: 'invoice_number', label: t('invoices.invoice_number') }, | ||||
|     { key: 'amount', label: t('payments.amount') }, | ||||
|     { | ||||
|       key: 'actions', | ||||
|       label: '', | ||||
|       tdClass: 'text-right text-sm font-medium', | ||||
|       sortable: false, | ||||
|     }, | ||||
|   ] | ||||
| }) | ||||
|  | ||||
| // Created | ||||
|  | ||||
| debouncedWatch( | ||||
|   filters, | ||||
|   () => { | ||||
|     setFilters() | ||||
|   }, | ||||
|   { debounce: 500 } | ||||
| ) | ||||
|  | ||||
| // Methods | ||||
|  | ||||
| async function searchPayment(search) { | ||||
|   let res = await paymentStore.fetchPaymentModes( | ||||
|     search, | ||||
|     globalStore.companySlug | ||||
|   ) | ||||
|   return res.data.data | ||||
| } | ||||
|  | ||||
| async function fetchData({ page, filter, sort }) { | ||||
|   let data = { | ||||
|     payment_method_id: | ||||
|       filters.payment_mode !== null ? filters.payment_mode : '', | ||||
|     payment_number: filters.payment_number, | ||||
|     orderByField: sort.fieldName || 'created_at', | ||||
|     orderBy: sort.order || 'desc', | ||||
|     page, | ||||
|   } | ||||
|  | ||||
|   isFetchingInitialData.value = true | ||||
|   let response = await paymentStore.fetchPayments(data, globalStore.companySlug) | ||||
|  | ||||
|   isFetchingInitialData.value = false | ||||
|  | ||||
|   return { | ||||
|     data: response.data.data, | ||||
|     pagination: { | ||||
|       totalPages: response.data.meta.last_page, | ||||
|       currentPage: page, | ||||
|       totalCount: response.data.meta.total, | ||||
|       limit: 10, | ||||
|     }, | ||||
|   } | ||||
| } | ||||
|  | ||||
| function refreshTable() { | ||||
|   table.value.refresh() | ||||
| } | ||||
|  | ||||
| function setFilters() { | ||||
|   refreshTable() | ||||
| } | ||||
|  | ||||
| function clearFilter() { | ||||
|   filters.customer = '' | ||||
|   filters.payment_mode = '' | ||||
|   filters.payment_number = '' | ||||
| } | ||||
|  | ||||
| function toggleFilter() { | ||||
|   if (showFilters.value) { | ||||
|     clearFilter() | ||||
|   } | ||||
|  | ||||
|   showFilters.value = !showFilters.value | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										374
									
								
								resources/scripts/customer/views/payments/View.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										374
									
								
								resources/scripts/customer/views/payments/View.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,374 @@ | ||||
| <template> | ||||
|   <BasePage class="xl:pl-96"> | ||||
|     <BasePageHeader :title="pageTitle.payment_number"> | ||||
|       <template #actions> | ||||
|         <BaseButton | ||||
|           :disabled="isSendingEmail" | ||||
|           variant="primary-outline" | ||||
|           tag="a" | ||||
|           download | ||||
|           :href="`/payments/pdf/${payment.unique_hash}`" | ||||
|         > | ||||
|           <template #left="slotProps"> | ||||
|             <BaseIcon name="DownloadIcon" :class="slotProps.class" /> | ||||
|             {{ $t('general.download') }} | ||||
|           </template> | ||||
|         </BaseButton> | ||||
|       </template> | ||||
|     </BasePageHeader> | ||||
|  | ||||
|     <!-- Sidebar --> | ||||
|     <div | ||||
|       class="fixed top-0 left-0 hidden h-full pt-16 pb-4 bg-white w-88 xl:block" | ||||
|     > | ||||
|       <div | ||||
|         class=" | ||||
|           flex | ||||
|           items-center | ||||
|           justify-between | ||||
|           px-4 | ||||
|           pt-8 | ||||
|           pb-6 | ||||
|           border border-gray-200 border-solid | ||||
|         " | ||||
|       > | ||||
|         <BaseInput | ||||
|           v-model="searchData.payment_number" | ||||
|           :placeholder="$t('general.search')" | ||||
|           type="text" | ||||
|           variant="gray" | ||||
|           @input="onSearch" | ||||
|         > | ||||
|           <template #right> | ||||
|             <BaseIcon name="SearchIcon" class="h-5 text-gray-400" /> | ||||
|           </template> | ||||
|         </BaseInput> | ||||
|  | ||||
|         <div class="flex ml-3" role="group" aria-label="First group"> | ||||
|           <BaseDropdown | ||||
|             position="bottom-start" | ||||
|             width-class="w-50" | ||||
|             position-class="left-0" | ||||
|           > | ||||
|             <template #activator> | ||||
|               <BaseButton variant="gray"> | ||||
|                 <BaseIcon name="FilterIcon" class="h-5" /> | ||||
|               </BaseButton> | ||||
|             </template> | ||||
|  | ||||
|             <div | ||||
|               class=" | ||||
|                 px-4 | ||||
|                 py-1 | ||||
|                 pb-2 | ||||
|                 mb-2 | ||||
|                 text-sm | ||||
|                 border-b border-gray-200 border-solid | ||||
|               " | ||||
|             > | ||||
|               {{ $t('general.sort_by') }} | ||||
|             </div> | ||||
|  | ||||
|             <div class="px-2"> | ||||
|               <BaseDropdownItem class="rounded-md pt-3 hover:rounded-md"> | ||||
|                 <BaseInputGroup class="-mt-3 font-normal"> | ||||
|                   <BaseRadio | ||||
|                     id="filter_invoice_number" | ||||
|                     v-model="searchData.orderByField" | ||||
|                     :label="$t('invoices.title')" | ||||
|                     size="sm" | ||||
|                     name="filter" | ||||
|                     value="invoice_number" | ||||
|                     @update:modelValue="onSearch" | ||||
|                   /> | ||||
|                 </BaseInputGroup> | ||||
|               </BaseDropdownItem> | ||||
|             </div> | ||||
|  | ||||
|             <div class="px-2"> | ||||
|               <BaseDropdownItem class="rounded-md pt-3 hover:rounded-md"> | ||||
|                 <BaseInputGroup class="-mt-3 font-normal"> | ||||
|                   <BaseRadio | ||||
|                     id="filter_payment_date" | ||||
|                     v-model="searchData.orderByField" | ||||
|                     :label="$t('payments.date')" | ||||
|                     size="sm" | ||||
|                     name="filter" | ||||
|                     value="payment_date" | ||||
|                     @update:modelValue="onSearch" | ||||
|                   /> | ||||
|                 </BaseInputGroup> | ||||
|               </BaseDropdownItem> | ||||
|             </div> | ||||
|  | ||||
|             <div class="px-2"> | ||||
|               <BaseDropdownItem class="rounded-md pt-3 hover:rounded-md"> | ||||
|                 <BaseInputGroup class="-mt-3 font-normal"> | ||||
|                   <BaseRadio | ||||
|                     id="filter_payment_number" | ||||
|                     v-model="searchData.orderByField" | ||||
|                     :label="$t('payments.payment_number')" | ||||
|                     size="sm" | ||||
|                     name="filter" | ||||
|                     value="payment_number" | ||||
|                     @update:modelValue="onSearch" | ||||
|                   /> | ||||
|                 </BaseInputGroup> | ||||
|               </BaseDropdownItem> | ||||
|             </div> | ||||
|           </BaseDropdown> | ||||
|  | ||||
|           <BaseButton class="ml-1" variant="white" @click="sortData"> | ||||
|             <BaseIcon v-if="getOrderBy" name="SortAscendingIcon" class="h-5" /> | ||||
|             <BaseIcon v-else name="SortDescendingIcon" class="h-5" /> | ||||
|           </BaseButton> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div | ||||
|         class=" | ||||
|           h-full | ||||
|           pb-32 | ||||
|           overflow-y-scroll | ||||
|           border-l border-gray-200 border-solid | ||||
|           sw-scroll | ||||
|         " | ||||
|       > | ||||
|         <router-link | ||||
|           v-for="(payment, index) in paymentStore.payments" | ||||
|           :id="'payment-' + payment.id" | ||||
|           :key="index" | ||||
|           :to="`/${globalStore.companySlug}/customer/payments/${payment.id}/view`" | ||||
|           :class="[ | ||||
|             'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent', | ||||
|             { | ||||
|               'bg-gray-100 border-l-4 border-primary-500 border-solid': | ||||
|                 hasActiveUrl(payment.id), | ||||
|             }, | ||||
|           ]" | ||||
|           style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)" | ||||
|         > | ||||
|           <div class="flex-2"> | ||||
|             <div | ||||
|               class=" | ||||
|                 mb-1 | ||||
|                 text-md | ||||
|                 not-italic | ||||
|                 font-medium | ||||
|                 leading-5 | ||||
|                 text-gray-500 | ||||
|                 capitalize | ||||
|               " | ||||
|             > | ||||
|               {{ payment.payment_number }} | ||||
|             </div> | ||||
|           </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="payment.amount" | ||||
|               :currency="payment.currency" | ||||
|             /> | ||||
|  | ||||
|             <div class="text-sm text-right text-gray-500 non-italic"> | ||||
|               {{ payment.formatted_payment_date }} | ||||
|             </div> | ||||
|           </div> | ||||
|         </router-link> | ||||
|  | ||||
|         <p | ||||
|           v-if="!paymentStore.payments.length" | ||||
|           class="flex justify-center px-4 mt-5 text-sm text-gray-600" | ||||
|         > | ||||
|           {{ $t('payments.no_matching_payments') }} | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- pdf --> | ||||
|     <div | ||||
|       class="flex flex-col min-h-0 mt-8 overflow-hidden" | ||||
|       style="height: 75vh" | ||||
|     > | ||||
|       <iframe | ||||
|         v-if="shareableLink" | ||||
|         :src="shareableLink" | ||||
|         class="flex-1 border border-gray-400 border-solid rounded-md" | ||||
|       /> | ||||
|     </div> | ||||
|   </BasePage> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import BaseDropdown from '@/scripts/components/base/BaseDropdown.vue' | ||||
| import BaseDropdownItem from '@/scripts/components/base/BaseDropdownItem.vue' | ||||
| import { debounce } from 'lodash' | ||||
| import { ref, reactive, computed, inject, watch } from 'vue' | ||||
| import { useRoute } from 'vue-router' | ||||
| import { useNotificationStore } from '@/scripts/stores/notification' | ||||
| import moment from 'moment' | ||||
| import { usePaymentStore } from '@/scripts/customer/stores/payment' | ||||
| import { useGlobalStore } from '@/scripts/customer/stores/global' | ||||
|  | ||||
| // Router | ||||
| const route = useRoute() | ||||
| const paymentStore = usePaymentStore() | ||||
| const globalStore = useGlobalStore() | ||||
|  | ||||
| const { tm, t } = useI18n() | ||||
| // let id = ref(null) | ||||
|  | ||||
| let payment = reactive({}) | ||||
| let searchData = reactive({ | ||||
|   orderBy: '', | ||||
|   orderByField: '', | ||||
|   payment_number: '', | ||||
| }) | ||||
|  | ||||
| let isSearching = ref(false) | ||||
| let isSendingEmail = ref(false) | ||||
| let isMarkingAsSent = ref(false) | ||||
|  | ||||
| //Utils | ||||
|  | ||||
| const $utils = inject('utils') | ||||
|  | ||||
| //Store | ||||
|  | ||||
| const notificationStore = useNotificationStore() | ||||
|  | ||||
| // Computed Props | ||||
| const pageTitle = computed(() => { | ||||
|   return paymentStore.selectedViewPayment | ||||
| }) | ||||
|  | ||||
| const getOrderBy = computed(() => { | ||||
|   if (searchData.orderBy === 'asc' || searchData.orderBy == null) { | ||||
|     return true | ||||
|   } | ||||
|   return false | ||||
| }) | ||||
|  | ||||
| const getOrderName = computed(() => | ||||
|   getOrderBy.value ? tm('general.ascending') : tm('general.descending') | ||||
| ) | ||||
|  | ||||
| const shareableLink = computed(() => { | ||||
|   return payment.unique_hash ? `/payments/pdf/${payment.unique_hash}` : false | ||||
| }) | ||||
|  | ||||
| // Watcher | ||||
|  | ||||
| watch(route, () => { | ||||
|   loadPayment() | ||||
| }) | ||||
|  | ||||
| // Created | ||||
|  | ||||
| loadPayments() | ||||
| loadPayment() | ||||
|  | ||||
| onSearch = debounce(onSearch, 500) | ||||
|  | ||||
| // Methods | ||||
|  | ||||
| function hasActiveUrl(id) { | ||||
|   return route.params.id == id | ||||
| } | ||||
|  | ||||
| async function loadPayments() { | ||||
|   await paymentStore.fetchPayments( | ||||
|     { | ||||
|       limit: 'all', | ||||
|     }, | ||||
|     globalStore.companySlug | ||||
|   ) | ||||
|  | ||||
|   setTimeout(() => { | ||||
|     scrollToPayment() | ||||
|   }, 500) | ||||
| } | ||||
|  | ||||
| async function loadPayment() { | ||||
|   if (route && route.params.id) { | ||||
|     let response = await paymentStore.fetchViewPayment( | ||||
|       { | ||||
|         id: route.params.id, | ||||
|       }, | ||||
|       globalStore.companySlug | ||||
|     ) | ||||
|  | ||||
|     if (response.data) { | ||||
|       Object.assign(payment, response.data.data) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| function scrollToPayment() { | ||||
|   const el = document.getElementById(`payment-${route.params.id}`) | ||||
|  | ||||
|   if (el) { | ||||
|     el.scrollIntoView({ behavior: 'smooth' }) | ||||
|     el.classList.add('shake') | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function onSearch() { | ||||
|   let data = {} | ||||
|  | ||||
|   if ( | ||||
|     searchData.payment_number !== '' && | ||||
|     searchData.payment_number !== null && | ||||
|     searchData.payment_number !== undefined | ||||
|   ) { | ||||
|     data.payment_number = searchData.payment_number | ||||
|   } | ||||
|  | ||||
|   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 | ||||
|   try { | ||||
|     let response = await paymentStore.searchPayment( | ||||
|       data, | ||||
|       globalStore.companySlug | ||||
|     ) | ||||
|     isSearching.value = false | ||||
|  | ||||
|     if (response.data.data) { | ||||
|       paymentStore.payments = response.data.data | ||||
|     } | ||||
|   } catch (error) { | ||||
|     isSearching.value = false | ||||
|   } | ||||
| } | ||||
|  | ||||
| function sortData() { | ||||
|   if (searchData.orderBy === 'asc') { | ||||
|     searchData.orderBy = 'desc' | ||||
|     onSearch() | ||||
|     return true | ||||
|   } | ||||
|   searchData.orderBy = 'asc' | ||||
|   onSearch() | ||||
|   return true | ||||
| } | ||||
| </script> | ||||
		Reference in New Issue
	
	Block a user