mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-10-31 05:31:10 -04:00 
			
		
		
		
	init crater
This commit is contained in:
		
							
								
								
									
										698
									
								
								resources/assets/js/views/estimates/Create.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										698
									
								
								resources/assets/js/views/estimates/Create.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,698 @@ | ||||
| <template> | ||||
|   <div class="estimate-create-page main-content"> | ||||
|     <form v-if="!initLoading" action="" @submit.prevent="submitEstimateData"> | ||||
|       <div class="page-header"> | ||||
|         <h3 v-if="$route.name === 'estimates.edit'" class="page-title">{{ $t('estimates.edit_estimate') }}</h3> | ||||
|         <h3 v-else class="page-title">{{ $t('estimates.new_estimate') }}</h3> | ||||
|         <ol class="breadcrumb"> | ||||
|           <li class="breadcrumb-item"><router-link slot="item-title" to="/admin/dashboard">{{ $t('general.home') }}</router-link></li> | ||||
|           <li class="breadcrumb-item"><router-link slot="item-title" to="/admin/estimates">{{ $tc('estimates.estimate', 2) }}</router-link></li> | ||||
|           <li v-if="$route.name === 'estimates.edit'" class="breadcrumb-item">{{ $t('estimates.edit_estimate') }}</li> | ||||
|           <li v-else class="breadcrumb-item">{{ $t('estimates.new_estimate') }}</li> | ||||
|         </ol> | ||||
|         <div class="page-actions row"> | ||||
|           <a v-if="$route.name === 'estimates.edit'" :href="`/estimates/pdf/${newEstimate.unique_hash}`" target="_blank" class="mr-3 base-button btn btn-outline-primary default-size" outline color="theme"> | ||||
|             {{ $t('general.view_pdf') }} | ||||
|           </a> | ||||
|           <base-button | ||||
|             :loading="isLoading" | ||||
|             :disabled="isLoading" | ||||
|             icon="save" | ||||
|             color="theme" | ||||
|             type="submit"> | ||||
|             {{ $t('estimates.save_estimate') }} | ||||
|           </base-button> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="row estimate-input-group"> | ||||
|         <div class="col-md-5"> | ||||
|           <div | ||||
|             v-if="selectedCustomer" | ||||
|             class="show-customer" | ||||
|           > | ||||
|             <div class="row px-2 mt-1"> | ||||
|               <div class="col col-6"> | ||||
|                 <div v-if="selectedCustomer.billing_address != null" class="row address-menu"> | ||||
|                   <label class="col-sm-4 px-2 title">{{ $t('general.bill_to') }}</label> | ||||
|                   <div class="col-sm p-0 px-2 content"> | ||||
|                     <label v-if="selectedCustomer.billing_address.name"> | ||||
|                       {{ selectedCustomer.billing_address.name }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.billing_address.address_street_1"> | ||||
|                       {{ selectedCustomer.billing_address.address_street_1 }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.billing_address.address_street_2"> | ||||
|                       {{ selectedCustomer.billing_address.address_street_2 }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.billing_address.city && selectedCustomer.billing_address.state"> | ||||
|                       {{ selectedCustomer.billing_address.city.name }}, {{ selectedCustomer.billing_address.state.name }} {{ selectedCustomer.billing_address.zip }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.billing_address.country"> | ||||
|                       {{ selectedCustomer.billing_address.country.name }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.billing_address.phone"> | ||||
|                       {{ selectedCustomer.billing_address.phone }} | ||||
|                     </label> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div class="col col-6"> | ||||
|                 <div v-if="selectedCustomer.shipping_address != null" class="row address-menu"> | ||||
|                   <label class="col-sm-4 px-2 title">{{ $t('general.ship_to') }}</label> | ||||
|                   <div class="col-sm p-0 px-2 content"> | ||||
|                     <label v-if="selectedCustomer.shipping_address.name"> | ||||
|                       {{ selectedCustomer.shipping_address.name }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.shipping_address.address_street_1"> | ||||
|                       {{ selectedCustomer.shipping_address.address_street_1 }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.shipping_address.address_street_2"> | ||||
|                       {{ selectedCustomer.shipping_address.address_street_2 }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.shipping_address.city && selectedCustomer.shipping_address"> | ||||
|                       {{ selectedCustomer.shipping_address.city.name }}, {{ selectedCustomer.shipping_address.state.name }} {{ selectedCustomer.shipping_address.zip }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.shipping_address.country" class="country"> | ||||
|                       {{ selectedCustomer.shipping_address.country.name }} | ||||
|                     </label> | ||||
|                     <label v-if="selectedCustomer.shipping_address.phone" class="phone"> | ||||
|                       {{ selectedCustomer.shipping_address.phone }} | ||||
|                     </label> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="customer-content mb-1"> | ||||
|               <label class="email">{{ selectedCustomer.email ? selectedCustomer.email : selectedCustomer.name }}</label> | ||||
|               <label class="action" @click="removeCustomer">{{ $t('general.remove') }}</label> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           <base-popup v-else :class="['add-customer', {'customer-required': $v.selectedCustomer.$error}]" > | ||||
|             <div slot="activator" class="add-customer-action"> | ||||
|               <font-awesome-icon icon="user" class="customer-icon"/> | ||||
|               <div> | ||||
|                 <label>{{ $t('customers.new_customer') }} <span class="text-danger"> * </span></label> | ||||
|                 <p v-if="$v.selectedCustomer.$error && !$v.selectedCustomer.required" class="text-danger"> {{ $t('estimates.errors.required') }} </p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <customer-select-popup type="estimate" /> | ||||
|           </base-popup> | ||||
|         </div> | ||||
|         <div class="col estimate-input"> | ||||
|           <div class="row mb-3"> | ||||
|             <div class="col"> | ||||
|               <label>{{ $t('reports.estimates.estimate_date') }}<span class="text-danger"> * </span></label> | ||||
|               <base-date-picker | ||||
|                 v-model="newEstimate.estimate_date" | ||||
|                 :calendar-button="true" | ||||
|                 calendar-button-icon="calendar" | ||||
|                 @change="$v.newEstimate.estimate_date.$touch()" | ||||
|               /> | ||||
|               <span v-if="$v.newEstimate.estimate_date.$error && !$v.newEstimate.estimate_date.required" class="text-danger"> {{ $t('validation.required') }} </span> | ||||
|             </div> | ||||
|             <div class="col"> | ||||
|               <label>{{ $t('estimates.due_date') }}<span class="text-danger"> * </span></label> | ||||
|               <base-date-picker | ||||
|                 v-model="newEstimate.expiry_date" | ||||
|                 :invalid="$v.newEstimate.expiry_date.$error" | ||||
|                 :calendar-button="true" | ||||
|                 calendar-button-icon="calendar" | ||||
|                 @change="$v.newEstimate.expiry_date.$touch()" | ||||
|               /> | ||||
|               <span v-if="$v.newEstimate.expiry_date.$error && !$v.newEstimate.expiry_date.required" class="text-danger mt-1"> {{ $t('validation.required') }}</span> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="row mt-4"> | ||||
|             <div class="col"> | ||||
|               <label>{{ $t('estimates.estimate_number') }}<span class="text-danger"> * </span></label> | ||||
|               <base-input | ||||
|                 :invalid="$v.newEstimate.estimate_number.$error" | ||||
|                 :read-only="true" | ||||
|                 v-model="newEstimate.estimate_number" | ||||
|                 icon="hashtag" | ||||
|                 @input="$v.newEstimate.estimate_number.$touch()" | ||||
|               /> | ||||
|               <span v-show="$v.newEstimate.estimate_number.$error && !$v.newEstimate.estimate_number.required" class="text-danger mt-1"> {{ $tc('estimates.errors.required') }}  </span> | ||||
|             </div> | ||||
|             <div class="col"> | ||||
|               <label>{{ $t('estimates.ref_number') }}</label> | ||||
|               <base-input | ||||
|                 v-model="newEstimate.reference_number" | ||||
|                 :invalid="$v.newEstimate.reference_number.$error" | ||||
|                 icon="hashtag" | ||||
|                 type="number" | ||||
|                 @input="$v.newEstimate.reference_number.$touch()" | ||||
|               /> | ||||
|               <div v-if="$v.newEstimate.reference_number.$error" class="text-danger">{{ $tc('validation.ref_number_maxlength') }}</div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <table class="item-table"> | ||||
|         <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%;"> | ||||
|         </colgroup> | ||||
|         <thead class="item-table-header"> | ||||
|           <tr> | ||||
|             <th class="text-left"> | ||||
|               <span class="column-heading item-heading"> | ||||
|                 {{ $tc('items.item',2) }} | ||||
|               </span> | ||||
|             </th> | ||||
|             <th class="text-right"> | ||||
|               <span class="column-heading"> | ||||
|                 {{ $t('estimates.item.quantity') }} | ||||
|               </span> | ||||
|             </th> | ||||
|             <th class="text-left"> | ||||
|               <span class="column-heading"> | ||||
|                 {{ $t('estimates.item.price') }} | ||||
|               </span> | ||||
|             </th> | ||||
|             <th v-if="discountPerItem === 'YES'" class="text-right"> | ||||
|               <span class="column-heading"> | ||||
|                 {{ $t('estimates.item.discount') }} | ||||
|               </span> | ||||
|             </th> | ||||
|             <th class="text-right"> | ||||
|               <span class="column-heading amount-heading"> | ||||
|                 {{ $t('estimates.item.amount') }} | ||||
|               </span> | ||||
|             </th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <draggable v-model="newEstimate.items" class="item-body" tag="tbody" handle=".handle"> | ||||
|           <estimate-item | ||||
|             v-for="(item, index) in newEstimate.items" | ||||
|             :key="item.id" | ||||
|             :index="index" | ||||
|             :item-data="item" | ||||
|             :currency="currency" | ||||
|             :tax-per-item="taxPerItem" | ||||
|             :discount-per-item="discountPerItem" | ||||
|             @remove="removeItem" | ||||
|             @update="updateItem" | ||||
|             @itemValidate="checkItemsData" | ||||
|           /> | ||||
|         </draggable> | ||||
|       </table> | ||||
|       <div class="add-item-action" @click="addItem"> | ||||
|         <font-awesome-icon icon="shopping-basket" class="mr-2"/> | ||||
|         {{ $t('estimates.add_item') }} | ||||
|       </div> | ||||
|  | ||||
|       <div class="estimate-foot"> | ||||
|         <div> | ||||
|           <label>{{ $t('estimates.notes') }}</label> | ||||
|           <base-text-area | ||||
|             v-model="newEstimate.notes" | ||||
|             rows="3" | ||||
|             cols="50" | ||||
|             @input="$v.newEstimate.notes.$touch()" | ||||
|           /> | ||||
|           <div v-if="$v.newEstimate.notes.$error"> | ||||
|             <span v-if="!$v.newEstimate.notes.maxLength" class="text-danger">{{ $t('validation.notes_maxlength') }}</span> | ||||
|           </div> | ||||
|           <label class="mt-3 mb-1 d-block">{{ $t('estimates.estimate_template') }} <span class="text-danger"> * </span></label> | ||||
|           <base-button type="button" class="btn-template" icon="pencil-alt" right-icon @click="openTemplateModal" > | ||||
|             <span class="mr-4"> {{ $t('estimates.estimate_template') }} {{ getTemplateId }} </span> | ||||
|           </base-button> | ||||
|         </div> | ||||
|  | ||||
|         <div class="estimate-total"> | ||||
|           <div class="section"> | ||||
|             <label class="estimate-label">{{ $t('estimates.sub_total') }}</label> | ||||
|             <label class="estimate-amount"> | ||||
|               <div v-html="$utils.formatMoney(subtotal, currency)" /> | ||||
|             </label> | ||||
|           </div> | ||||
|           <div v-for="tax in allTaxes" :key="tax.tax_type_id" class="section"> | ||||
|             <label class="estimate-label">{{ tax.name }} - {{ tax.percent }}% </label> | ||||
|             <label class="estimate-amount"> | ||||
|               <div v-html="$utils.formatMoney(tax.amount, currency)" /> | ||||
|             </label> | ||||
|           </div> | ||||
|           <div v-if="discountPerItem === 'NO' || discountPerItem === null" class="section mt-2"> | ||||
|             <label class="estimate-label">{{ $t('estimates.discount') }}</label> | ||||
|             <div | ||||
|               class="btn-group discount-drop-down" | ||||
|               role="group" | ||||
|             > | ||||
|               <base-input | ||||
|                 v-model="discount" | ||||
|                 :invalid="$v.newEstimate.discount_val.$error" | ||||
|                 input-class="item-discount" | ||||
|                 @input="$v.newEstimate.discount_val.$touch()" | ||||
|               /> | ||||
|               <v-dropdown :show-arrow="false"> | ||||
|                 <button | ||||
|                   slot="activator" | ||||
|                   type="button" | ||||
|                   class="btn item-dropdown dropdown-toggle" | ||||
|                   data-toggle="dropdown" | ||||
|                   aria-haspopup="true" | ||||
|                   aria-expanded="false" | ||||
|                 > | ||||
|                   {{ newEstimate.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> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           <div v-if="taxPerItem === 'NO' || taxPerItem === null"> | ||||
|             <tax | ||||
|               v-for="(tax, index) in newEstimate.taxes" | ||||
|               :index="index" | ||||
|               :total="subtotalWithDiscount" | ||||
|               :key="tax.id" | ||||
|               :tax="tax" | ||||
|               :taxes="newEstimate.taxes" | ||||
|               :currency="currency" | ||||
|               :total-tax="totalSimpleTax" | ||||
|               @remove="removeEstimateTax" | ||||
|               @update="updateTax" | ||||
|             /> | ||||
|           </div> | ||||
|  | ||||
|           <base-popup v-if="taxPerItem === 'NO' || taxPerItem === null" ref="taxModal" class="tax-selector"> | ||||
|             <div slot="activator" class="float-right"> | ||||
|               + {{ $t('estimates.add_tax') }} | ||||
|             </div> | ||||
|             <tax-select-popup :taxes="newEstimate.taxes" @select="onSelectTax" /> | ||||
|           </base-popup> | ||||
|  | ||||
|           <div class="section border-top mt-3"> | ||||
|             <label class="estimate-label">{{ $t('estimates.total') }} {{ $t('estimates.amount') }}:</label> | ||||
|             <label class="estimate-amount total"> | ||||
|               <div v-html="$utils.formatMoney(total, currency)" /> | ||||
|             </label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import draggable from 'vuedraggable' | ||||
| import MultiSelect from 'vue-multiselect' | ||||
| import EstimateItem from './Item' | ||||
| import EstimateStub from '../../stub/estimate' | ||||
| import { mapActions, mapGetters } from 'vuex' | ||||
| import moment from 'moment' | ||||
| import { validationMixin } from 'vuelidate' | ||||
| import Guid from 'guid' | ||||
| import TaxStub from '../../stub/tax' | ||||
| import Tax from './EstimateTax' | ||||
| const { required, between, maxLength } = require('vuelidate/lib/validators') | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     EstimateItem, | ||||
|     MultiSelect, | ||||
|     Tax, | ||||
|     draggable | ||||
|   }, | ||||
|   mixins: [validationMixin], | ||||
|   data () { | ||||
|     return { | ||||
|       newEstimate: { | ||||
|         estimate_date: null, | ||||
|         expiry_date: null, | ||||
|         estimate_number: null, | ||||
|         user_id: null, | ||||
|         estimate_template_id: 1, | ||||
|         sub_total: null, | ||||
|         total: null, | ||||
|         tax: null, | ||||
|         notes: null, | ||||
|         discount_type: 'fixed', | ||||
|         discount_val: 0, | ||||
|         reference_number: null, | ||||
|         discount: 0, | ||||
|         items: [{ | ||||
|           ...EstimateStub, | ||||
|           id: Guid.raw(), | ||||
|           taxes: [{...TaxStub, id: Guid.raw()}] | ||||
|         }], | ||||
|         taxes: [] | ||||
|       }, | ||||
|       customers: [], | ||||
|       itemList: [], | ||||
|       estimateTemplates: [], | ||||
|       selectedCurrency: '', | ||||
|       taxPerItem: null, | ||||
|       discountPerItem: null, | ||||
|       initLoading: false, | ||||
|       isLoading: false, | ||||
|       maxDiscount: 0 | ||||
|     } | ||||
|   }, | ||||
|   validations () { | ||||
|     return { | ||||
|       newEstimate: { | ||||
|         estimate_date: { | ||||
|           required | ||||
|         }, | ||||
|         expiry_date: { | ||||
|           required | ||||
|         }, | ||||
|         estimate_number: { | ||||
|           required | ||||
|         }, | ||||
|         discount_val: { | ||||
|           between: between(0, this.subtotal) | ||||
|         }, | ||||
|         notes: { | ||||
|           maxLength: maxLength(255) | ||||
|         }, | ||||
|         reference_number: { | ||||
|           maxLength: maxLength(10) | ||||
|         } | ||||
|       }, | ||||
|       selectedCustomer: { | ||||
|         required | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters('general', [ | ||||
|       'itemDiscount' | ||||
|     ]), | ||||
|     ...mapGetters('currency', [ | ||||
|       'defaultCurrency' | ||||
|     ]), | ||||
|     ...mapGetters('estimate', [ | ||||
|       'getTemplateId', | ||||
|       'selectedCustomer' | ||||
|     ]), | ||||
|     currency () { | ||||
|       return this.selectedCurrency | ||||
|     }, | ||||
|     subtotalWithDiscount () { | ||||
|       return this.subtotal - this.newEstimate.discount_val | ||||
|     }, | ||||
|     total () { | ||||
|       return this.subtotalWithDiscount + this.totalTax | ||||
|     }, | ||||
|     subtotal () { | ||||
|       return this.newEstimate.items.reduce(function (a, b) { | ||||
|         return a + b['total'] | ||||
|       }, 0) | ||||
|     }, | ||||
|     discount: { | ||||
|       get: function () { | ||||
|         return this.newEstimate.discount | ||||
|       }, | ||||
|       set: function (newValue) { | ||||
|         if (this.newEstimate.discount_type === 'percentage') { | ||||
|           this.newEstimate.discount_val = (this.subtotal * newValue) / 100 | ||||
|         } else { | ||||
|           this.newEstimate.discount_val = newValue * 100 | ||||
|         } | ||||
|  | ||||
|         this.newEstimate.discount = newValue | ||||
|       } | ||||
|     }, | ||||
|     totalSimpleTax () { | ||||
|       return window._.sumBy(this.newEstimate.taxes, function (tax) { | ||||
|         if (!tax.compound_tax) { | ||||
|           return tax.amount | ||||
|         } | ||||
|  | ||||
|         return 0 | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     totalCompoundTax () { | ||||
|       return window._.sumBy(this.newEstimate.taxes, function (tax) { | ||||
|         if (tax.compound_tax) { | ||||
|           return tax.amount | ||||
|         } | ||||
|  | ||||
|         return 0 | ||||
|       }) | ||||
|     }, | ||||
|     totalTax () { | ||||
|       if (this.taxPerItem === 'NO' || this.taxPerItem === null) { | ||||
|         return this.totalSimpleTax + this.totalCompoundTax | ||||
|       } | ||||
|  | ||||
|       return window._.sumBy(this.newEstimate.items, function (tax) { | ||||
|         return tax.tax | ||||
|       }) | ||||
|     }, | ||||
|     allTaxes () { | ||||
|       let taxes = [] | ||||
|  | ||||
|       this.newEstimate.items.forEach((item) => { | ||||
|         item.taxes.forEach((tax) => { | ||||
|           let found = taxes.find((_tax) => { | ||||
|             return _tax.tax_type_id === tax.tax_type_id | ||||
|           }) | ||||
|  | ||||
|           if (found) { | ||||
|             found.amount += tax.amount | ||||
|           } else if (tax.tax_type_id) { | ||||
|             taxes.push({ | ||||
|               tax_type_id: tax.tax_type_id, | ||||
|               amount: tax.amount, | ||||
|               percent: tax.percent, | ||||
|               name: tax.name | ||||
|             }) | ||||
|           } | ||||
|         }) | ||||
|       }) | ||||
|  | ||||
|       return taxes | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     selectedCustomer (newVal) { | ||||
|       if (newVal && newVal.currency) { | ||||
|         this.selectedCurrency = newVal.currency | ||||
|       } else { | ||||
|         this.selectedCurrency = this.defaultCurrency | ||||
|       } | ||||
|     }, | ||||
|     subtotal (newValue) { | ||||
|       if (this.newEstimate.discount_type === 'percentage') { | ||||
|         this.newEstimate.discount_val = (this.newEstimate.discount * newValue) / 100 | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   created () { | ||||
|     this.loadData() | ||||
|     this.fetchInitialItems() | ||||
|     this.resetSelectedCustomer() | ||||
|     window.hub.$on('newTax', this.onSelectTax) | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions('modal', [ | ||||
|       'openModal' | ||||
|     ]), | ||||
|     ...mapActions('estimate', [ | ||||
|       'addEstimate', | ||||
|       'fetchCreateEstimate', | ||||
|       'fetchEstimate', | ||||
|       'resetSelectedCustomer', | ||||
|       'selectCustomer', | ||||
|       'updateEstimate' | ||||
|     ]), | ||||
|     ...mapActions('item', [ | ||||
|       'fetchItems' | ||||
|     ]), | ||||
|     selectFixed () { | ||||
|       if (this.newEstimate.discount_type === 'fixed') { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       this.newEstimate.discount_val = this.newEstimate.discount * 100 | ||||
|       this.newEstimate.discount_type = 'fixed' | ||||
|     }, | ||||
|     selectPercentage () { | ||||
|       if (this.newEstimate.discount_type === 'percentage') { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       this.newEstimate.discount_val = (this.subtotal * this.newEstimate.discount) / 100 | ||||
|  | ||||
|       this.newEstimate.discount_type = 'percentage' | ||||
|     }, | ||||
|     updateTax (data) { | ||||
|       Object.assign(this.newEstimate.taxes[data.index], {...data.item}) | ||||
|     }, | ||||
|     async fetchInitialItems () { | ||||
|       await this.fetchItems({ | ||||
|         filter: {}, | ||||
|         orderByField: '', | ||||
|         orderBy: '' | ||||
|       }) | ||||
|     }, | ||||
|     async loadData () { | ||||
|       if (this.$route.name === 'estimates.edit') { | ||||
|         this.initLoading = true | ||||
|         let response = await this.fetchEstimate(this.$route.params.id) | ||||
|  | ||||
|         if (response.data) { | ||||
|           this.selectCustomer(response.data.estimate.user_id) | ||||
|           this.newEstimate = response.data.estimate | ||||
|           this.discountPerItem = response.data.discount_per_item | ||||
|           this.taxPerItem = response.data.tax_per_item | ||||
|           this.selectedCurrency = this.defaultCurrency | ||||
|           this.estimateTemplates = response.data.estimateTemplates | ||||
|         } | ||||
|         this.initLoading = false | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       this.initLoading = true | ||||
|       let response = await this.fetchCreateEstimate() | ||||
|       if (response.data) { | ||||
|         this.discountPerItem = response.data.discount_per_item | ||||
|         this.taxPerItem = response.data.tax_per_item | ||||
|         this.selectedCurrency = this.defaultCurrency | ||||
|         this.estimateTemplates = response.data.estimateTemplates | ||||
|         let today = new Date() | ||||
|         this.newEstimate.estimate_date = moment(today).toString() | ||||
|         this.newEstimate.expiry_date = moment(today).add(7, 'days').toString() | ||||
|         this.newEstimate.estimate_number = response.data.nextEstimateNumber | ||||
|         this.itemList = response.data.items | ||||
|       } | ||||
|       this.initLoading = false | ||||
|     }, | ||||
|     removeCustomer () { | ||||
|       this.resetSelectedCustomer() | ||||
|     }, | ||||
|     openTemplateModal () { | ||||
|       this.openModal({ | ||||
|         'title': 'Choose a template', | ||||
|         'componentName': 'EstimateTemplate', | ||||
|         'data': this.estimateTemplates | ||||
|       }) | ||||
|     }, | ||||
|     addItem () { | ||||
|       this.newEstimate.items.push({...EstimateStub, id: Guid.raw(), taxes: [{...TaxStub, id: Guid.raw()}]}) | ||||
|     }, | ||||
|     removeItem (index) { | ||||
|       this.newEstimate.items.splice(index, 1) | ||||
|     }, | ||||
|     updateItem (data) { | ||||
|       Object.assign(this.newEstimate.items[data.index], {...data.item}) | ||||
|     }, | ||||
|     submitEstimateData () { | ||||
|       if (!this.checkValid()) { | ||||
|         return false | ||||
|       } | ||||
|  | ||||
|       this.isLoading = true | ||||
|  | ||||
|       let data = { | ||||
|         ...this.newEstimate, | ||||
|         estimate_date: moment(this.newEstimate.estimate_date).format('DD/MM/YYYY'), | ||||
|         expiry_date: moment(this.newEstimate.expiry_date).format('DD/MM/YYYY'), | ||||
|         sub_total: this.subtotal, | ||||
|         total: this.total, | ||||
|         tax: this.totalTax, | ||||
|         user_id: null, | ||||
|         estimate_template_id: this.getTemplateId | ||||
|       } | ||||
|  | ||||
|       if (this.selectedCustomer != null) { | ||||
|         data.user_id = this.selectedCustomer.id | ||||
|       } | ||||
|  | ||||
|       if (this.$route.name === 'estimates.edit') { | ||||
|         this.submitUpdate(data) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       this.submitSave(data) | ||||
|     }, | ||||
|     submitSave (data) { | ||||
|       this.addEstimate(data).then((res) => { | ||||
|         if (res.data) { | ||||
|           window.toastr['success'](this.$t('estimates.created_message')) | ||||
|           this.$router.push('/admin/estimates') | ||||
|         } | ||||
|  | ||||
|         this.isLoading = false | ||||
|       }).catch((err) => { | ||||
|         this.isLoading = false | ||||
|         console.log(err) | ||||
|       }) | ||||
|     }, | ||||
|     submitUpdate (data) { | ||||
|       this.updateEstimate(data).then((res) => { | ||||
|         if (res.data) { | ||||
|           window.toastr['success'](this.$t('estimates.updated_message')) | ||||
|           this.$router.push('/admin/estimates') | ||||
|         } | ||||
|  | ||||
|         this.isLoading = false | ||||
|       }).catch((err) => { | ||||
|         this.isLoading = false | ||||
|         console.log(err) | ||||
|       }) | ||||
|     }, | ||||
|     checkItemsData (index, isValid) { | ||||
|       this.newEstimate.items[index].valid = isValid | ||||
|     }, | ||||
|     onSelectTax (selectedTax) { | ||||
|       let amount = 0 | ||||
|  | ||||
|       if (selectedTax.compound_tax && this.subtotalWithDiscount) { | ||||
|         amount = ((this.subtotalWithDiscount + this.totalSimpleTax) * selectedTax.percent) / 100 | ||||
|       } else if (this.subtotalWithDiscount && selectedTax.percent) { | ||||
|         amount = (this.subtotalWithDiscount * selectedTax.percent) / 100 | ||||
|       } | ||||
|  | ||||
|       this.newEstimate.taxes.push({ | ||||
|         ...TaxStub, | ||||
|         id: Guid.raw(), | ||||
|         name: selectedTax.name, | ||||
|         percent: selectedTax.percent, | ||||
|         compound_tax: selectedTax.compound_tax, | ||||
|         tax_type_id: selectedTax.id, | ||||
|         amount | ||||
|       }) | ||||
|  | ||||
|       this.$refs.taxModal.close() | ||||
|     }, | ||||
|     removeEstimateTax (index) { | ||||
|       this.newEstimate.taxes.splice(index, 1) | ||||
|     }, | ||||
|     checkValid () { | ||||
|       this.$v.newEstimate.$touch() | ||||
|       this.$v.selectedCustomer.$touch() | ||||
|       window.hub.$emit('checkItems') | ||||
|       let isValid = true | ||||
|       this.newEstimate.items.forEach((item) => { | ||||
|         if (!item.valid) { | ||||
|           isValid = false | ||||
|         } | ||||
|       }) | ||||
|       if (this.$v.newEstimate.$invalid === false && isValid === true) { | ||||
|         return true | ||||
|       } | ||||
|       return false | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										83
									
								
								resources/assets/js/views/estimates/EstimateTax.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								resources/assets/js/views/estimates/EstimateTax.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| <template> | ||||
|   <div class="section mt-2"> | ||||
|     <label class="estimate-label"> | ||||
|       {{ tax.name }} ({{ tax.percent }}%) | ||||
|     </label> | ||||
|     <label class="estimate-amount"> | ||||
|       <div v-html="$utils.formatMoney(tax.amount, currency)" /> | ||||
|  | ||||
|       <font-awesome-icon | ||||
|         class="ml-2" | ||||
|         icon="trash-alt" | ||||
|         @click="$emit('remove', index)" | ||||
|       /> | ||||
|     </label> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     index: { | ||||
|       type: Number, | ||||
|       required: true | ||||
|     }, | ||||
|     tax: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|     }, | ||||
|     taxes: { | ||||
|       type: Array, | ||||
|       required: true | ||||
|     }, | ||||
|     total: { | ||||
|       type: Number, | ||||
|       default: 0 | ||||
|     }, | ||||
|     totalTax: { | ||||
|       type: Number, | ||||
|       default: 0 | ||||
|     }, | ||||
|     currency: { | ||||
|       type: [Object, String], | ||||
|       required: true | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     taxAmount () { | ||||
|       if (this.tax.compound_tax && this.total) { | ||||
|         return ((this.total + this.totalTax) * this.tax.percent) / 100 | ||||
|       } | ||||
|  | ||||
|       if (this.total && this.tax.percent) { | ||||
|         return (this.total * this.tax.percent) / 100 | ||||
|       } | ||||
|  | ||||
|       return 0 | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     total: { | ||||
|       handler: 'updateTax' | ||||
|     }, | ||||
|     totalTax: { | ||||
|       handler: 'updateTax' | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     updateTax () { | ||||
|       this.$emit('update', { | ||||
|         'index': this.index, | ||||
|         'item': { | ||||
|           ...this.tax, | ||||
|           amount: this.taxAmount | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										560
									
								
								resources/assets/js/views/estimates/Index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										560
									
								
								resources/assets/js/views/estimates/Index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,560 @@ | ||||
| <template> | ||||
|   <div class="estimate-index-page estimates main-content"> | ||||
|     <div class="page-header"> | ||||
|       <h3 class="page-title">{{ $t('estimates.title') }}</h3> | ||||
|       <ol class="breadcrumb"> | ||||
|         <li class="breadcrumb-item"> | ||||
|           <router-link | ||||
|             slot="item-title" | ||||
|             to="dashboard"> | ||||
|             {{ $t('general.home') }} | ||||
|           </router-link> | ||||
|         </li> | ||||
|         <li class="breadcrumb-item"> | ||||
|           <router-link | ||||
|             slot="item-title" | ||||
|             to="#"> | ||||
|             {{ $tc('estimates.estimate', 2) }} | ||||
|           </router-link> | ||||
|         </li> | ||||
|       </ol> | ||||
|       <div class="page-actions row"> | ||||
|         <div class="col-xs-2 mr-4"> | ||||
|           <base-button | ||||
|             v-show="totalEstimates || filtersApplied" | ||||
|             :outline="true" | ||||
|             :icon="filterIcon" | ||||
|             size="large" | ||||
|             color="theme" | ||||
|             right-icon | ||||
|             @click="toggleFilter" | ||||
|           > | ||||
|             {{ $t('general.filter') }} | ||||
|           </base-button> | ||||
|         </div> | ||||
|         <router-link slot="item-title" class="col-xs-2" to="estimates/create"> | ||||
|           <base-button | ||||
|             size="large" | ||||
|             icon="plus" | ||||
|             color="theme" > | ||||
|             {{ $t('estimates.new_estimate') }}</base-button> | ||||
|         </router-link> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <transition name="fade"> | ||||
|       <div v-show="showFilters" class="filter-section"> | ||||
|         <div class="filter-container"> | ||||
|           <div class="filter-customer"> | ||||
|             <label>{{ $tc('customers.customer',1) }} </label> | ||||
|             <base-customer-select | ||||
|               ref="customerSelect" | ||||
|               @select="onSelectCustomer" | ||||
|               @deselect="clearCustomerSearch" | ||||
|             /> | ||||
|           </div> | ||||
|           <div class="filter-status"> | ||||
|             <label>{{ $t('estimates.status') }}</label> | ||||
|             <base-select | ||||
|               v-model="filters.status" | ||||
|               :options="status" | ||||
|               :searchable="true" | ||||
|               :show-labels="false" | ||||
|               :placeholder="$t('general.select_a_status')" | ||||
|               @remove="clearStatusSearch()" | ||||
|             /> | ||||
|           </div> | ||||
|           <div class="filter-date"> | ||||
|             <div class="from pr-3"> | ||||
|               <label>{{ $t('general.from') }}</label> | ||||
|               <base-date-picker | ||||
|                 v-model="filters.from_date" | ||||
|                 :calendar-button="true" | ||||
|                 calendar-button-icon="calendar" | ||||
|               /> | ||||
|             </div> | ||||
|             <div class="dashed" /> | ||||
|             <div class="to pl-3"> | ||||
|               <label>{{ $t('general.to') }}</label> | ||||
|               <base-date-picker | ||||
|                 v-model="filters.to_date" | ||||
|                 :calendar-button="true" | ||||
|                 calendar-button-icon="calendar" | ||||
|               /> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="filter-estimate"> | ||||
|             <label>{{ $t('estimates.estimate_number') }}</label> | ||||
|             <base-input | ||||
|               v-model="filters.estimate_number" | ||||
|               icon="hashtag"/> | ||||
|           </div> | ||||
|         </div> | ||||
|         <label class="clear-filter" @click="clearFilter">{{ $t('general.clear_all') }}</label> | ||||
|       </div> | ||||
|     </transition> | ||||
|     <div v-cloak v-show="showEmptyScreen" class="col-xs-1 no-data-info" align="center"> | ||||
|       <moon-walker-icon class="mt-5 mb-4"/> | ||||
|       <div class="row" align="center"> | ||||
|         <label class="col title">{{ $t('estimates.no_estimates') }}</label> | ||||
|       </div> | ||||
|       <div class="row"> | ||||
|         <label class="description col mt-1" align="center">{{ $t('estimates.list_of_estimates') }}</label> | ||||
|       </div> | ||||
|       <div class="btn-container"> | ||||
|         <base-button | ||||
|           :outline="true" | ||||
|           color="theme" | ||||
|           class="mt-3" | ||||
|           size="large" | ||||
|           @click="$router.push('estimates/create')" | ||||
|         > | ||||
|           {{ $t('estimates.add_new_estimate') }} | ||||
|         </base-button> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div v-show="!showEmptyScreen" class="table-container"> | ||||
|       <div class="table-actions mt-5"> | ||||
|         <p class="table-stats">{{ $t('general.showing') }}: <b>{{ estimates.length }}</b> {{ $t('general.of') }} <b>{{ totalEstimates }}</b></p> | ||||
|  | ||||
|         <!-- Tabs --> | ||||
|         <ul class="tabs"> | ||||
|           <li class="tab" @click="getStatus('DRAFT')"> | ||||
|             <a :class="['tab-link', {'a-active': filters.status === 'DRAFT'}]" href="#">{{ $t('general.draft') }}</a> | ||||
|           </li> | ||||
|           <li class="tab" @click="getStatus('SENT')"> | ||||
|             <a :class="['tab-link', {'a-active': filters.status === 'SENT'}]" href="#" >{{ $t('general.sent') }}</a> | ||||
|           </li> | ||||
|           <li class="tab" @click="getStatus('')"> | ||||
|             <a :class="['tab-link', {'a-active': filters.status === '' || filters.status === null}]" href="#">{{ $t('general.all') }}</a> | ||||
|           </li> | ||||
|         </ul> | ||||
|         <transition name="fade"> | ||||
|           <v-dropdown v-if="selectedEstimates.length" :show-arrow="false"> | ||||
|             <span slot="activator" href="#" class="table-actions-button dropdown-toggle"> | ||||
|               {{ $t('general.actions') }} | ||||
|             </span> | ||||
|             <v-dropdown-item> | ||||
|               <div class="dropdown-item" @click="removeMultipleEstimates"> | ||||
|                 <font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" /> | ||||
|                 {{ $t('general.delete') }} | ||||
|               </div> | ||||
|             </v-dropdown-item> | ||||
|           </v-dropdown> | ||||
|         </transition> | ||||
|       </div> | ||||
|  | ||||
|       <div class="custom-control custom-checkbox"> | ||||
|         <input | ||||
|           id="select-all" | ||||
|           v-model="selectAllFieldStatus" | ||||
|           :disabled="estimates.length <= 0" | ||||
|           type="checkbox" | ||||
|           class="custom-control-input" | ||||
|           @change="selectAllEstimates" | ||||
|         > | ||||
|         <label v-show="!isRequestOngoing" for="select-all" class="custom-control-label selectall"> | ||||
|           <span class="select-all-label">{{ $t('general.select_all') }} </span> | ||||
|         </label> | ||||
|       </div> | ||||
|  | ||||
|       <table-component | ||||
|         ref="table" | ||||
|         :show-filter="false" | ||||
|         :data="fetchData" | ||||
|         table-class="table" | ||||
|       > | ||||
|         <table-column | ||||
|           :sortable="false" | ||||
|           :filterable="false" | ||||
|           cell-class="no-click" | ||||
|         > | ||||
|           <template slot-scope="row"> | ||||
|             <div class="custom-control custom-checkbox"> | ||||
|               <input | ||||
|                 :id="row.id" | ||||
|                 v-model="selectField" | ||||
|                 :value="row.id" | ||||
|                 type="checkbox" | ||||
|                 class="custom-control-input" | ||||
|               > | ||||
|               <label :for="row.id" class="custom-control-label" /> | ||||
|             </div> | ||||
|           </template> | ||||
|         </table-column> | ||||
|         <table-column | ||||
|           :label="$t('estimates.date')" | ||||
|           sort-as="estimate_date" | ||||
|           show="formattedEstimateDate" /> | ||||
|         <table-column | ||||
|           :label="$t('estimates.contact')" | ||||
|           sort-as="name" | ||||
|           show="name" /> | ||||
|         <table-column | ||||
|           :label="$t('estimates.expiry_date')" | ||||
|           sort-as="expiry_date" | ||||
|           show="formattedExpiryDate" /> | ||||
|         <table-column | ||||
|           :label="$t('estimates.status')" | ||||
|           show="status" > | ||||
|           <template slot-scope="row" > | ||||
|             <span> {{ $t('estimates.status') }}</span> | ||||
|             <span :class="'est-status-'+row.status.toLowerCase()">{{ row.status }}</span> | ||||
|           </template> | ||||
|         </table-column> | ||||
|         <table-column | ||||
|           :label="$tc('estimates.estimate', 1)" | ||||
|           show="estimate_number"/> | ||||
|         <table-column | ||||
|           :label="$t('invoices.total')" | ||||
|           sort-as="total" | ||||
|         > | ||||
|           <template slot-scope="row"> | ||||
|             <div v-html="$utils.formatMoney(row.total, row.user.currency)" /> | ||||
|           </template> | ||||
|         </table-column> | ||||
|         <table-column | ||||
|           :sortable="false" | ||||
|           :filterable="false" | ||||
|           cell-class="action-dropdown" | ||||
|         > | ||||
|           <template slot-scope="row"> | ||||
|             <span> {{ $t('estimates.action') }} </span> | ||||
|             <v-dropdown> | ||||
|               <a slot="activator" href="#"> | ||||
|                 <dot-icon /> | ||||
|               </a> | ||||
|               <v-dropdown-item> | ||||
|                 <router-link :to="{path: `estimates/${row.id}/edit`}" class="dropdown-item"> | ||||
|                   <font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" /> | ||||
|                   {{ $t('general.edit') }} | ||||
|                 </router-link> | ||||
|               </v-dropdown-item> | ||||
|               <v-dropdown-item> | ||||
|                 <div class="dropdown-item" @click="removeEstimate(row.id)"> | ||||
|                   <font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" /> | ||||
|                   {{ $t('general.delete') }} | ||||
|                 </div> | ||||
|               </v-dropdown-item> | ||||
|               <v-dropdown-item> | ||||
|                 <router-link :to="{path: `estimates/${row.id}/view`}" class="dropdown-item"> | ||||
|                   <font-awesome-icon icon="eye" class="dropdown-item-icon" /> | ||||
|                   {{ $t('general.view') }} | ||||
|                 </router-link> | ||||
|               </v-dropdown-item> | ||||
|               <v-dropdown-item> | ||||
|                 <a class="dropdown-item" href="#" @click="convertInToinvoice(row.id)"> | ||||
|                   <font-awesome-icon icon="envelope" class="dropdown-item-icon" /> | ||||
|                   {{ $t('estimates.convert_to_invoice') }} | ||||
|                 </a> | ||||
|               </v-dropdown-item> | ||||
|               <v-dropdown-item> | ||||
|                 <a class="dropdown-item" href="#" @click.self="onMarkAsSent(row.id)"> | ||||
|                   <font-awesome-icon icon="check-circle" class="dropdown-item-icon" /> | ||||
|                   {{ $t('estimates.mark_as_sent') }} | ||||
|                 </a> | ||||
|               </v-dropdown-item> | ||||
|               <v-dropdown-item> | ||||
|                 <a class="dropdown-item" href="#" @click.self="sendEstimate(row.id)"> | ||||
|                   <font-awesome-icon icon="paper-plane" class="dropdown-item-icon" /> | ||||
|                   {{ $t('estimates.send_estimate') }} | ||||
|                 </a> | ||||
|               </v-dropdown-item> | ||||
|               <v-dropdown-item v-if="row.status === 'DRAFT'"> | ||||
|                 <a class="dropdown-item" href="#" @click.self="onMarkAsAccepted(row.id)"> | ||||
|                   <font-awesome-icon icon="check-circle" class="dropdown-item-icon" /> | ||||
|                   {{ $t('estimates.mark_as_accepted') }} | ||||
|                 </a> | ||||
|               </v-dropdown-item> | ||||
|               <v-dropdown-item v-if="row.status === 'DRAFT'"> | ||||
|                 <a class="dropdown-item" href="#" @click.self="onMarkAsRejected(row.id)"> | ||||
|                   <font-awesome-icon icon="times-circle" class="dropdown-item-icon" /> | ||||
|                   {{ $t('estimates.mark_as_rejected') }} | ||||
|                 </a> | ||||
|               </v-dropdown-item> | ||||
|             </v-dropdown> | ||||
|           </template> | ||||
|         </table-column> | ||||
|       </table-component> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import { mapActions, mapGetters } from 'vuex' | ||||
| import MoonWalkerIcon from '../../../js/components/icon/MoonwalkerIcon' | ||||
| import { SweetModal, SweetModalTab } from 'sweet-modal-vue' | ||||
| import ObservatoryIcon from '../../components/icon/ObservatoryIcon' | ||||
| import moment from 'moment' | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     'moon-walker-icon': MoonWalkerIcon | ||||
|   }, | ||||
|   data () { | ||||
|     return { | ||||
|       showFilters: false, | ||||
|       currency: null, | ||||
|       status: ['DRAFT', 'SENT', 'VIEWED', 'EXPIRED', 'ACCEPTED', 'REJECTED'], | ||||
|       filtersApplied: false, | ||||
|       isRequestOngoing: true, | ||||
|       filters: { | ||||
|         customer: '', | ||||
|         status: 'DRAFT', | ||||
|         from_date: '', | ||||
|         to_date: '', | ||||
|         estimate_number: '' | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     focus, | ||||
|     showEmptyScreen () { | ||||
|       return !this.totalEstimates && !this.isRequestOngoing && !this.filtersApplied | ||||
|     }, | ||||
|     filterIcon () { | ||||
|       return (this.showFilters) ? 'times' : 'filter' | ||||
|     }, | ||||
|     ...mapGetters('customer', [ | ||||
|       'customers' | ||||
|     ]), | ||||
|     ...mapGetters('estimate', [ | ||||
|       'selectedEstimates', | ||||
|       'totalEstimates', | ||||
|       'estimates', | ||||
|       'selectAllField' | ||||
|     ]), | ||||
|     selectField: { | ||||
|       get: function () { | ||||
|         return this.selectedEstimates | ||||
|       }, | ||||
|       set: function (val) { | ||||
|         this.selectEstimate(val) | ||||
|       } | ||||
|     }, | ||||
|     selectAllFieldStatus: { | ||||
|       get: function () { | ||||
|         return this.selectAllField | ||||
|       }, | ||||
|       set: function (val) { | ||||
|         this.setSelectAllState(val) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     filters: { | ||||
|       handler: 'setFilters', | ||||
|       deep: true | ||||
|     } | ||||
|   }, | ||||
|   created () { | ||||
|     this.fetchCustomers() | ||||
|   }, | ||||
|   destroyed () { | ||||
|     if (this.selectAllField) { | ||||
|       this.selectAllEstimates() | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions('estimate', [ | ||||
|       'fetchEstimates', | ||||
|       'resetSelectedEstimates', | ||||
|       'getRecord', | ||||
|       'selectEstimate', | ||||
|       'selectAllEstimates', | ||||
|       'deleteEstimate', | ||||
|       'deleteMultipleEstimates', | ||||
|       'markAsSent', | ||||
|       'convertToInvoice', | ||||
|       'setSelectAllState', | ||||
|       'markAsAccepted', | ||||
|       'markAsRejected', | ||||
|       'sendEmail' | ||||
|     ]), | ||||
|     ...mapActions('customer', [ | ||||
|       'fetchCustomers' | ||||
|     ]), | ||||
|     refreshTable () { | ||||
|       this.$refs.table.refresh() | ||||
|     }, | ||||
|     getStatus (val) { | ||||
|       this.filters.status = val | ||||
|     }, | ||||
|     async fetchData ({ page, filter, sort }) { | ||||
|       let data = { | ||||
|         customer_id: this.filters.customer === '' ? this.filters.customer : this.filters.customer.id, | ||||
|         status: this.filters.status, | ||||
|         from_date: this.filters.from_date === '' ? this.filters.from_date : moment(this.filters.from_date).format('DD/MM/YYYY'), | ||||
|         to_date: this.filters.to_date === '' ? this.filters.to_date : moment(this.filters.to_date).format('DD/MM/YYYY'), | ||||
|         estimate_number: this.filters.estimate_number, | ||||
|         orderByField: sort.fieldName || 'created_at', | ||||
|         orderBy: sort.order || 'desc', | ||||
|         page | ||||
|       } | ||||
|  | ||||
|       this.isRequestOngoing = true | ||||
|       let response = await this.fetchEstimates(data) | ||||
|       this.isRequestOngoing = false | ||||
|  | ||||
|       this.currency = response.data.currency | ||||
|  | ||||
|       return { | ||||
|         data: response.data.estimates.data, | ||||
|         pagination: { | ||||
|           totalPages: response.data.estimates.last_page, | ||||
|           currentPage: page, | ||||
|           count: response.data.estimates.count | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     async onMarkAsAccepted (id) { | ||||
|       const data = { | ||||
|         id: id | ||||
|       } | ||||
|       let response = await this.markAsAccepted(data) | ||||
|       this.refreshTable() | ||||
|       if (response.data) { | ||||
|         this.filters.status = 'ACCEPTED' | ||||
|         this.$refs.table.refresh() | ||||
|         window.toastr['success'](this.$tc('estimates.marked_as_rejected_message')) | ||||
|       } | ||||
|     }, | ||||
|     async onMarkAsRejected (id) { | ||||
|       const data = { | ||||
|         id: id | ||||
|       } | ||||
|       let response = await this.markAsRejected(data) | ||||
|       this.refreshTable() | ||||
|       if (response.data) { | ||||
|         this.filters.status = 'REJECTED' | ||||
|         this.$refs.table.refresh() | ||||
|         window.toastr['success'](this.$tc('estimates.marked_as_rejected_message')) | ||||
|       } | ||||
|     }, | ||||
|     setFilters () { | ||||
|       this.filtersApplied = true | ||||
|       this.resetSelectedEstimates() | ||||
|       this.$refs.table.refresh() | ||||
|     }, | ||||
|     clearFilter () { | ||||
|       if (this.filters.customer) { | ||||
|         this.$refs.customerSelect.$refs.baseSelect.removeElement(this.filters.customer) | ||||
|       } | ||||
|  | ||||
|       this.filters = { | ||||
|         customer: '', | ||||
|         status: '', | ||||
|         from_date: '', | ||||
|         to_date: '', | ||||
|         estimate_number: '' | ||||
|       } | ||||
|  | ||||
|       this.$nextTick(() => { | ||||
|         this.filtersApplied = false | ||||
|       }) | ||||
|     }, | ||||
|     toggleFilter () { | ||||
|       if (this.showFilters && this.filtersApplied) { | ||||
|         this.clearFilter() | ||||
|         this.refreshTable() | ||||
|       } | ||||
|  | ||||
|       this.showFilters = !this.showFilters | ||||
|     }, | ||||
|     onSelectCustomer (customer) { | ||||
|       this.filters.customer = customer | ||||
|     }, | ||||
|     async removeEstimate (id) { | ||||
|       this.id = id | ||||
|       swal({ | ||||
|         title: this.$t('general.are_you_sure'), | ||||
|         text: this.$tc('estimates.confirm_delete', 1), | ||||
|         icon: 'error', | ||||
|         buttons: true, | ||||
|         dangerMode: true | ||||
|       }).then(async (willDelete) => { | ||||
|         if (willDelete) { | ||||
|           let res = await this.deleteEstimate(this.id) | ||||
|           if (res.data.success) { | ||||
|             this.$refs.table.refresh() | ||||
|             this.filtersApplied = false | ||||
|             this.resetSelectedEstimates() | ||||
|             window.toastr['success'](this.$tc('estimates.deleted_message', 1)) | ||||
|           } else if (res.data.error) { | ||||
|             window.toastr['error'](res.data.message) | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     async convertInToinvoice (id) { | ||||
|       swal({ | ||||
|         title: this.$t('general.are_you_sure'), | ||||
|         text: this.$t('estimates.confirm_conversion'), | ||||
|         icon: 'error', | ||||
|         buttons: true, | ||||
|         dangerMode: true | ||||
|       }).then(async (willDelete) => { | ||||
|         if (willDelete) { | ||||
|           let res = await this.convertToInvoice(id) | ||||
|           if (res.data) { | ||||
|             window.toastr['success'](this.$t('estimates.conversion_message')) | ||||
|             this.$router.push(`invoices/${res.data.invoice.id}/edit`) | ||||
|           } else if (res.data.error) { | ||||
|             window.toastr['error'](res.data.message) | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     async removeMultipleEstimates () { | ||||
|       swal({ | ||||
|         title: this.$t('general.are_you_sure'), | ||||
|         text: this.$tc('estimates.confirm_delete', 2), | ||||
|         icon: 'error', | ||||
|         buttons: true, | ||||
|         dangerMode: true | ||||
|       }).then(async (willDelete) => { | ||||
|         if (willDelete) { | ||||
|           let res = await this.deleteMultipleEstimates() | ||||
|           if (res.data.success) { | ||||
|             this.$refs.table.refresh() | ||||
|             this.resetSelectedEstimates() | ||||
|             this.filtersApplied = false | ||||
|             window.toastr['success'](this.$tc('estimates.deleted_message', 2)) | ||||
|           } else if (res.data.error) { | ||||
|             window.toastr['error'](res.data.message) | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     async clearCustomerSearch (removedOption, id) { | ||||
|       this.filters.customer = '' | ||||
|       this.refreshTable() | ||||
|     }, | ||||
|     async clearStatusSearch (removedOption, id) { | ||||
|       this.filters.status = '' | ||||
|       this.refreshTable() | ||||
|     }, | ||||
|     async onMarkAsSent (id) { | ||||
|       const data = { | ||||
|         id: id | ||||
|       } | ||||
|       let response = await this.markAsSent(data) | ||||
|       this.refreshTable() | ||||
|       if (response.data) { | ||||
|         window.toastr['success'](this.$tc('estimates.mark_as_sent')) | ||||
|       } | ||||
|     }, | ||||
|     async sendEstimate (id) { | ||||
|       const data = { | ||||
|         id: id | ||||
|       } | ||||
|       let response = await this.sendEmail(data) | ||||
|       this.refreshTable() | ||||
|       if (response.data) { | ||||
|         window.toastr['success'](this.$tc('estimates.mark_as_sent')) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										402
									
								
								resources/assets/js/views/estimates/Item.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										402
									
								
								resources/assets/js/views/estimates/Item.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,402 @@ | ||||
| <template> | ||||
|   <tr class="item-row estimate-item-row"> | ||||
|     <td colspan="5"> | ||||
|       <table class="full-width"> | ||||
|         <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%;"> | ||||
|         </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" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <item-select | ||||
|                   ref="itemSelect" | ||||
|                   :invalid="$v.item.name.$error" | ||||
|                   :invalid-description="$v.item.description.$error" | ||||
|                   :item="item" | ||||
|                   @search="searchVal" | ||||
|                   @select="onSelectItem" | ||||
|                   @deselect="deselectItem" | ||||
|                   @onDesriptionInput="$v.item.description.$touch()" | ||||
|                 /> | ||||
|               </div> | ||||
|             </td> | ||||
|             <td class="text-right"> | ||||
|               <base-input | ||||
|                 v-model="item.quantity" | ||||
|                 :invalid="$v.item.quantity.$error" | ||||
|                 type="number" | ||||
|                 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> | ||||
|               </div> | ||||
|             </td> | ||||
|             <td class="text-left"> | ||||
|               <div class="d-flex flex-column"> | ||||
|                 <div class="flex-fillbd-highlight"> | ||||
|                   <div class="base-input"> | ||||
|                     <money | ||||
|                       v-model="price" | ||||
|                       v-bind="customerCurrency" | ||||
|                       class="input-field" | ||||
|                       @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> | ||||
|                   </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 | ||||
|                     v-model="discount" | ||||
|                     :invalid="$v.item.discount_val.$error" | ||||
|                     input-class="item-discount" | ||||
|                     @input="$v.item.discount_val.$touch()" | ||||
|                   /> | ||||
|                   <v-dropdown :show-arrow="false" theme-light> | ||||
|                     <button | ||||
|                       slot="activator" | ||||
|                       type="button" | ||||
|                       class="btn item-dropdown dropdown-toggle" | ||||
|                       data-toggle="dropdown" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-expanded="false" | ||||
|                     > | ||||
|                       {{ 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> | ||||
|                 </div> | ||||
|                 <!-- <div v-if="$v.item.discount.$error"> discount error </div> --> | ||||
|               </div> | ||||
|             </td> | ||||
|             <td class="text-right"> | ||||
|               <div class="item-amount"> | ||||
|                 <span> | ||||
|                   <div v-html="$utils.formatMoney(total, currency)" /> | ||||
|                 </span> | ||||
|  | ||||
|                 <div class="remove-icon-wrapper"> | ||||
|                   <font-awesome-icon | ||||
|                     v-if="index > 0" | ||||
|                     class="remove-icon" | ||||
|                     icon="trash-alt" | ||||
|                     @click="removeItem" | ||||
|                   /> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </td> | ||||
|           </tr> | ||||
|           <tr v-if="taxPerItem === 'YES'" class="tax-tr"> | ||||
|             <td /> | ||||
|             <td colspan="4"> | ||||
|               <tax | ||||
|                 v-for="(tax, index) in item.taxes" | ||||
|                 :key="tax.id" | ||||
|                 :index="index" | ||||
|                 :tax-data="tax" | ||||
|                 :taxes="item.taxes" | ||||
|                 :discounted-total="total" | ||||
|                 :total-tax="totalSimpleTax" | ||||
|                 :total="total" | ||||
|                 :currency="currency" | ||||
|                 @update="updateTax" | ||||
|                 @remove="removeTax" | ||||
|               /> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </td> | ||||
|   </tr> | ||||
| </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') | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     Tax, | ||||
|     ItemSelect | ||||
|   }, | ||||
|   mixins: [validationMixin], | ||||
|   props: { | ||||
|     itemData: { | ||||
|       type: Object, | ||||
|       default: null | ||||
|     }, | ||||
|     index: { | ||||
|       type: Number, | ||||
|       default: null | ||||
|     }, | ||||
|     type: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     }, | ||||
|     currency: { | ||||
|       type: [Object, String], | ||||
|       required: true | ||||
|     }, | ||||
|     taxPerItem: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     }, | ||||
|     discountPerItem: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     } | ||||
|   }, | ||||
|   data () { | ||||
|     return { | ||||
|       isClosePopup: false, | ||||
|       itemSelect: null, | ||||
|       item: {...this.itemData}, | ||||
|       maxDiscount: 0, | ||||
|       money: { | ||||
|         decimal: '.', | ||||
|         thousands: ',', | ||||
|         prefix: '$ ', | ||||
|         precision: 2, | ||||
|         masked: false | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters('item', [ | ||||
|       'items' | ||||
|     ]), | ||||
|     ...mapGetters('currency', [ | ||||
|       '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 | ||||
|         } | ||||
|       } else { | ||||
|         return this.defaultCurrencyForInput | ||||
|       } | ||||
|     }, | ||||
|     subtotal () { | ||||
|       return this.item.price * this.item.quantity | ||||
|     }, | ||||
|     discount: { | ||||
|       get: function () { | ||||
|         return this.item.discount | ||||
|       }, | ||||
|       set: function (newValue) { | ||||
|         if (this.item.discount_type === 'percentage') { | ||||
|           this.item.discount_val = (this.subtotal * newValue) / 100 | ||||
|         } else { | ||||
|           this.item.discount_val = newValue * 100 | ||||
|         } | ||||
|  | ||||
|         this.item.discount = newValue | ||||
|       } | ||||
|     }, | ||||
|     total () { | ||||
|       return this.subtotal - this.item.discount_val | ||||
|     }, | ||||
|     totalSimpleTax () { | ||||
|       return window._.sumBy(this.item.taxes, function (tax) { | ||||
|         if (!tax.compound_tax) { | ||||
|           return tax.amount | ||||
|         } | ||||
|  | ||||
|         return 0 | ||||
|       }) | ||||
|     }, | ||||
|     totalCompoundTax () { | ||||
|       return window._.sumBy(this.item.taxes, function (tax) { | ||||
|         if (tax.compound_tax) { | ||||
|           return tax.amount | ||||
|         } | ||||
|  | ||||
|         return 0 | ||||
|       }) | ||||
|     }, | ||||
|     totalTax () { | ||||
|       return this.totalSimpleTax + this.totalCompoundTax | ||||
|     }, | ||||
|     price: { | ||||
|       get: function () { | ||||
|         if (parseFloat(this.item.price) > 0) { | ||||
|           return this.item.price / 100 | ||||
|         } | ||||
|  | ||||
|         return this.item.price | ||||
|       }, | ||||
|       set: function (newValue) { | ||||
|         if (parseFloat(newValue) > 0) { | ||||
|           this.item.price = newValue * 100 | ||||
|           this.maxDiscount = this.item.price | ||||
|         } else { | ||||
|           this.item.price = newValue | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     item: { | ||||
|       handler: 'updateItem', | ||||
|       deep: true | ||||
|     }, | ||||
|     subtotal (newValue) { | ||||
|       if (this.item.discount_type === 'percentage') { | ||||
|         this.item.discount_val = (this.item.discount * newValue) / 100 | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   validations () { | ||||
|     return { | ||||
|       item: { | ||||
|         name: { | ||||
|           required | ||||
|         }, | ||||
|         quantity: { | ||||
|           required, | ||||
|           minValue: minValue(1), | ||||
|           maxLength: maxLength(10) | ||||
|         }, | ||||
|         price: { | ||||
|           required, | ||||
|           minValue: minValue(1), | ||||
|           maxLength: maxLength(10) | ||||
|         }, | ||||
|         discount_val: { | ||||
|           between: between(0, this.maxDiscount) | ||||
|         }, | ||||
|         description: { | ||||
|           maxLength: maxLength(255) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   created () { | ||||
|     window.hub.$on('checkItems', this.validateItem) | ||||
|     window.hub.$on('newItem', this.onSelectItem) | ||||
|   }, | ||||
|   methods: { | ||||
|     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.updateItem() | ||||
|     }, | ||||
|     removeTax (index) { | ||||
|       this.item.taxes.splice(index, 1) | ||||
|  | ||||
|       this.updateItem() | ||||
|     }, | ||||
|     taxWithPercentage ({ name, percent }) { | ||||
|       return `${name} (${percent}%)` | ||||
|     }, | ||||
|     searchVal (val) { | ||||
|       this.item.name = val | ||||
|     }, | ||||
|     deselectItem () { | ||||
|       this.item = {...EstimateStub, id: this.item.id} | ||||
|       this.$nextTick(() => { | ||||
|         this.$refs.itemSelect.$refs.baseSelect.$refs.search.focus() | ||||
|       }) | ||||
|     }, | ||||
|     onSelectItem (item) { | ||||
|       this.item.name = item.name | ||||
|       this.item.price = item.price | ||||
|       this.item.item_id = item.id | ||||
|       this.item.description = item.description | ||||
|  | ||||
|       // if (this.item.taxes.length) { | ||||
|       //   this.item.taxes = {...item.taxes} | ||||
|       // } | ||||
|     }, | ||||
|     selectFixed () { | ||||
|       if (this.item.discount_type === 'fixed') { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       this.item.discount_val = this.item.discount * 100 | ||||
|       this.item.discount_type = 'fixed' | ||||
|     }, | ||||
|     selectPercentage () { | ||||
|       if (this.item.discount_type === 'percentage') { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       this.item.discount_val = (this.subtotal * this.item.discount) / 100 | ||||
|  | ||||
|       this.item.discount_type = 'percentage' | ||||
|     }, | ||||
|     updateItem () { | ||||
|       this.$emit('update', { | ||||
|         '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] | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     removeItem () { | ||||
|       this.$emit('remove', this.index) | ||||
|     }, | ||||
|     validateItem () { | ||||
|       this.$v.item.$touch() | ||||
|  | ||||
|       if (this.item !== null) { | ||||
|         this.$emit('itemValidate', this.index, !this.$v.$invalid) | ||||
|       } else { | ||||
|         this.$emit('itemValidate', this.index, false) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										140
									
								
								resources/assets/js/views/estimates/ItemSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								resources/assets/js/views/estimates/ItemSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,140 @@ | ||||
| <template> | ||||
|   <div class="item-selector"> | ||||
|     <div v-if="item.item_id" class="selected-item"> | ||||
|       {{ item.name }} | ||||
|  | ||||
|       <span class="deselect-icon" @click="deselectItem"> | ||||
|         <font-awesome-icon icon="times-circle" /> | ||||
|       </span> | ||||
|     </div> | ||||
|     <base-select | ||||
|       v-else | ||||
|       ref="baseSelect" | ||||
|       v-model="itemSelect" | ||||
|       :options="items" | ||||
|       :show-labels="false" | ||||
|       :preserve-search="true" | ||||
|       :initial-search="item.name" | ||||
|       :invalid="invalid" | ||||
|       :placeholder="$t('estimates.item.select_an_item')" | ||||
|       label="name" | ||||
|       class="multi-select-item" | ||||
|       @value="onTextChange" | ||||
|       @select="(val) => $emit('select', val)" | ||||
|     > | ||||
|       <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> | ||||
|       </div> | ||||
|     </base-select> | ||||
|     <div class="item-description"> | ||||
|       <base-text-area | ||||
|         v-autoresize | ||||
|         v-model.trim="item.description" | ||||
|         :invalid-description="invalidDescription" | ||||
|         :placeholder="$t('estimates.item.type_item_description')" | ||||
|         type="text" | ||||
|         rows="1" | ||||
|         class="description-input" | ||||
|         @input="$emit('onDesriptionInput')" | ||||
|       /> | ||||
|       <div v-if="invalidDescription"> | ||||
|         <span class="text-danger">{{ $tc('validation.description_maxlength') }}</span> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapActions, mapGetters } from 'vuex' | ||||
| import { validationMixin } from 'vuelidate' | ||||
| const { maxLength } = require('vuelidate/lib/validators') | ||||
|  | ||||
| export default { | ||||
|   mixins: [validationMixin], | ||||
|   props: { | ||||
|     item: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|     }, | ||||
|     invalid: { | ||||
|       type: Boolean, | ||||
|       required: false, | ||||
|       default: false | ||||
|     }, | ||||
|     invalidDescription: { | ||||
|       type: Boolean, | ||||
|       required: false, | ||||
|       default: false | ||||
|     } | ||||
|   }, | ||||
|   data () { | ||||
|     return { | ||||
|       itemSelect: null, | ||||
|       loading: false | ||||
|     } | ||||
|   }, | ||||
|   validations () { | ||||
|     return { | ||||
|       item: { | ||||
|         description: { | ||||
|           maxLength: maxLength(255) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters('item', [ | ||||
|       'items' | ||||
|     ]) | ||||
|   }, | ||||
|   watch: { | ||||
|     invalidDescription (newValue) { | ||||
|       console.log(newValue) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions('modal', [ | ||||
|       'openModal' | ||||
|     ]), | ||||
|     ...mapActions('item', [ | ||||
|       'fetchItems' | ||||
|     ]), | ||||
|     async searchItems (search) { | ||||
|       let data = { | ||||
|         filter: { | ||||
|           name: search, | ||||
|           unit: '', | ||||
|           price: '' | ||||
|         }, | ||||
|         orderByField: '', | ||||
|         orderBy: '', | ||||
|         page: 1 | ||||
|       } | ||||
|  | ||||
|       this.loading = true | ||||
|  | ||||
|       await this.fetchItems(data) | ||||
|  | ||||
|       this.loading = false | ||||
|     }, | ||||
|     onTextChange (val) { | ||||
|       this.searchItems(val) | ||||
|  | ||||
|       this.$emit('search', val) | ||||
|     }, | ||||
|     openItemModal () { | ||||
|       this.openModal({ | ||||
|         'title': 'Add Item', | ||||
|         'componentName': 'ItemModal' | ||||
|       }) | ||||
|     }, | ||||
|     deselectItem () { | ||||
|       this.itemSelect = null | ||||
|       this.$emit('deselect') | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										168
									
								
								resources/assets/js/views/estimates/Tax.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								resources/assets/js/views/estimates/Tax.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,168 @@ | ||||
| <template> | ||||
|   <div class="tax-row"> | ||||
|     <div class="d-flex align-items-center tax-select"> | ||||
|       <label class="bd-highlight pr-2 mb-0" align="right"> | ||||
|         {{ $t('estimates.tax') }} | ||||
|       </label> | ||||
|       <base-select | ||||
|         v-model="selectedTax" | ||||
|         :options="filteredTypes" | ||||
|         :allow-empty="false" | ||||
|         :show-labels="false" | ||||
|         :custom-label="customLabel" | ||||
|         :placeholder="$t('general.select_a_tax')" | ||||
|         track-by="name" | ||||
|         label="name" | ||||
|         @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> | ||||
|         </div> | ||||
|       </base-select> <br> | ||||
|     </div> | ||||
|     <div class="text-right tax-amount"> | ||||
|       <div v-html="$utils.formatMoney(taxAmount, currency)" /> | ||||
|     </div> | ||||
|     <div class="remove-icon-wrapper"> | ||||
|       <font-awesome-icon | ||||
|         v-if="taxes.length && index !== taxes.length - 1" | ||||
|         class="remove-icon" | ||||
|         icon="trash-alt" | ||||
|         @click="removeTax" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapActions, mapGetters } from 'vuex' | ||||
|  | ||||
| export default { | ||||
|   props: { | ||||
|     index: { | ||||
|       type: Number, | ||||
|       required: true | ||||
|     }, | ||||
|     taxData: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|     }, | ||||
|     taxes: { | ||||
|       type: Array, | ||||
|       default: [] | ||||
|     }, | ||||
|     total: { | ||||
|       type: Number, | ||||
|       default: 0 | ||||
|     }, | ||||
|     totalTax: { | ||||
|       type: Number, | ||||
|       default: 0 | ||||
|     }, | ||||
|     currency: { | ||||
|       type: [Object, String], | ||||
|       required: true | ||||
|     } | ||||
|   }, | ||||
|   data () { | ||||
|     return { | ||||
|       tax: {...this.taxData}, | ||||
|       selectedTax: null | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ...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) | ||||
|  | ||||
|         if (found) { | ||||
|           taxType.$isDisabled = true | ||||
|         } else { | ||||
|           taxType.$isDisabled = false | ||||
|         } | ||||
|  | ||||
|         return taxType | ||||
|       }) | ||||
|     }, | ||||
|     taxAmount () { | ||||
|       if (this.tax.compound_tax && this.total) { | ||||
|         return ((this.total + this.totalTax) * this.tax.percent) / 100 | ||||
|       } | ||||
|  | ||||
|       if (this.total && this.tax.percent) { | ||||
|         return (this.total * this.tax.percent) / 100 | ||||
|       } | ||||
|  | ||||
|       return 0 | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     total: { | ||||
|       handler: 'updateTax' | ||||
|     }, | ||||
|     totalTax: { | ||||
|       handler: 'updateTax' | ||||
|     } | ||||
|   }, | ||||
|   created () { | ||||
|     if (this.taxData.tax_type_id > 0) { | ||||
|       this.selectedTax = this.taxTypes.find(_type => _type.id === this.taxData.tax_type_id) | ||||
|     } | ||||
|  | ||||
|     window.hub.$on('newTax', (val) => { | ||||
|       if (!this.selectedTax) { | ||||
|         this.selectedTax = val | ||||
|         this.onSelectTax(val) | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     this.updateTax() | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions('modal', [ | ||||
|       'openModal' | ||||
|     ]), | ||||
|     customLabel ({ name, percent }) { | ||||
|       return `${name} - ${percent}%` | ||||
|     }, | ||||
|     onSelectTax (val) { | ||||
|       this.tax.percent = val.percent | ||||
|       this.tax.tax_type_id = val.id | ||||
|       this.tax.compound_tax = val.compound_tax | ||||
|       this.tax.name = val.name | ||||
|  | ||||
|       this.updateTax() | ||||
|     }, | ||||
|     updateTax () { | ||||
|       if (this.tax.tax_type_id === 0) { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       this.$emit('update', { | ||||
|         'index': this.index, | ||||
|         'item': { | ||||
|           ...this.tax, | ||||
|           amount: this.taxAmount | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     removeTax () { | ||||
|       this.$emit('remove', this.index, this.tax) | ||||
|     }, | ||||
|     openTaxModal () { | ||||
|       this.openModal({ | ||||
|         'title': 'Add Tax', | ||||
|         'componentName': 'TaxTypeModal' | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										255
									
								
								resources/assets/js/views/estimates/View.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								resources/assets/js/views/estimates/View.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,255 @@ | ||||
| <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"> | ||||
|           <base-button | ||||
|             :loading="isRequestOnGoing" | ||||
|             :disabled="isRequestOnGoing" | ||||
|             :outline="true" | ||||
|             color="theme" | ||||
|             @click="onMarkAsSent" | ||||
|           > | ||||
|             {{ $t('estimates.mark_as_sent') }} | ||||
|           </base-button> | ||||
|         </div> | ||||
|         <v-dropdown :close-on-select="false" align="left" class="filter-container"> | ||||
|           <a slot="activator" href="#"> | ||||
|             <base-button color="theme"> | ||||
|               <font-awesome-icon icon="ellipsis-h" /> | ||||
|             </base-button> | ||||
|           </a> | ||||
|           <v-dropdown-item> | ||||
|             <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 | ||||
|           v-model="searchData.searchText" | ||||
|           :placeholder="$t('general.search')" | ||||
|           input-class="inv-search" | ||||
|           icon="search" | ||||
|           type="text" | ||||
|           align-icon="right" | ||||
|           @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-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 class="inv-button inv-filter-sorting-btn" color="default" size="medium" @click="sortData"> | ||||
|             <font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" /> | ||||
|             <font-awesome-icon v-else icon="sort-amount-down" /> | ||||
|           </base-button> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="side-content"> | ||||
|         <router-link | ||||
|           v-for="(estimate,index) in estimates" | ||||
|           :to="`/admin/estimates/${estimate.id}/view`" | ||||
|           :key="index" | ||||
|           class="side-estimate" | ||||
|         > | ||||
|           <div class="left"> | ||||
|             <div class="inv-name">{{ estimate.user.name }}</div> | ||||
|             <div class="inv-number">{{ estimate.estimate_number }}</div> | ||||
|             <div :class="'est-status-'+estimate.status.toLowerCase()" class="inv-status">{{ estimate.status }}</div> | ||||
|           </div> | ||||
|           <div class="right"> | ||||
|             <div class="inv-amount" v-html="$utils.formatMoney(estimate.total, estimate.user.currency)" /> | ||||
|             <div class="inv-date">{{ estimate.formattedEstimateDate }}</div> | ||||
|           </div> | ||||
|         </router-link> | ||||
|         <p v-if="!estimates.length" class="no-result"> | ||||
|           {{ $t('estimates.no_matching_estimates') }} | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="estimate-view-page-container"> | ||||
|       <iframe :src="`${shareableLink}`" class="frame-style"/> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| import { mapActions } from 'vuex' | ||||
| const _ = require('lodash') | ||||
| export default { | ||||
|   data () { | ||||
|     return { | ||||
|       id: null, | ||||
|       count: null, | ||||
|       estimates: [], | ||||
|       estimate: null, | ||||
|       currency: null, | ||||
|       shareableLink: null, | ||||
|       searchData: { | ||||
|         orderBy: null, | ||||
|         orderByField: null, | ||||
|         searchText: null | ||||
|       }, | ||||
|       isRequestOnGoing: false, | ||||
|       isSearching: false | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     getOrderBy () { | ||||
|       if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) { | ||||
|         return true | ||||
|       } | ||||
|       return false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     '$route.params.id' (val) { | ||||
|       this.fetchEstimate() | ||||
|     } | ||||
|   }, | ||||
|   mounted () { | ||||
|     this.loadEstimates() | ||||
|     this.onSearched = _.debounce(this.onSearched, 500) | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions('estimate', [ | ||||
|       'fetchEstimates', | ||||
|       'fetchViewEstimate', | ||||
|       'getRecord', | ||||
|       'searchEstimate', | ||||
|       'markAsSent', | ||||
|       'deleteEstimate', | ||||
|       'selectEstimate' | ||||
|     ]), | ||||
|     async loadEstimates () { | ||||
|       let response = await this.fetchEstimates() | ||||
|       if (response.data) { | ||||
|         this.estimates = response.data.estimates.data | ||||
|       } | ||||
|       this.fetchEstimate() | ||||
|     }, | ||||
|     async onSearched () { | ||||
|       let data = '' | ||||
|       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) { | ||||
|         data += `orderBy=${this.searchData.orderBy}&` | ||||
|       } | ||||
|  | ||||
|       if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) { | ||||
|         data += `orderByField=${this.searchData.orderByField}` | ||||
|       } | ||||
|       this.isSearching = true | ||||
|       let response = await this.searchEstimate(data) | ||||
|       this.isSearching = false | ||||
|       if (response.data) { | ||||
|         this.estimates = response.data.estimates.data | ||||
|       } | ||||
|     }, | ||||
|     async fetchEstimate () { | ||||
|       let estimate = await this.fetchViewEstimate(this.$route.params.id) | ||||
|  | ||||
|       if (estimate.data) { | ||||
|         this.estimate = estimate.data.estimate | ||||
|         this.shareableLink = estimate.data.shareable_link | ||||
|         this.currency = estimate.data.estimate.user.currency | ||||
|       } | ||||
|     }, | ||||
|     sortData () { | ||||
|       if (this.searchData.orderBy === 'asc') { | ||||
|         this.searchData.orderBy = 'desc' | ||||
|         this.onSearched() | ||||
|         return true | ||||
|       } | ||||
|       this.searchData.orderBy = 'asc' | ||||
|       this.onSearched() | ||||
|       return true | ||||
|     }, | ||||
|     async onMarkAsSent () { | ||||
|       this.isRequestOnGoing = true | ||||
|       let response = await this.markAsSent({id: this.estimate.id}) | ||||
|       this.isRequestOnGoing = false | ||||
|       if (response.data) { | ||||
|         window.toastr['success'](this.$tc('estimates.mark_as_sent')) | ||||
|       } | ||||
|     }, | ||||
|     async removeEstimate (id) { | ||||
|       this.selectEstimate([parseInt(id)]) | ||||
|       this.id = id | ||||
|       swal({ | ||||
|         title: 'Deleted', | ||||
|         text: 'you will not be able to recover this estimate!', | ||||
|         icon: 'error', | ||||
|         buttons: true, | ||||
|         dangerMode: true | ||||
|       }).then(async (willDelete) => { | ||||
|         if (willDelete) { | ||||
|           let request = await this.deleteEstimate(this.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