init crater

This commit is contained in:
Mohit Panjwani
2019-11-11 12:16:00 +05:30
commit bdf2ba51d6
668 changed files with 158503 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>