mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-10-29 12:41:10 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			536 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			536 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|   <BaseContentPlaceholders v-if="contentLoading">
 | |
|     <BaseContentPlaceholdersBox
 | |
|       :rounded="true"
 | |
|       class="w-full"
 | |
|       style="min-height: 170px"
 | |
|     />
 | |
|   </BaseContentPlaceholders>
 | |
| 
 | |
|   <div v-else class="max-h-[173px]">
 | |
|     <CustomerModal />
 | |
|     <!-- <SalesTax :type="type" /> -->
 | |
| 
 | |
|     <div
 | |
|       v-if="selectedCustomer"
 | |
|       class="
 | |
|         flex flex-col
 | |
|         p-4
 | |
|         bg-white
 | |
|         border border-gray-200 border-solid
 | |
|         min-h-[170px]
 | |
|         rounded-md
 | |
|       "
 | |
|       @click.stop
 | |
|     >
 | |
|       <div class="flex relative justify-between mb-2">
 | |
|         <BaseText
 | |
|           :text="selectedCustomer.name"
 | |
|           :length="30"
 | |
|           class="flex-1 text-base font-medium text-left text-gray-900"
 | |
|         />
 | |
|         <div class="flex">
 | |
|           <a
 | |
|             class="
 | |
|               relative
 | |
|               my-0
 | |
|               ml-6
 | |
|               text-sm
 | |
|               font-medium
 | |
|               cursor-pointer
 | |
|               text-primary-500
 | |
|               items-center
 | |
|               flex
 | |
|             "
 | |
|             @click.stop="editCustomer"
 | |
|           >
 | |
|             <BaseIcon name="PencilIcon" class="text-gray-500 h-4 w-4 mr-1" />
 | |
| 
 | |
|             {{ $t('general.edit') }}
 | |
|           </a>
 | |
|           <a
 | |
|             class="
 | |
|               relative
 | |
|               my-0
 | |
|               ml-6
 | |
|               text-sm
 | |
|               flex
 | |
|               items-center
 | |
|               font-medium
 | |
|               cursor-pointer
 | |
|               text-primary-500
 | |
|             "
 | |
|             @click="resetSelectedCustomer"
 | |
|           >
 | |
|             <BaseIcon name="XCircleIcon" class="text-gray-500 h-4 w-4 mr-1" />
 | |
|             {{ $t('general.deselect') }}
 | |
|           </a>
 | |
|         </div>
 | |
|       </div>
 | |
|       <div class="grid grid-cols-2 gap-8 mt-2">
 | |
|         <div v-if="selectedCustomer.billing" class="flex flex-col">
 | |
|           <label
 | |
|             class="
 | |
|               mb-1
 | |
|               text-sm
 | |
|               font-medium
 | |
|               text-left text-gray-400
 | |
|               uppercase
 | |
|               whitespace-nowrap
 | |
|             "
 | |
|           >
 | |
|             {{ $t('general.bill_to') }}
 | |
|           </label>
 | |
| 
 | |
|           <div
 | |
|             v-if="selectedCustomer.billing"
 | |
|             class="flex flex-col flex-1 p-0 text-left"
 | |
|           >
 | |
|             <label
 | |
|               v-if="selectedCustomer.billing.name"
 | |
|               class="relative w-11/12 text-sm truncate"
 | |
|             >
 | |
|               {{ selectedCustomer.billing.name }}
 | |
|             </label>
 | |
| 
 | |
|             <label class="relative w-11/12 text-sm truncate">
 | |
|               <span v-if="selectedCustomer.billing.city">
 | |
|                 {{ selectedCustomer.billing.city }}
 | |
|               </span>
 | |
|               <span
 | |
|                 v-if="
 | |
|                   selectedCustomer.billing.city &&
 | |
|                   selectedCustomer.billing.state
 | |
|                 "
 | |
|               >
 | |
|                 ,
 | |
|               </span>
 | |
|               <span v-if="selectedCustomer.billing.state">
 | |
|                 {{ selectedCustomer.billing.state }}
 | |
|               </span>
 | |
|             </label>
 | |
|             <label
 | |
|               v-if="selectedCustomer.billing.zip"
 | |
|               class="relative w-11/12 text-sm truncate"
 | |
|             >
 | |
|               {{ selectedCustomer.billing.zip }}
 | |
|             </label>
 | |
|           </div>
 | |
|         </div>
 | |
| 
 | |
|         <div v-if="selectedCustomer.shipping" class="flex flex-col">
 | |
|           <label
 | |
|             class="
 | |
|               mb-1
 | |
|               text-sm
 | |
|               font-medium
 | |
|               text-left text-gray-400
 | |
|               uppercase
 | |
|               whitespace-nowrap
 | |
|             "
 | |
|           >
 | |
|             {{ $t('general.ship_to') }}
 | |
|           </label>
 | |
| 
 | |
|           <div
 | |
|             v-if="selectedCustomer.shipping"
 | |
|             class="flex flex-col flex-1 p-0 text-left"
 | |
|           >
 | |
|             <label
 | |
|               v-if="selectedCustomer.shipping.name"
 | |
|               class="relative w-11/12 text-sm truncate"
 | |
|             >
 | |
|               {{ selectedCustomer.shipping.name }}
 | |
|             </label>
 | |
| 
 | |
|             <label class="relative w-11/12 text-sm truncate">
 | |
|               <span v-if="selectedCustomer.shipping.city">
 | |
|                 {{ selectedCustomer.shipping.city }}
 | |
|               </span>
 | |
|               <span
 | |
|                 v-if="
 | |
|                   selectedCustomer.shipping.city &&
 | |
|                   selectedCustomer.shipping.state
 | |
|                 "
 | |
|               >
 | |
|                 ,
 | |
|               </span>
 | |
|               <span v-if="selectedCustomer.shipping.state">
 | |
|                 {{ selectedCustomer.shipping.state }}
 | |
|               </span>
 | |
|             </label>
 | |
|             <label
 | |
|               v-if="selectedCustomer.shipping.zip"
 | |
|               class="relative w-11/12 text-sm truncate"
 | |
|             >
 | |
|               {{ selectedCustomer.shipping.zip }}
 | |
|             </label>
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>
 | |
| 
 | |
|     <Popover v-else v-slot="{ open }" class="relative flex flex-col rounded-md">
 | |
|       <PopoverButton
 | |
|         :class="{
 | |
|           'text-opacity-90': open,
 | |
|           'border border-solid border-red-500 focus:ring-red-500 rounded':
 | |
|             valid.$error,
 | |
|           'focus:ring-2 focus:ring-primary-400': !valid.$error,
 | |
|         }"
 | |
|         class="w-full outline-none rounded-md"
 | |
|       >
 | |
|         <div
 | |
|           class="
 | |
|             relative
 | |
|             flex
 | |
|             justify-center
 | |
|             px-0
 | |
|             p-0
 | |
|             py-16
 | |
|             bg-white
 | |
|             border border-gray-200 border-solid
 | |
|             rounded-md
 | |
|             min-h-[170px]
 | |
|           "
 | |
|         >
 | |
|           <BaseIcon
 | |
|             name="UserIcon"
 | |
|             class="
 | |
|               flex
 | |
|               justify-center
 | |
|               !w-10
 | |
|               !h-10
 | |
|               p-2
 | |
|               mr-5
 | |
|               text-sm text-white
 | |
|               bg-gray-200
 | |
|               rounded-full
 | |
|               font-base
 | |
|             "
 | |
|           />
 | |
| 
 | |
|           <div class="mt-1">
 | |
|             <label class="text-lg font-medium text-gray-900">
 | |
|               {{ $t('customers.new_customer') }}
 | |
|               <span class="text-red-500"> * </span>
 | |
|             </label>
 | |
| 
 | |
|             <p
 | |
|               v-if="valid.$error && valid.$errors[0].$message"
 | |
|               class="text-red-500 text-sm absolute right-3 bottom-3"
 | |
|             >
 | |
|               {{ $t('estimates.errors.required') }}
 | |
|             </p>
 | |
|           </div>
 | |
|         </div>
 | |
|       </PopoverButton>
 | |
| 
 | |
|       <!-- Customer Select Popup -->
 | |
|       <transition
 | |
|         enter-active-class="transition duration-200 ease-out"
 | |
|         enter-from-class="translate-y-1 opacity-0"
 | |
|         enter-to-class="translate-y-0 opacity-100"
 | |
|         leave-active-class="transition duration-150 ease-in"
 | |
|         leave-from-class="translate-y-0 opacity-100"
 | |
|         leave-to-class="translate-y-1 opacity-0"
 | |
|       >
 | |
|         <div v-if="open" class="absolute min-w-full z-10">
 | |
|           <PopoverPanel
 | |
|             v-slot="{ close }"
 | |
|             focus
 | |
|             static
 | |
|             class="
 | |
|               overflow-hidden
 | |
|               rounded-md
 | |
|               shadow-lg
 | |
|               ring-1 ring-black ring-opacity-5
 | |
|               bg-white
 | |
|             "
 | |
|           >
 | |
|             <div class="relative">
 | |
|               <BaseInput
 | |
|                 v-model="search"
 | |
|                 container-class="m-4"
 | |
|                 :placeholder="$t('general.search')"
 | |
|                 type="text"
 | |
|                 icon="search"
 | |
|                 @update:modelValue="(val) => debounceSearchCustomer(val)"
 | |
|               />
 | |
| 
 | |
|               <ul
 | |
|                 class="
 | |
|                   max-h-80
 | |
|                   flex flex-col
 | |
|                   overflow-auto
 | |
|                   list
 | |
|                   border-t border-gray-200
 | |
|                 "
 | |
|               >
 | |
|                 <li
 | |
|                   v-for="(customer, index) in customerStore.customers"
 | |
|                   :key="index"
 | |
|                   href="#"
 | |
|                   class="
 | |
|                     flex
 | |
|                     px-6
 | |
|                     py-2
 | |
|                     border-b border-gray-200 border-solid
 | |
|                     cursor-pointer
 | |
|                     hover:cursor-pointer hover:bg-gray-100
 | |
|                     focus:outline-none focus:bg-gray-100
 | |
|                     last:border-b-0
 | |
|                   "
 | |
|                   @click="selectNewCustomer(customer.id, close)"
 | |
|                 >
 | |
|                   <span
 | |
|                     class="
 | |
|                       flex
 | |
|                       items-center
 | |
|                       content-center
 | |
|                       justify-center
 | |
|                       w-10
 | |
|                       h-10
 | |
|                       mr-4
 | |
|                       text-xl
 | |
|                       font-semibold
 | |
|                       leading-9
 | |
|                       text-white
 | |
|                       bg-gray-300
 | |
|                       rounded-full
 | |
|                       avatar
 | |
|                     "
 | |
|                   >
 | |
|                     {{ initGenerator(customer.name) }}
 | |
|                   </span>
 | |
| 
 | |
|                   <div class="flex flex-col justify-center text-left">
 | |
|                     <BaseText
 | |
|                       v-if="customer.name"
 | |
|                       :text="customer.name"
 | |
|                       :length="30"
 | |
|                       class="
 | |
|                         m-0
 | |
|                         text-base
 | |
|                         font-normal
 | |
|                         leading-tight
 | |
|                         cursor-pointer
 | |
|                       "
 | |
|                     />
 | |
|                     <BaseText
 | |
|                       v-if="customer.contact_name"
 | |
|                       :text="customer.contact_name"
 | |
|                       :length="30"
 | |
|                       class="
 | |
|                         m-0
 | |
|                         text-sm
 | |
|                         font-medium
 | |
|                         text-gray-400
 | |
|                         cursor-pointer
 | |
|                       "
 | |
|                     />
 | |
|                   </div>
 | |
|                 </li>
 | |
|                 <div
 | |
|                   v-if="customerStore.customers.length === 0"
 | |
|                   class="flex justify-center p-5 text-gray-400"
 | |
|                 >
 | |
|                   <label class="text-base text-gray-500 cursor-pointer">
 | |
|                     {{ $t('customers.no_customers_found') }}
 | |
|                   </label>
 | |
|                 </div>
 | |
|               </ul>
 | |
|             </div>
 | |
| 
 | |
|             <button
 | |
|               v-if="userStore.hasAbilities(abilities.CREATE_CUSTOMER)"
 | |
|               type="button"
 | |
|               class="
 | |
|                 h-10
 | |
|                 flex
 | |
|                 items-center
 | |
|                 justify-center
 | |
|                 w-full
 | |
|                 px-2
 | |
|                 py-3
 | |
|                 bg-gray-200
 | |
|                 border-none
 | |
|                 outline-none
 | |
|                 focus:bg-gray-300
 | |
|               "
 | |
|               @click="openCustomerModal"
 | |
|             >
 | |
|               <BaseIcon name="UserAddIcon" class="text-primary-400" />
 | |
| 
 | |
|               <label
 | |
|                 class="
 | |
|                   m-0
 | |
|                   ml-3
 | |
|                   text-sm
 | |
|                   leading-none
 | |
|                   cursor-pointer
 | |
|                   font-base
 | |
|                   text-primary-400
 | |
|                 "
 | |
|               >
 | |
|                 {{ $t('customers.add_new_customer') }}
 | |
|               </label>
 | |
|             </button>
 | |
|           </PopoverPanel>
 | |
|         </div>
 | |
|       </transition>
 | |
|     </Popover>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <script setup>
 | |
| import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
 | |
| import { useEstimateStore } from '@/scripts/admin/stores/estimate'
 | |
| import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
 | |
| import { useRecurringInvoiceStore } from '@/scripts/admin/stores/recurring-invoice'
 | |
| import { useModalStore } from '@/scripts/stores/modal'
 | |
| import { useGlobalStore } from '@/scripts/admin/stores/global'
 | |
| import { useCustomerStore } from '@/scripts/admin/stores/customer'
 | |
| import { computed, ref } from 'vue'
 | |
| import { useI18n } from 'vue-i18n'
 | |
| import { useDebounceFn } from '@vueuse/core'
 | |
| import { useUserStore } from '@/scripts/admin/stores/user'
 | |
| import abilities from '@/scripts/admin/stub/abilities'
 | |
| import { useRoute } from 'vue-router'
 | |
| import CustomerModal from '@/scripts/admin/components/modal-components/CustomerModal.vue'
 | |
| 
 | |
| const props = defineProps({
 | |
|   valid: {
 | |
|     type: Object,
 | |
|     default: () => {},
 | |
|   },
 | |
|   customerId: {
 | |
|     type: Number,
 | |
|     default: null,
 | |
|   },
 | |
|   type: {
 | |
|     type: String,
 | |
|     default: null,
 | |
|   },
 | |
|   contentLoading: {
 | |
|     type: Boolean,
 | |
|     default: false,
 | |
|   },
 | |
| })
 | |
| 
 | |
| const modalStore = useModalStore()
 | |
| const estimateStore = useEstimateStore()
 | |
| const customerStore = useCustomerStore()
 | |
| const globalStore = useGlobalStore()
 | |
| const invoiceStore = useInvoiceStore()
 | |
| const recurringInvoiceStore = useRecurringInvoiceStore()
 | |
| const userStore = useUserStore()
 | |
| const routes = useRoute()
 | |
| const { t } = useI18n()
 | |
| const search = ref(null)
 | |
| const isSearchingCustomer = ref(false)
 | |
| 
 | |
| const selectedCustomer = computed(() => {
 | |
|   switch (props.type) {
 | |
|     case 'estimate':
 | |
|       return estimateStore.newEstimate.customer
 | |
|     case 'invoice':
 | |
|       return invoiceStore.newInvoice.customer
 | |
|     case 'recurring-invoice':
 | |
|       return recurringInvoiceStore.newRecurringInvoice.customer
 | |
|     default:
 | |
|       return ''
 | |
|   }
 | |
| })
 | |
| 
 | |
| function resetSelectedCustomer() {
 | |
|   if (props.type === 'estimate') {
 | |
|     estimateStore.resetSelectedCustomer()
 | |
|   } else if (props.type === 'invoice') {
 | |
|     invoiceStore.resetSelectedCustomer()
 | |
|   } else {
 | |
|     recurringInvoiceStore.resetSelectedCustomer()
 | |
|   }
 | |
| }
 | |
| 
 | |
| if (props.customerId && props.type === 'estimate') {
 | |
|   estimateStore.selectCustomer(props.customerId)
 | |
| } else if (props.customerId && props.type === 'invoice') {
 | |
|   invoiceStore.selectCustomer(props.customerId)
 | |
| } else {
 | |
|   if (props.customerId) recurringInvoiceStore.selectCustomer(props.customerId)
 | |
| }
 | |
| 
 | |
| async function editCustomer() {
 | |
|   await customerStore.fetchCustomer(selectedCustomer.value.id)
 | |
|   modalStore.openModal({
 | |
|     title: t('customers.edit_customer'),
 | |
|     componentName: 'CustomerModal',
 | |
|   })
 | |
| }
 | |
| 
 | |
| async function fetchInitialCustomers() {
 | |
|   await customerStore.fetchCustomers({
 | |
|     filter: {},
 | |
|     orderByField: '',
 | |
|     orderBy: '',
 | |
|     customer_id: props.customerId,
 | |
|   })
 | |
| }
 | |
| 
 | |
| const debounceSearchCustomer = useDebounceFn(() => {
 | |
|   isSearchingCustomer.value = true
 | |
| 
 | |
|   searchCustomer()
 | |
| }, 500)
 | |
| 
 | |
| async function searchCustomer() {
 | |
|   let data = {
 | |
|     display_name: search.value,
 | |
|     page: 1,
 | |
|   }
 | |
| 
 | |
|   await customerStore.fetchCustomers(data)
 | |
|   isSearchingCustomer.value = false
 | |
| }
 | |
| 
 | |
| function openCustomerModal() {
 | |
|   modalStore.openModal({
 | |
|     title: t('customers.add_customer'),
 | |
|     componentName: 'CustomerModal',
 | |
|     variant: 'md',
 | |
|   })
 | |
| }
 | |
| 
 | |
| function initGenerator(name) {
 | |
|   if (name) {
 | |
|     let nameSplit = name.split(' ')
 | |
|     let initials = nameSplit[0].charAt(0).toUpperCase()
 | |
|     return initials
 | |
|   }
 | |
| }
 | |
| 
 | |
| function selectNewCustomer(id, close) {
 | |
|   let params = {
 | |
|     userId: id,
 | |
|   }
 | |
|   if (routes.params.id) params.model_id = routes.params.id
 | |
| 
 | |
|   if (props.type === 'estimate') {
 | |
|     estimateStore.getNextNumber(params, true)
 | |
|     estimateStore.selectCustomer(id)
 | |
|   } else if (props.type === 'invoice') {
 | |
|     invoiceStore.getNextNumber(params, true)
 | |
|     invoiceStore.selectCustomer(id)
 | |
|   } else {
 | |
|     recurringInvoiceStore.selectCustomer(id)
 | |
|   }
 | |
|   close()
 | |
|   search.value = null
 | |
| }
 | |
| 
 | |
| globalStore.fetchCurrencies()
 | |
| globalStore.fetchCountries()
 | |
| fetchInitialCustomers()
 | |
| </script>
 |