mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-10-31 05:31:10 -04:00 
			
		
		
		
	build version 400
This commit is contained in:
		
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										211
									
								
								resources/assets/js/views/estimates/CustomerSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								resources/assets/js/views/estimates/CustomerSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,211 @@ | ||||
| <template> | ||||
|   <div class="col-span-5 pr-0"> | ||||
|     <div | ||||
|       v-if="selectedCustomer" | ||||
|       class="flex flex-col p-4 bg-white border border-gray-200 border-solid" | ||||
|       style="min-height: 170px" | ||||
|     > | ||||
|       <div class="relative flex justify-between mb-1"> | ||||
|         <label class="flex-1 font-medium">{{ selectedCustomer.name }}</label> | ||||
|         <a | ||||
|           class="relative my-0 ml-0 mr-6 text-sm font-medium cursor-pointer text-primary-500" | ||||
|           @click.prevent="editCustomer" | ||||
|         > | ||||
|           {{ $t('general.edit') }} | ||||
|         </a> | ||||
|         <a | ||||
|           class="relative my-0 ml-0 mr-6 text-sm font-medium cursor-pointer text-primary-500" | ||||
|           @click.prevent="resetSelectedCustomer" | ||||
|         > | ||||
|           {{ $t('general.deselect') }} | ||||
|         </a> | ||||
|       </div> | ||||
|  | ||||
|       <div class="grid grid-cols-2 gap-4 mt-1"> | ||||
|         <div v-if="selectedCustomer.billing_address"> | ||||
|           <div class="flex flex-col"> | ||||
|             <label | ||||
|               class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap" | ||||
|             > | ||||
|               {{ $t('general.bill_to') }} | ||||
|             </label> | ||||
|             <div class="flex flex-col flex-1 p-0"> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.billing_address.name" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.billing_address.name }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.billing_address.address_street_1" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.billing_address.address_street_1 }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.billing_address.address_street_2" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.billing_address.address_street_2 }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if=" | ||||
|                   selectedCustomer.billing_address.city && | ||||
|                   selectedCustomer.billing_address.state | ||||
|                 " | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.billing_address.city }}, | ||||
|                 {{ selectedCustomer.billing_address.state }} | ||||
|                 {{ selectedCustomer.billing_address.zip }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.billing_address.country" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.billing_address.country.name }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.billing_address.phone" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.billing_address.phone }} | ||||
|               </label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div v-if="selectedCustomer.shipping_address"> | ||||
|           <div class="flex flex-col"> | ||||
|             <label | ||||
|               class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap" | ||||
|             > | ||||
|               {{ $t('general.ship_to') }} | ||||
|             </label> | ||||
|             <div class="flex flex-col flex-1 p-0"> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.shipping_address.name" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.shipping_address.name }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.shipping_address.address_street_1" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.shipping_address.address_street_1 }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.shipping_address.address_street_2" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.shipping_address.address_street_2 }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if=" | ||||
|                   selectedCustomer.shipping_address.city && | ||||
|                   selectedCustomer.shipping_address | ||||
|                 " | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.shipping_address.city }}, | ||||
|                 {{ selectedCustomer.shipping_address.state }} | ||||
|                 {{ selectedCustomer.shipping_address.zip }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.shipping_address.country" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.shipping_address.country.name }} | ||||
|               </label> | ||||
|               <label | ||||
|                 v-if="selectedCustomer.shipping_address.phone" | ||||
|                 class="relative w-11/12 text-sm truncate" | ||||
|               > | ||||
|                 {{ selectedCustomer.shipping_address.phone }} | ||||
|               </label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div v-else> | ||||
|       <sw-popup | ||||
|         :class="[ | ||||
|           'p-0', | ||||
|           { | ||||
|             'border border-solid border-danger rounded': valid.$error, | ||||
|           }, | ||||
|         ]" | ||||
|       > | ||||
|         <div | ||||
|           slot="activator" | ||||
|           class="relative flex justify-center px-0 py-16 bg-white border border-gray-200 border-solid rounded" | ||||
|           style="min-height: 170px" | ||||
|         > | ||||
|           <user-icon | ||||
|             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"> | ||||
|               {{ $t('customers.new_customer') }} | ||||
|               <span class="text-danger"> * </span> | ||||
|             </label> | ||||
|             <p v-if="valid.$error && !valid.required" class="text-danger"> | ||||
|               {{ $t('estimates.errors.required') }} | ||||
|             </p> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <customer-select-popup :user-id="customerId" type="estimate" /> | ||||
|       </sw-popup> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapActions, mapGetters } from 'vuex' | ||||
| import { UserIcon } from '@vue-hero-icons/solid' | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     UserIcon, | ||||
|   }, | ||||
|   props: { | ||||
|     valid: { | ||||
|       type: Object, | ||||
|       default: () => {}, | ||||
|     }, | ||||
|     customerId: { | ||||
|       type: Number, | ||||
|       default: null, | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     ...mapGetters('estimate', ['getTemplateId', 'selectedCustomer']), | ||||
|   }, | ||||
|  | ||||
|   created() { | ||||
|     this.resetSelectedCustomer() | ||||
|     if (this.customerId) { | ||||
|       this.selectCustomer(this.customerId) | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     ...mapActions('estimate', ['resetSelectedCustomer', 'selectCustomer']), | ||||
|  | ||||
|     ...mapActions('modal', ['openModal']), | ||||
|  | ||||
|     editCustomer() { | ||||
|       this.openModal({ | ||||
|         title: this.$t('customers.edit_customer'), | ||||
|         componentName: 'CustomerModal', | ||||
|         id: this.selectedCustomer.id, | ||||
|         data: this.selectedCustomer, | ||||
|       }) | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
| @ -1,50 +1,49 @@ | ||||
| <template> | ||||
|   <div class="section mt-2"> | ||||
|     <label class="estimate-label"> | ||||
|   <div class="flex items-center justify-between w-full mt-2 text-sm"> | ||||
|     <label class="font-semibold leading-5 text-gray-500 uppercase"> | ||||
|       {{ tax.name }} ({{ tax.percent }}%) | ||||
|     </label> | ||||
|     <label class="estimate-amount"> | ||||
|     <label class="flex items-center justify-center text-lg text-black"> | ||||
|       <div v-html="$utils.formatMoney(tax.amount, currency)" /> | ||||
|  | ||||
|       <font-awesome-icon | ||||
|         class="ml-2" | ||||
|         icon="trash-alt" | ||||
|         @click="$emit('remove', index)" | ||||
|       /> | ||||
|       <trash-icon class="h-5 ml-2" @click="$emit('remove', index)" /> | ||||
|     </label> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { TrashIcon } from '@vue-hero-icons/solid' | ||||
| export default { | ||||
|   components: { | ||||
|     TrashIcon, | ||||
|   }, | ||||
|   props: { | ||||
|     index: { | ||||
|       type: Number, | ||||
|       required: true | ||||
|       required: true, | ||||
|     }, | ||||
|     tax: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|       required: true, | ||||
|     }, | ||||
|     taxes: { | ||||
|       type: Array, | ||||
|       required: true | ||||
|       required: true, | ||||
|     }, | ||||
|     total: { | ||||
|       type: Number, | ||||
|       default: 0 | ||||
|       default: 0, | ||||
|     }, | ||||
|     totalTax: { | ||||
|       type: Number, | ||||
|       default: 0 | ||||
|       default: 0, | ||||
|     }, | ||||
|     currency: { | ||||
|       type: [Object, String], | ||||
|       required: true | ||||
|     } | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     taxAmount () { | ||||
|     taxAmount() { | ||||
|       if (this.tax.compound_tax && this.total) { | ||||
|         return ((this.total + this.totalTax) * this.tax.percent) / 100 | ||||
|       } | ||||
| @ -54,30 +53,26 @@ export default { | ||||
|       } | ||||
|  | ||||
|       return 0 | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     total: { | ||||
|       handler: 'updateTax' | ||||
|       handler: 'updateTax', | ||||
|     }, | ||||
|     totalTax: { | ||||
|       handler: 'updateTax' | ||||
|     } | ||||
|       handler: 'updateTax', | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     updateTax () { | ||||
|     updateTax() { | ||||
|       this.$emit('update', { | ||||
|         'index': this.index, | ||||
|         'item': { | ||||
|         index: this.index, | ||||
|         item: { | ||||
|           ...this.tax, | ||||
|           amount: this.taxAmount | ||||
|         } | ||||
|           amount: this.taxAmount, | ||||
|         }, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|  | ||||
| </style> | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,23 +1,22 @@ | ||||
| <template> | ||||
|   <tr class="item-row estimate-item-row"> | ||||
|     <td colspan="5"> | ||||
|       <table class="full-width"> | ||||
|   <tr class="box-border bg-white border border-gray-200 border-solid rounded-b"> | ||||
|     <td colspan="5" class="p-0 text-left align-top"> | ||||
|       <table class="w-full"> | ||||
|         <colgroup> | ||||
|           <col style="width: 40%;"> | ||||
|           <col style="width: 10%;"> | ||||
|           <col style="width: 15%;"> | ||||
|           <col v-if="discountPerItem === 'YES'" style="width: 15%;"> | ||||
|           <col style="width: 15%;"> | ||||
|           <col style="width: 40%" /> | ||||
|           <col style="width: 10%" /> | ||||
|           <col style="width: 15%" /> | ||||
|           <col v-if="discountPerItem === 'YES'" style="width: 15%" /> | ||||
|           <col style="width: 15%" /> | ||||
|         </colgroup> | ||||
|         <tbody> | ||||
|           <tr> | ||||
|             <td class=""> | ||||
|               <div class="item-select-wrapper"> | ||||
|                 <div class="sort-icon-wrapper handle"> | ||||
|                   <font-awesome-icon | ||||
|                     class="sort-icon" | ||||
|                     icon="grip-vertical" | ||||
|                   /> | ||||
|             <td class="px-5 py-4 text-left align-top"> | ||||
|               <div class="flex justify-start"> | ||||
|                 <div | ||||
|                   class="flex items-center justify-center w-12 h-5 mt-2 text-gray-400 cursor-move handle" | ||||
|                 > | ||||
|                   <drag-icon /> | ||||
|                 </div> | ||||
|                 <item-select | ||||
|                   ref="itemSelect" | ||||
| @ -34,87 +33,94 @@ | ||||
|                 /> | ||||
|               </div> | ||||
|             </td> | ||||
|             <td class="text-right"> | ||||
|               <base-input | ||||
|             <td class="px-5 py-4 text-right align-top"> | ||||
|               <sw-input | ||||
|                 v-model="item.quantity" | ||||
|                 :invalid="$v.item.quantity.$error" | ||||
|                 :is-input-group="!!item.unit_name" | ||||
|                 :input-group-text="item.unit_name" | ||||
|                 type="text" | ||||
|                 small | ||||
|                 @keyup="updateItem" | ||||
|                 @input="$v.item.quantity.$touch()" | ||||
|               /> | ||||
|               <div v-if="$v.item.quantity.$error"> | ||||
|                 <span v-if="!$v.item.quantity.maxLength" class="text-danger">{{ $t('validation.quantity_maxlength') }}</span> | ||||
|                 <span v-if="!$v.item.quantity.maxLength" class="text-danger"> | ||||
|                   {{ $t('validation.quantity_maxlength') }} | ||||
|                 </span> | ||||
|               </div> | ||||
|             </td> | ||||
|             <td class="text-left"> | ||||
|               <div class="d-flex flex-column"> | ||||
|                 <div class="flex-fillbd-highlight"> | ||||
|                   <div class="base-input"> | ||||
|                     <money | ||||
|             <td class="px-5 py-4 text-left align-top"> | ||||
|               <div class="flex flex-col"> | ||||
|                 <div class="flex-auto flex-fill bd-highlight"> | ||||
|                   <div class="relative w-full"> | ||||
|                     <sw-money | ||||
|                       v-model="price" | ||||
|                       v-bind="customerCurrency" | ||||
|                       class="input-field" | ||||
|                       :currency="customerCurrency" | ||||
|                       :invalid="$v.item.price.$error" | ||||
|                       @input="$v.item.price.$touch()" | ||||
|                     /> | ||||
|                   </div> | ||||
|                   <div v-if="$v.item.price.$error"> | ||||
|                     <span v-if="!$v.item.price.maxLength" class="text-danger">{{ $t('validation.price_maxlength') }}</span> | ||||
|                     <span v-if="!$v.item.price.maxLength" class="text-danger"> | ||||
|                       {{ $t('validation.price_maxlength') }} | ||||
|                     </span> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </td> | ||||
|             <td v-if="discountPerItem === 'YES'" class=""> | ||||
|               <div class="d-flex flex-column bd-highlight"> | ||||
|                 <div | ||||
|                   class="btn-group flex-fill bd-highlight" | ||||
|                   role="group" | ||||
|                 > | ||||
|                   <base-input | ||||
|             <td | ||||
|               v-if="discountPerItem === 'YES'" | ||||
|               class="px-5 py-4 text-left align-top" | ||||
|             > | ||||
|               <div class="flex flex-col"> | ||||
|                 <div class="flex flex-auto" role="group"> | ||||
|                   <sw-input | ||||
|                     v-model="discount" | ||||
|                     :invalid="$v.item.discount_val.$error" | ||||
|                     input-class="item-discount" | ||||
|                     class="border-r-0 rounded-tr-none rounded-br-none" | ||||
|                     @input="$v.item.discount_val.$touch()" | ||||
|                   /> | ||||
|                   <v-dropdown :show-arrow="false" theme-light> | ||||
|                     <button | ||||
|  | ||||
|                   <sw-dropdown> | ||||
|                     <sw-button | ||||
|                       slot="activator" | ||||
|                       type="button" | ||||
|                       class="btn item-dropdown dropdown-toggle" | ||||
|                       data-toggle="dropdown" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-expanded="false" | ||||
|                       style="height: 43px; padding: 6px" | ||||
|                       variant="white" | ||||
|                     > | ||||
|                       {{ item.discount_type == 'fixed' ? currency.symbol : '%' }} | ||||
|                     </button> | ||||
|                     <v-dropdown-item> | ||||
|                       <a class="dropdown-item" href="#" @click.prevent="selectFixed" > | ||||
|                         {{ $t('general.fixed') }} | ||||
|                       </a> | ||||
|                     </v-dropdown-item> | ||||
|                     <v-dropdown-item> | ||||
|                       <a class="dropdown-item" href="#" @click.prevent="selectPercentage"> | ||||
|                         {{ $t('general.percentage') }} | ||||
|                       </a> | ||||
|                     </v-dropdown-item> | ||||
|                   </v-dropdown> | ||||
|                       <span class="flex"> | ||||
|                         {{ | ||||
|                           item.discount_type == 'fixed' ? currency.symbol : '%' | ||||
|                         }} | ||||
|                         <chevron-down-icon class="h-5" /> | ||||
|                       </span> | ||||
|                     </sw-button> | ||||
|  | ||||
|                     <sw-dropdown-item @click="selectFixed"> | ||||
|                       {{ $t('general.fixed') }} | ||||
|                     </sw-dropdown-item> | ||||
|  | ||||
|                     <sw-dropdown-item @click="selectPercentage"> | ||||
|                       {{ $t('general.percentage') }} | ||||
|                     </sw-dropdown-item> | ||||
|                   </sw-dropdown> | ||||
|                 </div> | ||||
|                 <!-- <div v-if="$v.item.discount.$error"> discount error </div> --> | ||||
|               </div> | ||||
|             </td> | ||||
|             <td class="text-right"> | ||||
|               <div class="item-amount"> | ||||
|             <td class="px-5 py-4 text-right align-top"> | ||||
|               <div class="flex items-center justify-end text-sm"> | ||||
|                 <span> | ||||
|                   <div v-html="$utils.formatMoney(total, currency)" /> | ||||
|                 </span> | ||||
|  | ||||
|                 <div class="remove-icon-wrapper"> | ||||
|                   <font-awesome-icon | ||||
|                 <div | ||||
|                   class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer" | ||||
|                 > | ||||
|                   <trash-icon | ||||
|                     v-if="isShowRemoveItemIcon" | ||||
|                     class="remove-icon" | ||||
|                     icon="trash-alt" | ||||
|                     class="h-5 text-gray-700" | ||||
|                     @click="removeItem" | ||||
|                   /> | ||||
|                 </div> | ||||
| @ -122,8 +128,8 @@ | ||||
|             </td> | ||||
|           </tr> | ||||
|           <tr v-if="taxPerItem === 'YES'" class="tax-tr"> | ||||
|             <td /> | ||||
|             <td colspan="4"> | ||||
|             <td class="px-5 py-4 text-left align-top" /> | ||||
|             <td colspan="4" class="px-5 py-4 text-left align-top"> | ||||
|               <tax | ||||
|                 v-for="(tax, index) in item.taxes" | ||||
|                 :key="tax.id" | ||||
| @ -146,96 +152,100 @@ | ||||
| </template> | ||||
| <script> | ||||
| import Guid from 'guid' | ||||
| import { validationMixin } from 'vuelidate' | ||||
| import { mapGetters } from 'vuex' | ||||
| import TaxStub from '../../stub/tax' | ||||
| import EstimateStub from '../../stub/estimate' | ||||
| import ItemSelect from './ItemSelect' | ||||
| import Tax from './Tax' | ||||
| const { required, minValue, between, maxLength } = require('vuelidate/lib/validators') | ||||
| import { TrashIcon, ChevronDownIcon } from '@vue-hero-icons/solid' | ||||
| import DragIcon from '@/components/icon/DragIcon' | ||||
|  | ||||
| const { | ||||
|   required, | ||||
|   minValue, | ||||
|   between, | ||||
|   maxLength, | ||||
| } = require('vuelidate/lib/validators') | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     Tax, | ||||
|     ItemSelect | ||||
|     ItemSelect, | ||||
|     TrashIcon, | ||||
|     ChevronDownIcon, | ||||
|     DragIcon, | ||||
|   }, | ||||
|   mixins: [validationMixin], | ||||
|  | ||||
|   props: { | ||||
|     itemData: { | ||||
|       type: Object, | ||||
|       default: null | ||||
|       default: null, | ||||
|     }, | ||||
|     index: { | ||||
|       type: Number, | ||||
|       default: null | ||||
|       default: null, | ||||
|     }, | ||||
|     type: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|       default: '', | ||||
|     }, | ||||
|     currency: { | ||||
|       type: [Object, String], | ||||
|       required: true | ||||
|       required: true, | ||||
|     }, | ||||
|     taxPerItem: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|       default: '', | ||||
|     }, | ||||
|     discountPerItem: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|       default: '', | ||||
|     }, | ||||
|     estimateItems: { | ||||
|       type: Array, | ||||
|       required: true | ||||
|     } | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   data () { | ||||
|   data() { | ||||
|     return { | ||||
|       isClosePopup: false, | ||||
|       itemSelect: null, | ||||
|       item: {...this.itemData}, | ||||
|       item: { ...this.itemData }, | ||||
|       maxDiscount: 0, | ||||
|       money: { | ||||
|         decimal: '.', | ||||
|         thousands: ',', | ||||
|         prefix: '$ ', | ||||
|         precision: 2, | ||||
|         masked: false | ||||
|         masked: false, | ||||
|       }, | ||||
|       isSelected: false | ||||
|       isSelected: false, | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters('item', [ | ||||
|       'items' | ||||
|     ]), | ||||
|     ...mapGetters('modal', [ | ||||
|       'modalActive' | ||||
|     ]), | ||||
|     ...mapGetters('currency', [ | ||||
|       'defaultCurrencyForInput' | ||||
|     ]), | ||||
|     customerCurrency () { | ||||
|     ...mapGetters('item', ['items']), | ||||
|     ...mapGetters('modal', ['modalActive']), | ||||
|     ...mapGetters('company', ['defaultCurrencyForInput']), | ||||
|     customerCurrency() { | ||||
|       if (this.currency) { | ||||
|         return { | ||||
|           decimal: this.currency.decimal_separator, | ||||
|           thousands: this.currency.thousand_separator, | ||||
|           prefix: this.currency.symbol + ' ', | ||||
|           precision: this.currency.precision, | ||||
|           masked: false | ||||
|           masked: false, | ||||
|         } | ||||
|       } else { | ||||
|         return this.defaultCurrencyForInput | ||||
|       } | ||||
|     }, | ||||
|     isShowRemoveItemIcon () { | ||||
|     isShowRemoveItemIcon() { | ||||
|       if (this.estimateItems.length == 1) { | ||||
|         return false | ||||
|       } | ||||
|       return true | ||||
|     }, | ||||
|     subtotal () { | ||||
|     subtotal() { | ||||
|       return this.item.price * this.item.quantity | ||||
|     }, | ||||
|     discount: { | ||||
| @ -250,12 +260,12 @@ export default { | ||||
|         } | ||||
|  | ||||
|         this.item.discount = newValue | ||||
|       } | ||||
|       }, | ||||
|     }, | ||||
|     total () { | ||||
|     total() { | ||||
|       return this.subtotal - this.item.discount_val | ||||
|     }, | ||||
|     totalSimpleTax () { | ||||
|     totalSimpleTax() { | ||||
|       return window._.sumBy(this.item.taxes, function (tax) { | ||||
|         if (!tax.compound_tax) { | ||||
|           return tax.amount | ||||
| @ -264,7 +274,7 @@ export default { | ||||
|         return 0 | ||||
|       }) | ||||
|     }, | ||||
|     totalCompoundTax () { | ||||
|     totalCompoundTax() { | ||||
|       return window._.sumBy(this.item.taxes, function (tax) { | ||||
|         if (tax.compound_tax) { | ||||
|           return tax.amount | ||||
| @ -273,7 +283,7 @@ export default { | ||||
|         return 0 | ||||
|       }) | ||||
|     }, | ||||
|     totalTax () { | ||||
|     totalTax() { | ||||
|       return this.totalSimpleTax + this.totalCompoundTax | ||||
|     }, | ||||
|     price: { | ||||
| @ -291,51 +301,51 @@ export default { | ||||
|         } else { | ||||
|           this.item.price = newValue | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     item: { | ||||
|       handler: 'updateItem', | ||||
|       deep: true | ||||
|       deep: true, | ||||
|     }, | ||||
|     subtotal (newValue) { | ||||
|     subtotal(newValue) { | ||||
|       if (this.item.discount_type === 'percentage') { | ||||
|         this.item.discount_val = (this.item.discount * newValue) / 100 | ||||
|       } | ||||
|     }, | ||||
|     modalActive (val) { | ||||
|     modalActive(val) { | ||||
|       if (!val) { | ||||
|         this.isSelected = false | ||||
|       } | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   validations () { | ||||
|   validations() { | ||||
|     return { | ||||
|       item: { | ||||
|         name: { | ||||
|           required | ||||
|           required, | ||||
|         }, | ||||
|         quantity: { | ||||
|           required, | ||||
|           minValue: minValue(1), | ||||
|           maxLength: maxLength(20) | ||||
|           minValue: minValue(0), | ||||
|           maxLength: maxLength(20), | ||||
|         }, | ||||
|         price: { | ||||
|           required, | ||||
|           minValue: minValue(1), | ||||
|           maxLength: maxLength(20) | ||||
|           maxLength: maxLength(20), | ||||
|         }, | ||||
|         discount_val: { | ||||
|           between: between(0, this.maxDiscount) | ||||
|           between: between(0, this.maxDiscount), | ||||
|         }, | ||||
|         description: { | ||||
|           maxLength: maxLength(255) | ||||
|         } | ||||
|       } | ||||
|           maxLength: maxLength(255), | ||||
|         }, | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
|   created () { | ||||
|   created() { | ||||
|     window.hub.$on('checkItems', this.validateItem) | ||||
|     window.hub.$on('newItem', (val) => { | ||||
|       if (this.taxPerItem === 'YES') { | ||||
| @ -346,36 +356,43 @@ export default { | ||||
|       } | ||||
|     }) | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$v.item.$reset() | ||||
|   }, | ||||
|   methods: { | ||||
|     updateTax (data) { | ||||
|     updateTax(data) { | ||||
|       this.$set(this.item.taxes, data.index, data.item) | ||||
|  | ||||
|       let lastTax = this.item.taxes[this.item.taxes.length - 1] | ||||
|  | ||||
|       if (lastTax.tax_type_id !== 0) { | ||||
|         this.item.taxes.push({...TaxStub, id: Guid.raw()}) | ||||
|         this.item.taxes.push({ ...TaxStub, id: Guid.raw() }) | ||||
|       } | ||||
|  | ||||
|       this.updateItem() | ||||
|     }, | ||||
|     removeTax (index) { | ||||
|     removeTax(index) { | ||||
|       this.item.taxes.splice(index, 1) | ||||
|  | ||||
|       this.updateItem() | ||||
|     }, | ||||
|     taxWithPercentage ({ name, percent }) { | ||||
|     taxWithPercentage({ name, percent }) { | ||||
|       return `${name} (${percent}%)` | ||||
|     }, | ||||
|     searchVal (val) { | ||||
|     searchVal(val) { | ||||
|       this.item.name = val | ||||
|     }, | ||||
|     deselectItem () { | ||||
|       this.item = {...EstimateStub, id: this.item.id, taxes: [{...TaxStub, id: Guid.raw()}]} | ||||
|     deselectItem() { | ||||
|       this.item = { | ||||
|         ...EstimateStub, | ||||
|         id: this.item.id, | ||||
|         taxes: [{ ...TaxStub, id: Guid.raw() }], | ||||
|       } | ||||
|       this.$nextTick(() => { | ||||
|         this.$refs.itemSelect.$refs.baseSelect.$refs.search.focus() | ||||
|       }) | ||||
|     }, | ||||
|     onSelectItem (item) { | ||||
|     onSelectItem(item) { | ||||
|       this.item.name = item.name | ||||
|       this.item.price = item.price | ||||
|       this.item.item_id = item.id | ||||
| @ -383,16 +400,13 @@ export default { | ||||
|       this.item.unit_name = item.unit_name | ||||
|       if (this.taxPerItem === 'YES' && item.taxes) { | ||||
|         let index = 0 | ||||
|         item.taxes.forEach(tax => { | ||||
|           this.updateTax({index, item: { ...tax }}) | ||||
|         item.taxes.forEach((tax) => { | ||||
|           this.updateTax({ index, item: { ...tax } }) | ||||
|           index++ | ||||
|         }) | ||||
|       } | ||||
|       // if (this.item.taxes.length) { | ||||
|       //   this.item.taxes = {...item.taxes} | ||||
|       // } | ||||
|     }, | ||||
|     selectFixed () { | ||||
|     selectFixed() { | ||||
|       if (this.item.discount_type === 'fixed') { | ||||
|         return | ||||
|       } | ||||
| @ -400,7 +414,7 @@ export default { | ||||
|       this.item.discount_val = this.item.discount * 100 | ||||
|       this.item.discount_type = 'fixed' | ||||
|     }, | ||||
|     selectPercentage () { | ||||
|     selectPercentage() { | ||||
|       if (this.item.discount_type === 'percentage') { | ||||
|         return | ||||
|       } | ||||
| @ -409,24 +423,24 @@ export default { | ||||
|  | ||||
|       this.item.discount_type = 'percentage' | ||||
|     }, | ||||
|     updateItem () { | ||||
|     updateItem() { | ||||
|       this.$emit('update', { | ||||
|         'index': this.index, | ||||
|         'item': { | ||||
|         index: this.index, | ||||
|         item: { | ||||
|           ...this.item, | ||||
|           total: this.total, | ||||
|           totalSimpleTax: this.totalSimpleTax, | ||||
|           totalCompoundTax: this.totalCompoundTax, | ||||
|           totalTax: this.totalTax, | ||||
|           tax: this.totalTax, | ||||
|           taxes: [...this.item.taxes] | ||||
|         } | ||||
|           taxes: [...this.item.taxes], | ||||
|         }, | ||||
|       }) | ||||
|     }, | ||||
|     removeItem () { | ||||
|     removeItem() { | ||||
|       this.$emit('remove', this.index) | ||||
|     }, | ||||
|     validateItem () { | ||||
|     validateItem() { | ||||
|       this.$v.item.$touch() | ||||
|  | ||||
|       if (this.item !== null) { | ||||
| @ -434,7 +448,7 @@ export default { | ||||
|       } else { | ||||
|         this.$emit('itemValidate', this.index, false) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
| @ -1,13 +1,20 @@ | ||||
| <template> | ||||
|   <div class="item-selector"> | ||||
|     <div v-if="item.item_id" class="selected-item"> | ||||
|   <div class="flex-1 text-sm"> | ||||
|     <div | ||||
|       v-if="item.item_id" | ||||
|       class="relative flex items-center h-10 pl-2 bg-gray-200 border border-gray-200 border-solid rounded" | ||||
|     > | ||||
|       {{ item.name }} | ||||
|  | ||||
|       <span class="deselect-icon" @click="deselectItem"> | ||||
|         <font-awesome-icon icon="times-circle" /> | ||||
|       <span | ||||
|         class="absolute text-gray-400 cursor-pointer" | ||||
|         style="top: 8px; right: 10px" | ||||
|         @click="deselectItem" | ||||
|       > | ||||
|         <x-circle-icon class="h-5" /> | ||||
|       </span> | ||||
|     </div> | ||||
|     <base-select | ||||
|     <sw-select | ||||
|       v-else | ||||
|       ref="baseSelect" | ||||
|       v-model="itemSelect" | ||||
| @ -24,25 +31,34 @@ | ||||
|       @select="onSelect" | ||||
|     > | ||||
|       <div slot="afterList"> | ||||
|         <button type="button" class="list-add-button" @click="openItemModal"> | ||||
|           <font-awesome-icon class="icon" icon="cart-plus" /> | ||||
|           <label>{{ $t('general.add_new_item') }}</label> | ||||
|         <button | ||||
|           type="button" | ||||
|           class="flex items-center justify-center w-full p-3 bg-gray-200 border-none outline-none" | ||||
|           @click="openItemModal" | ||||
|         > | ||||
|           <shopping-cart-icon class="h-5 text-primary-400" /> | ||||
|           <label class="ml-2 text-sm leading-none text-primary-400">{{ | ||||
|             $t('general.add_new_item') | ||||
|           }}</label> | ||||
|         </button> | ||||
|       </div> | ||||
|     </base-select> | ||||
|     <div class="item-description"> | ||||
|       <base-text-area | ||||
|     </sw-select> | ||||
|     <div class="w-full pt-1 text-xs text-light"> | ||||
|       <sw-textarea | ||||
|         v-autoresize | ||||
|         v-model.trim="item.description" | ||||
|         :invalid-description="invalidDescription" | ||||
|         :placeholder="$t('estimates.item.type_item_description')" | ||||
|         type="text" | ||||
|         rows="1" | ||||
|         class="description-input" | ||||
|         variant="inv-desc" | ||||
|         class="w-full text-gray-600 border-none resize-none" | ||||
|         @input="$emit('onDesriptionInput')" | ||||
|       /> | ||||
|       <div v-if="invalidDescription"> | ||||
|         <span class="text-danger">{{ $tc('validation.description_maxlength') }}</span> | ||||
|         <span class="text-danger">{{ | ||||
|           $tc('validation.description_maxlength') | ||||
|         }}</span> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -50,78 +66,79 @@ | ||||
|  | ||||
| <script> | ||||
| import { mapActions, mapGetters } from 'vuex' | ||||
| import { validationMixin } from 'vuelidate' | ||||
| import { XCircleIcon, ShoppingCartIcon } from '@vue-hero-icons/solid' | ||||
| const { maxLength } = require('vuelidate/lib/validators') | ||||
|  | ||||
| export default { | ||||
|   mixins: [validationMixin], | ||||
|   components: { | ||||
|     XCircleIcon, | ||||
|     ShoppingCartIcon, | ||||
|   }, | ||||
|   props: { | ||||
|     item: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|       required: true, | ||||
|     }, | ||||
|     invalid: { | ||||
|       type: Boolean, | ||||
|       required: false, | ||||
|       default: false | ||||
|       default: false, | ||||
|     }, | ||||
|     invalidDescription: { | ||||
|       type: Boolean, | ||||
|       required: false, | ||||
|       default: false | ||||
|       default: false, | ||||
|     }, | ||||
|     taxPerItem: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|       default: '', | ||||
|     }, | ||||
|     taxes: { | ||||
|       type: Array, | ||||
|       default: null | ||||
|     } | ||||
|       default: null, | ||||
|     }, | ||||
|   }, | ||||
|   data () { | ||||
|   data() { | ||||
|     return { | ||||
|       itemSelect: null, | ||||
|       loading: false | ||||
|       loading: false, | ||||
|     } | ||||
|   }, | ||||
|   validations () { | ||||
|   validations() { | ||||
|     return { | ||||
|       item: { | ||||
|         description: { | ||||
|           maxLength: maxLength(255) | ||||
|         } | ||||
|       } | ||||
|           maxLength: maxLength(255), | ||||
|         }, | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters('item', [ | ||||
|       'items' | ||||
|     ]) | ||||
|     ...mapGetters('item', ['items']), | ||||
|   }, | ||||
|   watch: { | ||||
|     invalidDescription (newValue) { | ||||
|     invalidDescription(newValue) { | ||||
|       console.log(newValue) | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions('modal', [ | ||||
|       'openModal' | ||||
|     ]), | ||||
|     ...mapActions('item', [ | ||||
|       'fetchItems' | ||||
|     ]), | ||||
|     async searchItems (search) { | ||||
|     ...mapActions('modal', ['openModal']), | ||||
|     ...mapActions('item', ['fetchItems']), | ||||
|     async searchItems(search) { | ||||
|       let data = { | ||||
|         search, | ||||
|         filter: { | ||||
|           name: search, | ||||
|           unit: '', | ||||
|           price: '' | ||||
|           price: '', | ||||
|         }, | ||||
|         orderByField: '', | ||||
|         orderBy: '', | ||||
|         page: 1 | ||||
|         page: 1, | ||||
|       } | ||||
|  | ||||
|       if (this.item) { | ||||
|         data.item_id = this.item.item_id | ||||
|       } | ||||
|  | ||||
|       this.loading = true | ||||
| @ -130,27 +147,27 @@ export default { | ||||
|  | ||||
|       this.loading = false | ||||
|     }, | ||||
|     onTextChange (val) { | ||||
|     onTextChange(val) { | ||||
|       this.searchItems(val) | ||||
|  | ||||
|       this.$emit('search', val) | ||||
|     }, | ||||
|     openItemModal () { | ||||
|     openItemModal() { | ||||
|       this.$emit('onSelectItem') | ||||
|       this.openModal({ | ||||
|         'title': this.$t('items.add_item'), | ||||
|         'componentName': 'ItemModal', | ||||
|         'data': {taxPerItem: this.taxPerItem, taxes: this.taxes} | ||||
|         title: this.$t('items.add_item'), | ||||
|         componentName: 'ItemModal', | ||||
|         data: { taxPerItem: this.taxPerItem, taxes: this.taxes }, | ||||
|       }) | ||||
|     }, | ||||
|     onSelect(val) { | ||||
|       this.$emit('select', val) | ||||
|       this.fetchItems() | ||||
|     },     | ||||
|     deselectItem () { | ||||
|     }, | ||||
|     deselectItem() { | ||||
|       this.itemSelect = null | ||||
|       this.$emit('deselect') | ||||
|     } | ||||
|   } | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| <template> | ||||
|   <div class="tax-row"> | ||||
|     <div class="d-flex align-items-center tax-select"> | ||||
|       <label class="bd-highlight pr-2 mb-0" align="right"> | ||||
|   <div class="flex items-center justify-between mb-3"> | ||||
|     <div class="flex items-center text-base" style="flex: 4"> | ||||
|       <label class="pr-2 mb-0" align="right"> | ||||
|         {{ $t('estimates.tax') }} | ||||
|       </label> | ||||
|       <base-select | ||||
|       <sw-select | ||||
|         v-model="selectedTax" | ||||
|         :options="filteredTypes" | ||||
|         :allow-empty="false" | ||||
| @ -16,20 +16,27 @@ | ||||
|         @select="(val) => onSelectTax(val)" | ||||
|       > | ||||
|         <div slot="afterList"> | ||||
|           <button type="button" class="list-add-button" @click="openTaxModal"> | ||||
|             <font-awesome-icon class="icon" icon="check-circle" /> | ||||
|             <label>{{ $t('estimates.add_new_tax') }}</label> | ||||
|           <button | ||||
|             type="button" | ||||
|             class="flex items-center justify-center w-full px-2 py-2 bg-gray-200 border-none outline-none" | ||||
|             @click="openTaxModal" | ||||
|           > | ||||
|             <check-circle-icon class="h-5 text-primary-400" /> | ||||
|             <label class="ml-2 text-sm leading-none text-primary-400">{{ | ||||
|               $t('estimates.add_new_tax') | ||||
|             }}</label> | ||||
|           </button> | ||||
|         </div> | ||||
|       </base-select> <br> | ||||
|       </sw-select> | ||||
|       <br /> | ||||
|     </div> | ||||
|     <div class="text-right tax-amount"> | ||||
|     <div class="text-sm text-right" style="flex: 3"> | ||||
|       <div v-html="$utils.formatMoney(taxAmount, currency)" /> | ||||
|     </div> | ||||
|     <div class="remove-icon-wrapper"> | ||||
|       <font-awesome-icon | ||||
|     <div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer"> | ||||
|       <trash-icon | ||||
|         v-if="taxes.length && index !== taxes.length - 1" | ||||
|         class="remove-icon" | ||||
|         class="h-5 text-gray-700" | ||||
|         icon="trash-alt" | ||||
|         @click="removeTax" | ||||
|       /> | ||||
| @ -39,49 +46,52 @@ | ||||
|  | ||||
| <script> | ||||
| import { mapActions, mapGetters } from 'vuex' | ||||
| import { CheckCircleIcon, TrashIcon } from '@vue-hero-icons/solid' | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     CheckCircleIcon, | ||||
|     TrashIcon, | ||||
|   }, | ||||
|   props: { | ||||
|     index: { | ||||
|       type: Number, | ||||
|       required: true | ||||
|       required: true, | ||||
|     }, | ||||
|     taxData: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|       required: true, | ||||
|     }, | ||||
|     taxes: { | ||||
|       type: Array, | ||||
|       default: [] | ||||
|       default: [], | ||||
|     }, | ||||
|     total: { | ||||
|       type: Number, | ||||
|       default: 0 | ||||
|       default: 0, | ||||
|     }, | ||||
|     totalTax: { | ||||
|       type: Number, | ||||
|       default: 0 | ||||
|       default: 0, | ||||
|     }, | ||||
|     currency: { | ||||
|       type: [Object, String], | ||||
|       required: true | ||||
|     } | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   data () { | ||||
|   data() { | ||||
|     return { | ||||
|       tax: {...this.taxData}, | ||||
|       selectedTax: null | ||||
|       tax: { ...this.taxData }, | ||||
|       selectedTax: null, | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters('taxType', [ | ||||
|       'taxTypes' | ||||
|     ]), | ||||
|     filteredTypes () { | ||||
|       const clonedTypes = this.taxTypes.map(a => ({...a})) | ||||
|     ...mapGetters('taxType', ['taxTypes']), | ||||
|     filteredTypes() { | ||||
|       const clonedTypes = this.taxTypes.map((a) => ({ ...a })) | ||||
|  | ||||
|       return clonedTypes.map((taxType) => { | ||||
|         let found = this.taxes.find(tax => tax.tax_type_id === taxType.id) | ||||
|         let found = this.taxes.find((tax) => tax.tax_type_id === taxType.id) | ||||
|  | ||||
|         if (found) { | ||||
|           taxType.$isDisabled = true | ||||
| @ -92,7 +102,7 @@ export default { | ||||
|         return taxType | ||||
|       }) | ||||
|     }, | ||||
|     taxAmount () { | ||||
|     taxAmount() { | ||||
|       if (this.tax.compound_tax && this.total) { | ||||
|         return ((this.total + this.totalTax) * this.tax.percent) / 100 | ||||
|       } | ||||
| @ -102,19 +112,21 @@ export default { | ||||
|       } | ||||
|  | ||||
|       return 0 | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     total: { | ||||
|       handler: 'updateTax' | ||||
|       handler: 'updateTax', | ||||
|     }, | ||||
|     totalTax: { | ||||
|       handler: 'updateTax' | ||||
|     } | ||||
|       handler: 'updateTax', | ||||
|     }, | ||||
|   }, | ||||
|   created () { | ||||
|   created() { | ||||
|     if (this.taxData.tax_type_id > 0) { | ||||
|       this.selectedTax = this.taxTypes.find(_type => _type.id === this.taxData.tax_type_id) | ||||
|       this.selectedTax = this.taxTypes.find( | ||||
|         (_type) => _type.id === this.taxData.tax_type_id | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     window.hub.$on('newTax', (val) => { | ||||
| @ -127,13 +139,11 @@ export default { | ||||
|     this.updateTax() | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions('modal', [ | ||||
|       'openModal' | ||||
|     ]), | ||||
|     customLabel ({ name, percent }) { | ||||
|     ...mapActions('modal', ['openModal']), | ||||
|     customLabel({ name, percent }) { | ||||
|       return `${name} - ${percent}%` | ||||
|     }, | ||||
|     onSelectTax (val) { | ||||
|     onSelectTax(val) { | ||||
|       this.tax.percent = val.percent | ||||
|       this.tax.tax_type_id = val.id | ||||
|       this.tax.compound_tax = val.compound_tax | ||||
| @ -141,28 +151,28 @@ export default { | ||||
|  | ||||
|       this.updateTax() | ||||
|     }, | ||||
|     updateTax () { | ||||
|     updateTax() { | ||||
|       if (this.tax.tax_type_id === 0) { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       this.$emit('update', { | ||||
|         'index': this.index, | ||||
|         'item': { | ||||
|         index: this.index, | ||||
|         item: { | ||||
|           ...this.tax, | ||||
|           amount: this.taxAmount | ||||
|         } | ||||
|           amount: this.taxAmount, | ||||
|         }, | ||||
|       }) | ||||
|     }, | ||||
|     removeTax () { | ||||
|     removeTax() { | ||||
|       this.$emit('remove', this.index, this.tax) | ||||
|     }, | ||||
|     openTaxModal () { | ||||
|     openTaxModal() { | ||||
|       this.openModal({ | ||||
|         'title': this.$t('settings.tax_types.add_tax'), | ||||
|         'componentName': 'TaxTypeModal' | ||||
|         title: this.$t('settings.tax_types.add_tax'), | ||||
|         componentName: 'TaxTypeModal', | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
| @ -1,202 +1,244 @@ | ||||
| <template> | ||||
|   <div v-if="estimate" class="main-content estimate-view-page"> | ||||
|     <div class="page-header"> | ||||
|       <h3 class="page-title">{{ estimate.estimate_number }}</h3> | ||||
|       <div class="page-actions row"> | ||||
|         <div class="col-xs-2 mr-3"> | ||||
|           <base-button | ||||
|   <base-page v-if="estimate" class="xl:pl-96"> | ||||
|     <sw-page-header :title="pageTitle"> | ||||
|       <template slot="actions"> | ||||
|         <div class="mr-3 text-sm"> | ||||
|           <sw-button | ||||
|             v-if="estimate.status === 'DRAFT'" | ||||
|             :loading="isMarkAsSent" | ||||
|             :disabled="isMarkAsSent" | ||||
|             :outline="true" | ||||
|             color="theme" | ||||
|             variant="primary-outline" | ||||
|             @click="onMarkAsSent" | ||||
|           > | ||||
|             {{ $t('estimates.mark_as_sent') }} | ||||
|           </base-button> | ||||
|           </sw-button> | ||||
|         </div> | ||||
|         <div class="col-xs-2"> | ||||
|           <base-button | ||||
|             v-if="estimate.status === 'DRAFT'" | ||||
|             :loading="isSendingEmail" | ||||
|             :disabled="isSendingEmail" | ||||
|             color="theme" | ||||
|             @click="onSendEstimate" | ||||
|           > | ||||
|             {{ $t('estimates.send_estimate') }} | ||||
|           </base-button> | ||||
|         </div> | ||||
|         <v-dropdown | ||||
|           :close-on-select="true" | ||||
|           align="left" | ||||
|           class="filter-container" | ||||
|         <sw-button | ||||
|           v-if="estimate.status === 'DRAFT'" | ||||
|           :disabled="isSendingEmail" | ||||
|           variant="primary" | ||||
|           class="text-sm" | ||||
|           @click="onSendEstimate" | ||||
|         > | ||||
|           <a slot="activator" href="#"> | ||||
|             <base-button color="theme"> | ||||
|               <font-awesome-icon icon="ellipsis-h" /> | ||||
|             </base-button> | ||||
|           </a> | ||||
|           <v-dropdown-item> | ||||
|             <div class="dropdown-item" @click="copyPdfUrl()"> | ||||
|               <font-awesome-icon | ||||
|                 :icon="['fas', 'link']" | ||||
|                 class="dropdown-item-icon" | ||||
|               /> | ||||
|               {{ $t('general.copy_pdf_url') }} | ||||
|             </div> | ||||
|             <router-link | ||||
|               :to="{ path: `/admin/estimates/${$route.params.id}/edit` }" | ||||
|               class="dropdown-item" | ||||
|             > | ||||
|               <font-awesome-icon | ||||
|                 :icon="['fas', 'pencil-alt']" | ||||
|                 class="dropdown-item-icon" | ||||
|               /> | ||||
|               {{ $t('general.edit') }} | ||||
|             </router-link> | ||||
|             <div | ||||
|               class="dropdown-item" | ||||
|               @click="removeEstimate($route.params.id)" | ||||
|             > | ||||
|               <font-awesome-icon | ||||
|                 :icon="['fas', 'trash']" | ||||
|                 class="dropdown-item-icon" | ||||
|               /> | ||||
|               {{ $t('general.delete') }} | ||||
|             </div> | ||||
|           </v-dropdown-item> | ||||
|         </v-dropdown> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="estimate-sidebar"> | ||||
|       <base-loader v-if="isSearching" /> | ||||
|       <div v-else class="side-header"> | ||||
|         <base-input | ||||
|           {{ $t('estimates.send_estimate') }} | ||||
|         </sw-button> | ||||
|         <sw-dropdown class="ml-3"> | ||||
|           <sw-button slot="activator" variant="primary"> | ||||
|             <dots-horizontal-icon class="h-5" /> | ||||
|           </sw-button> | ||||
|  | ||||
|           <sw-dropdown-item @click="copyPdfUrl"> | ||||
|             <link-icon class="h-5 mr-3 text-primary-800" /> | ||||
|             {{ $t('general.copy_pdf_url') }} | ||||
|           </sw-dropdown-item> | ||||
|  | ||||
|           <sw-dropdown-item | ||||
|             tag-name="router-link" | ||||
|             :to="`/admin/estimates/${$route.params.id}/edit`" | ||||
|           > | ||||
|             <pencil-icon class="h-5 mr-3 text-primary-800" /> | ||||
|             {{ $t('general.edit') }} | ||||
|           </sw-dropdown-item> | ||||
|  | ||||
|           <sw-dropdown-item @click="removeEstimate($route.params.id)"> | ||||
|             <trash-icon class="h-5 mr-3 text-primary-800" /> | ||||
|             {{ $t('general.delete') }} | ||||
|           </sw-dropdown-item> | ||||
|         </sw-dropdown> | ||||
|       </template> | ||||
|     </sw-page-header> | ||||
|  | ||||
|     <!-- 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" | ||||
|       > | ||||
|         <sw-input | ||||
|           v-model="searchData.searchText" | ||||
|           :placeholder="$t('general.search')" | ||||
|           input-class="inv-search" | ||||
|           icon="search" | ||||
|           class="mb-6" | ||||
|           type="text" | ||||
|           align-icon="right" | ||||
|           variant="gray" | ||||
|           @input="onSearched()" | ||||
|         /> | ||||
|         <div class="btn-group ml-3" role="group" aria-label="First group"> | ||||
|           <v-dropdown | ||||
|             :close-on-select="false" | ||||
|             align="left" | ||||
|             class="filter-container" | ||||
|           > | ||||
|             <a slot="activator" href="#"> | ||||
|               <base-button | ||||
|                 class="inv-button inv-filter-fields-btn" | ||||
|                 color="default" | ||||
|                 size="medium" | ||||
|               > | ||||
|                 <font-awesome-icon icon="filter" /> | ||||
|               </base-button> | ||||
|             </a> | ||||
|             <div class="filter-title"> | ||||
|         > | ||||
|           <search-icon slot="rightIcon" class="h-5" /> | ||||
|         </sw-input> | ||||
|  | ||||
|         <div class="flex mb-6 ml-3" role="group" aria-label="First group"> | ||||
|           <sw-dropdown class="ml-3" position="bottom-start"> | ||||
|             <sw-button slot="activator" size="md" variant="gray-light"> | ||||
|               <filter-icon class="h-5" /> | ||||
|             </sw-button> | ||||
|  | ||||
|             <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> | ||||
|             <div class="filter-items"> | ||||
|               <input | ||||
|                 id="filter_estimate_date" | ||||
|                 v-model="searchData.orderByField" | ||||
|                 type="radio" | ||||
|                 name="filter" | ||||
|                 class="inv-radio" | ||||
|                 value="estimate_date" | ||||
|                 @change="onSearched" | ||||
|               /> | ||||
|               <label class="inv-label" for="filter_estimate_date">{{ | ||||
|                 $t('reports.estimates.estimate_date') | ||||
|               }}</label> | ||||
|             </div> | ||||
|             <div class="filter-items"> | ||||
|               <input | ||||
|                 id="filter_due_date" | ||||
|                 v-model="searchData.orderByField" | ||||
|                 type="radio" | ||||
|                 name="filter" | ||||
|                 class="inv-radio" | ||||
|                 value="expiry_date" | ||||
|                 @change="onSearched" | ||||
|               /> | ||||
|               <label class="inv-label" for="filter_due_date">{{ | ||||
|                 $t('estimates.due_date') | ||||
|               }}</label> | ||||
|             </div> | ||||
|             <div class="filter-items"> | ||||
|               <input | ||||
|                 id="filter_estimate_number" | ||||
|                 v-model="searchData.orderByField" | ||||
|                 type="radio" | ||||
|                 name="filter" | ||||
|                 class="inv-radio" | ||||
|                 value="estimate_number" | ||||
|                 @change="onSearched" | ||||
|               /> | ||||
|               <label class="inv-label" for="filter_estimate_number">{{ | ||||
|                 $t('estimates.estimate_number') | ||||
|               }}</label> | ||||
|             </div> | ||||
|           </v-dropdown> | ||||
|           <base-button | ||||
|  | ||||
|             <sw-dropdown-item class="flex px-1 py-2 cursor-pointer"> | ||||
|               <sw-input-group class="-mt-3 font-normal"> | ||||
|                 <sw-radio | ||||
|                   id="filter_estimate_date" | ||||
|                   v-model="searchData.orderByField" | ||||
|                   :label="$t('reports.estimates.estimate_date')" | ||||
|                   size="sm" | ||||
|                   name="filter" | ||||
|                   value="estimate_date" | ||||
|                   @change="onSearched" | ||||
|                 /> | ||||
|               </sw-input-group> | ||||
|             </sw-dropdown-item> | ||||
|  | ||||
|             <sw-dropdown-item class="flex px-1 py-2 cursor-pointer"> | ||||
|               <sw-input-group class="-mt-3 font-normal"> | ||||
|                 <sw-radio | ||||
|                   id="filter_due_date" | ||||
|                   v-model="searchData.orderByField" | ||||
|                   value="expiry_date" | ||||
|                   :label="$t('estimates.due_date')" | ||||
|                   size="sm" | ||||
|                   name="filter" | ||||
|                   @change="onSearched" | ||||
|                 /> | ||||
|               </sw-input-group> | ||||
|             </sw-dropdown-item> | ||||
|  | ||||
|             <sw-dropdown-item class="flex px-1 py-2 cursor-pointer"> | ||||
|               <sw-input-group class="-mt-3 font-normal"> | ||||
|                 <sw-radio | ||||
|                   id="filter_estimate_number" | ||||
|                   v-model="searchData.orderByField" | ||||
|                   value="estimate_number" | ||||
|                   :label="$t('estimates.estimate_number')" | ||||
|                   size="sm" | ||||
|                   name="filter" | ||||
|                   @change="onSearched" | ||||
|                 /> | ||||
|               </sw-input-group> | ||||
|             </sw-dropdown-item> | ||||
|           </sw-dropdown> | ||||
|  | ||||
|           <sw-button | ||||
|             class="ml-1" | ||||
|             v-tooltip.top-center="{ content: getOrderName }" | ||||
|             class="inv-button inv-filter-sorting-btn" | ||||
|             color="default" | ||||
|             size="medium" | ||||
|             size="md" | ||||
|             variant="gray-light" | ||||
|             @click="sortData" | ||||
|           > | ||||
|             <font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" /> | ||||
|             <font-awesome-icon v-else icon="sort-amount-down" /> | ||||
|           </base-button> | ||||
|             <sort-ascending-icon v-if="getOrderBy" class="h-5" /> | ||||
|             <sort-descending-icon v-else class="h-5" /> | ||||
|           </sw-button> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="side-content"> | ||||
|  | ||||
|       <base-loader v-if="isSearching" :show-bg-overlay="true" /> | ||||
|  | ||||
|       <div | ||||
|         v-else | ||||
|         class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sw-scroll" | ||||
|       > | ||||
|         <router-link | ||||
|           v-for="(estimate, index) in estimates" | ||||
|           :to="`/admin/estimates/${estimate.id}/view`" | ||||
|           :id="'estimate-' + estimate.id" | ||||
|           :key="index" | ||||
|           class="side-estimate" | ||||
|           :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="left"> | ||||
|             <div class="inv-name">{{ estimate.user.name }}</div> | ||||
|             <div class="inv-number">{{ estimate.estimate_number }}</div> | ||||
|           <div class="flex-2"> | ||||
|             <div | ||||
|               :class="'est-status-' + estimate.status.toLowerCase()" | ||||
|               class="inv-status" | ||||
|               class="pr-2 mb-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate" | ||||
|             > | ||||
|               {{ estimate.user.name }} | ||||
|             </div> | ||||
|  | ||||
|             <div | ||||
|               class="mt-1 mb-2 text-xs not-italic font-medium leading-5 text-gray-600" | ||||
|             > | ||||
|               {{ estimate.estimate_number }} | ||||
|             </div> | ||||
|  | ||||
|             <sw-badge | ||||
|               class="px-1 text-xs" | ||||
|               :bg-color="$utils.getBadgeStatusColor(estimate.status).bgColor" | ||||
|               :color="$utils.getBadgeStatusColor(estimate.status).color" | ||||
|             > | ||||
|               {{ estimate.status }} | ||||
|             </div> | ||||
|             </sw-badge> | ||||
|           </div> | ||||
|           <div class="right"> | ||||
|  | ||||
|           <div class="flex-1 whitespace-no-wrap right"> | ||||
|             <div | ||||
|               class="inv-amount" | ||||
|               class="mb-2 text-xl not-italic font-semibold leading-8 text-right text-gray-900" | ||||
|               v-html=" | ||||
|                 $utils.formatMoney(estimate.total, estimate.user.currency) | ||||
|               " | ||||
|             /> | ||||
|             <div class="inv-date">{{ estimate.formattedEstimateDate }}</div> | ||||
|  | ||||
|             <div | ||||
|               class="text-sm not-italic font-normal leading-5 text-right text-gray-600 est-date" | ||||
|             > | ||||
|               {{ estimate.formattedEstimateDate }} | ||||
|             </div> | ||||
|           </div> | ||||
|         </router-link> | ||||
|         <p v-if="!estimates.length" class="no-result"> | ||||
|  | ||||
|         <p | ||||
|           v-if="!estimates.length" | ||||
|           class="flex justify-center px-4 mt-5 text-sm text-gray-600" | ||||
|         > | ||||
|           {{ $t('estimates.no_matching_estimates') }} | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="estimate-view-page-container"> | ||||
|       <iframe :src="`${shareableLink}`" class="frame-style" /> | ||||
|  | ||||
|     <div | ||||
|       class="flex flex-col min-h-0 mt-8 overflow-hidden sw-scroll" | ||||
|       style="height: 75vh" | ||||
|     > | ||||
|       <iframe | ||||
|         :src="`${shareableLink}`" | ||||
|         class="flex-1 border border-gray-400 border-solid rounded-md frame-style" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
|   </base-page> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapActions } from 'vuex' | ||||
| import { | ||||
|   DotsHorizontalIcon, | ||||
|   FilterIcon, | ||||
|   SortAscendingIcon, | ||||
|   SortDescendingIcon, | ||||
|   SearchIcon, | ||||
|   LinkIcon, | ||||
|   TrashIcon, | ||||
|   PencilIcon, | ||||
| } from '@vue-hero-icons/solid' | ||||
| const _ = require('lodash') | ||||
|  | ||||
| export default { | ||||
|   data () { | ||||
|   components: { | ||||
|     DotsHorizontalIcon, | ||||
|     FilterIcon, | ||||
|     SortAscendingIcon, | ||||
|     SortDescendingIcon, | ||||
|     SearchIcon, | ||||
|     LinkIcon, | ||||
|     TrashIcon, | ||||
|     PencilIcon, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       id: null, | ||||
|       count: null, | ||||
| @ -206,38 +248,50 @@ export default { | ||||
|       searchData: { | ||||
|         orderBy: null, | ||||
|         orderByField: null, | ||||
|         searchText: null | ||||
|         searchText: null, | ||||
|       }, | ||||
|       status: ['DRAFT', 'SENT', 'VIEWED', 'EXPIRED', 'ACCEPTED', 'REJECTED'], | ||||
|       isMarkAsSent: false, | ||||
|       isSendingEmail: false, | ||||
|       isRequestOnGoing: false, | ||||
|       isSearching: false | ||||
|       isSearching: false, | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     getOrderBy () { | ||||
|       if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) { | ||||
|     pageTitle() { | ||||
|       return this.estimate.estimate_number | ||||
|     }, | ||||
|     getOrderBy() { | ||||
|       if ( | ||||
|         this.searchData.orderBy === 'asc' || | ||||
|         this.searchData.orderBy == null | ||||
|       ) { | ||||
|         return true | ||||
|       } | ||||
|       return false | ||||
|     }, | ||||
|     getOrderName () { | ||||
|     getOrderName() { | ||||
|       if (this.getOrderBy) { | ||||
|         return this.$t('general.ascending') | ||||
|       } | ||||
|       return this.$t('general.descending') | ||||
|     }, | ||||
|     shareableLink () { | ||||
|     shareableLink() { | ||||
|       return `/estimates/pdf/${this.estimate.unique_hash}` | ||||
|     } | ||||
|     }, | ||||
|     getCurrentEstimateId() { | ||||
|       if (this.estimate && this.estimate.id) { | ||||
|         return this.estimate.id | ||||
|       } | ||||
|       return null | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     $route (to, from) { | ||||
|     $route(to, from) { | ||||
|       this.loadEstimate() | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   created () { | ||||
|   created() { | ||||
|     this.loadEstimates() | ||||
|     this.loadEstimate() | ||||
|     this.onSearched = _.debounce(this.onSearched, 500) | ||||
| @ -251,32 +305,67 @@ export default { | ||||
|       'sendEmail', | ||||
|       'deleteEstimate', | ||||
|       'selectEstimate', | ||||
|       'fetchViewEstimate' | ||||
|       'fetchViewEstimate', | ||||
|     ]), | ||||
|     async loadEstimates () { | ||||
|       let response = await this.fetchEstimates() | ||||
|  | ||||
|     ...mapActions('modal', ['openModal']), | ||||
|  | ||||
|     hasActiveUrl(id) { | ||||
|       return this.$route.params.id == id | ||||
|     }, | ||||
|  | ||||
|     async loadEstimates() { | ||||
|       let response = await this.fetchEstimates({ limit: 'all' }) | ||||
|       if (response.data) { | ||||
|         this.estimates = response.data.estimates.data | ||||
|       } | ||||
|       setTimeout(() => { | ||||
|         this.scrollToEstimate() | ||||
|       }, 500) | ||||
|     }, | ||||
|     async loadEstimate () { | ||||
|     scrollToEstimate() { | ||||
|       const el = document.getElementById(`estimate-${this.$route.params.id}`) | ||||
|  | ||||
|       if (el) { | ||||
|         el.scrollIntoView({ behavior: 'smooth' }) | ||||
|         el.classList.add('shake') | ||||
|       } | ||||
|     }, | ||||
|     async loadEstimate() { | ||||
|       let response = await this.fetchViewEstimate(this.$route.params.id) | ||||
|  | ||||
|       if (response.data) { | ||||
|         this.estimate = response.data.estimate | ||||
|         this.estimate = { ...response.data.estimate } | ||||
|       } | ||||
|     }, | ||||
|     async onSearched () { | ||||
|     copyPdfUrl() { | ||||
|       let pdfUrl = `${window.location.origin}/estimates/pdf/${this.estimate.unique_hash}` | ||||
|  | ||||
|       let response = this.$utils.copyTextToClipboard(pdfUrl) | ||||
|  | ||||
|       window.toastr['success'](this.$tc('general.copied_pdf_url_clipboard')) | ||||
|     }, | ||||
|     async onSearched() { | ||||
|       let data = '' | ||||
|       if (this.searchData.searchText !== '' && this.searchData.searchText !== null && this.searchData.searchText !== undefined) { | ||||
|       if ( | ||||
|         this.searchData.searchText !== '' && | ||||
|         this.searchData.searchText !== null && | ||||
|         this.searchData.searchText !== undefined | ||||
|       ) { | ||||
|         data += `search=${this.searchData.searchText}&` | ||||
|       } | ||||
|  | ||||
|       if (this.searchData.orderBy !== null && this.searchData.orderBy !== undefined) { | ||||
|       if ( | ||||
|         this.searchData.orderBy !== null && | ||||
|         this.searchData.orderBy !== undefined | ||||
|       ) { | ||||
|         data += `orderBy=${this.searchData.orderBy}&` | ||||
|       } | ||||
|  | ||||
|       if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) { | ||||
|       if ( | ||||
|         this.searchData.orderByField !== null && | ||||
|         this.searchData.orderByField !== undefined | ||||
|       ) { | ||||
|         data += `orderByField=${this.searchData.orderByField}` | ||||
|       } | ||||
|       this.isSearching = true | ||||
| @ -286,7 +375,7 @@ export default { | ||||
|         this.estimates = response.data.estimates.data | ||||
|       } | ||||
|     }, | ||||
|     sortData () { | ||||
|     sortData() { | ||||
|       if (this.searchData.orderBy === 'asc') { | ||||
|         this.searchData.orderBy = 'desc' | ||||
|         this.onSearched() | ||||
| @ -296,74 +385,68 @@ export default { | ||||
|       this.onSearched() | ||||
|       return true | ||||
|     }, | ||||
|     async onMarkAsSent () { | ||||
|       window.swal({ | ||||
|         title: this.$t('general.are_you_sure'), | ||||
|         text: this.$t('estimates.confirm_mark_as_sent'), | ||||
|         icon: '/assets/icon/check-circle-solid.svg', | ||||
|         buttons: true, | ||||
|         dangerMode: true | ||||
|       }).then(async (value) => { | ||||
|         if (value) { | ||||
|           this.isMarkAsSent = true | ||||
|           let response = await this.markAsSent({id: this.estimate.id}) | ||||
|           this.isMarkAsSent = false | ||||
|           if (response.data) { | ||||
|             window.toastr['success'](this.$tc('estimates.mark_as_sent_successfully')) | ||||
|     async onMarkAsSent() { | ||||
|       window | ||||
|         .swal({ | ||||
|           title: this.$t('general.are_you_sure'), | ||||
|           text: this.$t('estimates.confirm_mark_as_sent'), | ||||
|           icon: '/assets/icon/check-circle-solid.svg', | ||||
|           buttons: true, | ||||
|           dangerMode: true, | ||||
|         }) | ||||
|         .then(async (value) => { | ||||
|           if (value) { | ||||
|             this.isMarkAsSent = true | ||||
|             let response = await this.markAsSent({ | ||||
|               id: this.estimate.id, | ||||
|               status: 'SENT', | ||||
|             }) | ||||
|             this.isMarkAsSent = false | ||||
|             if (response.data) { | ||||
|               this.estimate.status = 'SENT' | ||||
|               window.toastr['success']( | ||||
|                 this.$tc('estimates.mark_as_sent_successfully') | ||||
|               ) | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         }) | ||||
|     }, | ||||
|     async onSendEstimate(id) { | ||||
|       this.openModal({ | ||||
|         title: this.$t('estimates.send_estimate'), | ||||
|         componentName: 'SendEstimateModal', | ||||
|         id: this.estimate.id, | ||||
|         data: this.estimate, | ||||
|       }) | ||||
|     }, | ||||
|     async onSendEstimate (id) { | ||||
|       window.swal({ | ||||
|         title: this.$t('general.are_you_sure'), | ||||
|         text: this.$t('estimates.confirm_send_estimate'), | ||||
|         icon: '/assets/icon/paper-plane-solid.svg', | ||||
|         buttons: true, | ||||
|         dangerMode: true | ||||
|       }).then(async (value) => { | ||||
|         if (value) { | ||||
|           this.isSendingEmail = true | ||||
|           let response = await this.sendEmail({id: this.estimate.id}) | ||||
|           this.isSendingEmail = false | ||||
|           if (response.data.success) { | ||||
|             window.toastr['success'](this.$tc('estimates.send_estimate_successfully')) | ||||
|             return true | ||||
|           } | ||||
|           if (response.data.error === 'user_email_does_not_exist') { | ||||
|             window.toastr['error'](this.$tc('estimates.user_email_does_not_exist')) | ||||
|             return true | ||||
|           } | ||||
|           window.toastr['error'](this.$tc('estimates.something_went_wrong')) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     copyPdfUrl () { | ||||
|     copyPdfUrl() { | ||||
|       let pdfUrl = `${window.location.origin}/estimates/pdf/${this.estimate.unique_hash}` | ||||
|  | ||||
|       let response = this.$utils.copyTextToClipboard(pdfUrl) | ||||
|  | ||||
|       window.toastr['success'](this.$tc('Copied PDF url to clipboard!')) | ||||
|       window.toastr['success'](this.$tc('general.copied_pdf_url_clipboard')) | ||||
|     }, | ||||
|     async removeEstimate (id) { | ||||
|       window.swal({ | ||||
|         title: 'Deleted', | ||||
|         text: 'you will not be able to recover this estimate!', | ||||
|         icon: '/assets/icon/trash-solid.svg', | ||||
|         buttons: true, | ||||
|         dangerMode: true | ||||
|       }).then(async (value) => { | ||||
|         if (value) { | ||||
|           let request = await this.deleteEstimate(id) | ||||
|           if (request.data.success) { | ||||
|             window.toastr['success'](this.$tc('estimates.deleted_message', 1)) | ||||
|             this.$router.push('/admin/estimates') | ||||
|           } else if (request.data.error) { | ||||
|             window.toastr['error'](request.data.message) | ||||
|     async removeEstimate(id) { | ||||
|       window | ||||
|         .swal({ | ||||
|           title: this.$t('general.are_you_sure'), | ||||
|           text: 'you will not be able to recover this estimate!', | ||||
|           icon: '/assets/icon/trash-solid.svg', | ||||
|           buttons: true, | ||||
|           dangerMode: true, | ||||
|         }) | ||||
|         .then(async (value) => { | ||||
|           if (value) { | ||||
|             let request = await this.deleteEstimate({ ids: [id] }) | ||||
|             if (request.data.success) { | ||||
|               window.toastr['success'](this.$tc('estimates.deleted_message', 1)) | ||||
|               this.$router.push('/admin/estimates') | ||||
|             } else if (request.data.error) { | ||||
|               window.toastr['error'](request.data.message) | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|         }) | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user