mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-10-31 05:31:10 -04:00 
			
		
		
		
	v5.0.0 update
This commit is contained in:
		| @ -0,0 +1,98 @@ | ||||
| <template> | ||||
|   <div class="relative table-container"> | ||||
|     <BaseTable | ||||
|       ref="table" | ||||
|       :data="recurringInvoiceStore.newRecurringInvoice.invoices" | ||||
|       :columns="invoiceColumns" | ||||
|       :loading="recurringInvoiceStore.isFetchingViewData" | ||||
|       :placeholder-count="5" | ||||
|       class="mt-5" | ||||
|     > | ||||
|       <!-- Invoice Number  --> | ||||
|       <template #cell-invoice_number="{ row }"> | ||||
|         <router-link | ||||
|           :to="{ path: `/admin/invoices/${row.data.id}/view` }" | ||||
|           class="font-medium text-primary-500" | ||||
|         > | ||||
|           {{ row.data.invoice_number }} | ||||
|         </router-link> | ||||
|       </template> | ||||
|  | ||||
|       <!-- Invoice Due amount  --> | ||||
|       <template #cell-total="{ row }"> | ||||
|         <BaseFormatMoney | ||||
|           :amount="row.data.due_amount" | ||||
|           :currency="row.data.currency" | ||||
|         /> | ||||
|       </template> | ||||
|  | ||||
|       <!-- Invoice status  --> | ||||
|       <template #cell-status="{ row }"> | ||||
|         <BaseInvoiceStatusBadge :status="row.data.status" class="px-3 py-1"> | ||||
|           {{ row.data.status }} | ||||
|         </BaseInvoiceStatusBadge> | ||||
|       </template> | ||||
|  | ||||
|       <!-- Actions --> | ||||
|       <template v-if="hasAtleastOneAbility()" #cell-actions="{ row }"> | ||||
|         <InvoiceDropdown :row="row.data" :table="table" /> | ||||
|       </template> | ||||
|     </BaseTable> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { computed, ref, inject } from 'vue' | ||||
| import { useRouter } from 'vue-router' | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { useUserStore } from '@/scripts/stores/user' | ||||
| import { useRecurringInvoiceStore } from '@/scripts/stores/recurring-invoice' | ||||
| import abilities from '@/scripts/stub/abilities' | ||||
| import InvoiceDropdown from '@/scripts/components/dropdowns/InvoiceIndexDropdown.vue' | ||||
|  | ||||
| const recurringInvoiceStore = useRecurringInvoiceStore() | ||||
|  | ||||
| const table = ref(null) | ||||
| const baseSelect = ref(null) | ||||
| const utils = inject('$utils') | ||||
| const { t } = useI18n() | ||||
| const currency = ref(null) | ||||
| const router = useRouter() | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const invoiceColumns = computed(() => { | ||||
|   return [ | ||||
|     { | ||||
|       key: 'invoice_date', | ||||
|       label: t('invoices.date'), | ||||
|       thClass: 'extra', | ||||
|       tdClass: 'font-medium text-gray-900', | ||||
|     }, | ||||
|     { key: 'invoice_number', label: t('invoices.invoice') }, | ||||
|     { key: 'customer.name', label: t('invoices.customer') }, | ||||
|     { key: 'status', label: t('invoices.status') }, | ||||
|     { key: 'total', label: t('invoices.total') }, | ||||
|  | ||||
|     { | ||||
|       key: 'actions', | ||||
|       label: t('invoices.action'), | ||||
|       tdClass: 'text-right text-sm font-medium', | ||||
|       thClass: 'text-right', | ||||
|       sortable: false, | ||||
|     }, | ||||
|   ] | ||||
| }) | ||||
|  | ||||
| function hasAtleastOneAbility() { | ||||
|   return userStore.hasAbilities([ | ||||
|     abilities.DELETE_INVOICE, | ||||
|     abilities.EDIT_INVOICE, | ||||
|     abilities.VIEW_INVOICE, | ||||
|     abilities.SEND_INVOICE, | ||||
|   ]) | ||||
| } | ||||
|  | ||||
| function refreshTable() { | ||||
|   table.value && table.value.refresh() | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,94 @@ | ||||
| <template> | ||||
|   <BaseCard class="mt-10"> | ||||
|     <BaseHeading> | ||||
|       {{ $t('customers.basic_info') }} | ||||
|     </BaseHeading> | ||||
|  | ||||
|     <BaseDescriptionList class="mt-5"> | ||||
|       <BaseDescriptionListItem | ||||
|         :label="$t('recurring_invoices.starts_at')" | ||||
|         :content-loading="isLoading" | ||||
|         :value="recurringInvoiceStore.newRecurringInvoice?.formatted_starts_at" | ||||
|       /> | ||||
|  | ||||
|       <BaseDescriptionListItem | ||||
|         :label="$t('recurring_invoices.next_invoice_date')" | ||||
|         :content-loading="isLoading" | ||||
|         :value=" | ||||
|           recurringInvoiceStore.newRecurringInvoice?.formatted_next_invoice_at | ||||
|         " | ||||
|       /> | ||||
|  | ||||
|       <BaseDescriptionListItem | ||||
|         v-if=" | ||||
|           recurringInvoiceStore.newRecurringInvoice?.limit_date && | ||||
|           recurringInvoiceStore.newRecurringInvoice?.limit_by !== 'NONE' | ||||
|         " | ||||
|         :label="$t('recurring_invoices.limit_date')" | ||||
|         :content-loading="isLoading" | ||||
|         :value="recurringInvoiceStore.newRecurringInvoice?.limit_date" | ||||
|       /> | ||||
|  | ||||
|       <BaseDescriptionListItem | ||||
|         v-if=" | ||||
|           recurringInvoiceStore.newRecurringInvoice?.limit_date && | ||||
|           recurringInvoiceStore.newRecurringInvoice?.limit_by !== 'NONE' | ||||
|         " | ||||
|         :label="$t('recurring_invoices.limit_by')" | ||||
|         :content-loading="isLoading" | ||||
|         :value="recurringInvoiceStore.newRecurringInvoice?.limit_by" | ||||
|       /> | ||||
|  | ||||
|       <BaseDescriptionListItem | ||||
|         v-if="recurringInvoiceStore.newRecurringInvoice?.limit_count" | ||||
|         :label="$t('recurring_invoices.limit_count')" | ||||
|         :value="recurringInvoiceStore.newRecurringInvoice?.limit_count" | ||||
|         :content-loading="isLoading" | ||||
|       /> | ||||
|  | ||||
|       <BaseDescriptionListItem | ||||
|         v-if="recurringInvoiceStore.newRecurringInvoice?.selectedFrequency" | ||||
|         :label="$t('recurring_invoices.frequency.title')" | ||||
|         :value=" | ||||
|           recurringInvoiceStore.newRecurringInvoice?.selectedFrequency?.label | ||||
|         " | ||||
|         :content-loading="isLoading" | ||||
|       /> | ||||
|     </BaseDescriptionList> | ||||
|  | ||||
|     <BaseHeading class="mt-8"> | ||||
|       {{ $t('invoices.title', 2) }} | ||||
|     </BaseHeading> | ||||
|  | ||||
|     <Invoices /> | ||||
|   </BaseCard> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { ref, computed, watch, reactive, inject } from 'vue' | ||||
| import { useRoute } from 'vue-router' | ||||
| import { useRecurringInvoiceStore } from '@/scripts/stores/recurring-invoice' | ||||
| import Invoices from './Invoices.vue' | ||||
|  | ||||
| const recurringInvoiceStore = useRecurringInvoiceStore() | ||||
|  | ||||
| const route = useRoute() | ||||
|  | ||||
| let isLoading = computed(() => { | ||||
|   return recurringInvoiceStore.isFetchingViewData | ||||
| }) | ||||
|  | ||||
| watch( | ||||
|   route, | ||||
|   () => { | ||||
|     if (route.params.id) { | ||||
|       loadRecurringInvoice() | ||||
|     } | ||||
|   }, | ||||
|   { immediate: true } | ||||
| ) | ||||
|  | ||||
| async function loadRecurringInvoice() { | ||||
|   await recurringInvoiceStore.fetchRecurringInvoice(route.params.id) | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,322 @@ | ||||
| <script setup> | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import { computed, reactive, ref, watch, inject } from 'vue' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import { debounce } from 'lodash' | ||||
| import { useRecurringInvoiceStore } from '@/scripts/stores/recurring-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 LoadingIcon from '@/scripts/components/icons/LoadingIcon.vue' | ||||
|  | ||||
| const modalStore = useModalStore() | ||||
| const recurringInvoiceStore = useRecurringInvoiceStore() | ||||
| const notificationStore = useNotificationStore() | ||||
| const userStore = useUserStore() | ||||
| const dialogStore = useDialogStore() | ||||
|  | ||||
| const { t } = useI18n() | ||||
| const id = ref(null) | ||||
| const count = ref(null) | ||||
| const currency = ref(null) | ||||
| const route = useRoute() | ||||
| const router = useRouter() | ||||
| const status = ref([ | ||||
|   'DRAFT', | ||||
|   'SENT', | ||||
|   'VIEWED', | ||||
|   'EXPIRED', | ||||
|   'ACCEPTED', | ||||
|   'REJECTED', | ||||
| ]) | ||||
| const isSearching = ref(false) | ||||
| const isLoading = ref(false) | ||||
|  | ||||
| const searchData = reactive({ | ||||
|   orderBy: null, | ||||
|   orderByField: null, | ||||
|   searchText: null, | ||||
| }) | ||||
|  | ||||
| const getOrderBy = computed(() => { | ||||
|   if (searchData.orderBy === 'asc' || searchData.orderBy == null) { | ||||
|     return true | ||||
|   } | ||||
|   return false | ||||
| }) | ||||
|  | ||||
| function hasActiveUrl(id) { | ||||
|   return route.params.id == id | ||||
| } | ||||
|  | ||||
| async function loadRecurringInvoices() { | ||||
|   isLoading.value = true | ||||
|   await recurringInvoiceStore.fetchRecurringInvoices() | ||||
|   isLoading.value = false | ||||
|  | ||||
|   setTimeout(() => { | ||||
|     scrollToRecurringInvoice() | ||||
|   }, 500) | ||||
| } | ||||
|  | ||||
| function scrollToRecurringInvoice() { | ||||
|   const el = document.getElementById(`recurring-invoice-${route.params.id}`) | ||||
|   if (el) { | ||||
|     el.scrollIntoView({ behavior: 'smooth' }) | ||||
|     el.classList.add('shake') | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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 recurringInvoiceStore.searchRecurringInvoice(data) | ||||
|   isSearching.value = false | ||||
|   if (response.data) { | ||||
|     recurringInvoiceStore.recurringInvoices = response.data.data | ||||
|   } | ||||
| } | ||||
|  | ||||
| function sortData() { | ||||
|   if (searchData.orderBy === 'asc') { | ||||
|     searchData.orderBy = 'desc' | ||||
|     onSearched() | ||||
|     return true | ||||
|   } | ||||
|   searchData.orderBy = 'asc' | ||||
|   onSearched() | ||||
|   return true | ||||
| } | ||||
|  | ||||
| loadRecurringInvoices() | ||||
| onSearched = debounce(onSearched, 500) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <!-- 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" class="h-5" /> | ||||
|             </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_next_invoice_date" | ||||
|                 v-model="searchData.orderByField" | ||||
|                 :label="$t('recurring_invoices.next_invoice_date')" | ||||
|                 size="sm" | ||||
|                 name="filter" | ||||
|                 value="next_invoice_at" | ||||
|                 @update:modelValue="onSearched" | ||||
|               /> | ||||
|             </BaseInputGroup> | ||||
|           </BaseDropdownItem> | ||||
|  | ||||
|           <BaseDropdownItem class="flex px-1 py-2 cursor-pointer"> | ||||
|             <BaseInputGroup class="-mt-3 font-normal"> | ||||
|               <BaseRadio | ||||
|                 id="filter_start_date" | ||||
|                 v-model="searchData.orderByField" | ||||
|                 :label="$t('recurring_invoices.starts_at')" | ||||
|                 value="starts_at" | ||||
|                 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" class="h-5" /> | ||||
|           <BaseIcon v-else name="SortDescendingIcon" class="h-5" /> | ||||
|         </BaseButton> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div | ||||
|       v-if="recurringInvoiceStore && recurringInvoiceStore.recurringInvoices" | ||||
|       class=" | ||||
|         h-full | ||||
|         pb-32 | ||||
|         overflow-y-scroll | ||||
|         border-l border-gray-200 border-solid | ||||
|         base-scroll | ||||
|       " | ||||
|     > | ||||
|       <div | ||||
|         v-for="(invoice, index) in recurringInvoiceStore.recurringInvoices" | ||||
|         :key="index" | ||||
|       > | ||||
|         <router-link | ||||
|           v-if="invoice && !isLoading" | ||||
|           :id="'recurring-invoice-' + invoice.id" | ||||
|           :to="`/admin/recurring-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> | ||||
|             <BaseRecurringInvoiceStatusBadge | ||||
|               :status="invoice.status" | ||||
|               class="px-1 text-xs" | ||||
|             > | ||||
|               {{ invoice.status }} | ||||
|             </BaseRecurringInvoiceStatusBadge> | ||||
|           </div> | ||||
|  | ||||
|           <div class="flex-1 whitespace-nowrap right"> | ||||
|             <BaseFormatMoney | ||||
|               class=" | ||||
|                 block | ||||
|                 mb-2 | ||||
|                 text-xl | ||||
|                 not-italic | ||||
|                 font-semibold | ||||
|                 leading-8 | ||||
|                 text-right text-gray-900 | ||||
|               " | ||||
|               :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_starts_at }} | ||||
|             </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="!recurringInvoiceStore.recurringInvoices.length && !isLoading" | ||||
|         class="flex justify-center px-4 mt-5 text-sm text-gray-600" | ||||
|       > | ||||
|         {{ $t('invoices.no_matching_invoices') }} | ||||
|       </p> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
		Reference in New Issue
	
	Block a user