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,711 @@
<template>
<div class="invoice-create-page main-content">
<form v-if="!initLoading" action="" @submit.prevent="submitInvoiceData">
<div class="page-header">
<h3 v-if="$route.name === 'invoices.edit'" class="page-title">{{ $t('invoices.edit_invoice') }}</h3>
<h3 v-else class="page-title">{{ $t('invoices.new_invoice') }} </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/invoices">{{ $tc('invoices.invoice', 2) }}</router-link></li>
<li v-if="$route.name === 'invoices.edit'" class="breadcrumb-item">{{ $t('invoices.edit_invoice') }}</li>
<li v-else class="breadcrumb-item">{{ $t('invoices.new_invoice') }}</li>
</ol>
<div class="page-actions row">
<a v-if="$route.name === 'invoices.edit'" :href="`/invoices/pdf/${newInvoice.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('invoices.save_invoice') }}
</base-button>
</div>
</div>
<div class="row invoice-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" 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" 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.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('validation.required') }}
</p>
</div>
</div>
<customer-select-popup type="invoice" />
</base-popup>
</div>
<div class="col invoice-input">
<div class="row mb-3">
<div class="col">
<label>{{ $tc('invoices.invoice',1) }} {{ $t('invoices.date') }}<span class="text-danger"> * </span></label>
<base-date-picker
v-model="newInvoice.invoice_date"
:calendar-button="true"
calendar-button-icon="calendar"
@change="$v.newInvoice.invoice_date.$touch()"
/>
<span v-if="$v.newInvoice.invoice_date.$error && !$v.newInvoice.invoice_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
<div class="col">
<label>{{ $t('invoices.due_date') }}<span class="text-danger"> * </span></label>
<base-date-picker
v-model="newInvoice.due_date"
:invalid="$v.newInvoice.due_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
@change="$v.newInvoice.due_date.$touch()"
/>
<span v-if="$v.newInvoice.due_date.$error && !$v.newInvoice.due_date.required" class="text-danger mt-1"> {{ $t('validation.required') }}</span>
</div>
</div>
<div class="row mt-4">
<div class="col">
<label>{{ $t('invoices.invoice_number') }}<span class="text-danger"> * </span></label>
<base-input
:invalid="$v.newInvoice.invoice_number.$error"
:read-only="true"
v-model="newInvoice.invoice_number"
icon="hashtag"
@input="$v.newInvoice.invoice_number.$touch()"
/>
<span v-show="$v.newInvoice.invoice_number.$error && !$v.newInvoice.invoice_number.required" class="text-danger mt-1"> {{ $tc('validation.required') }} </span>
</div>
<div class="col">
<label>{{ $t('invoices.ref_number') }}</label>
<base-input
v-model="newInvoice.reference_number"
:invalid="$v.newInvoice.reference_number.$error"
icon="hashtag"
type="number"
@input="$v.newInvoice.reference_number.$touch()"
/>
<div v-if="$v.newInvoice.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('invoices.item.quantity') }}
</span>
</th>
<th class="text-left">
<span class="column-heading">
{{ $t('invoices.item.price') }}
</span>
</th>
<th v-if="discountPerItem === 'YES'" class="text-right">
<span class="column-heading">
{{ $t('invoices.item.discount') }}
</span>
</th>
<th class="text-right">
<span class="column-heading amount-heading">
{{ $t('invoices.item.amount') }}
</span>
</th>
</tr>
</thead>
<draggable v-model="newInvoice.items" class="item-body" tag="tbody" handle=".handle">
<invoice-item
v-for="(item, index) in newInvoice.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('invoices.add_item') }}
</div>
<div class="invoice-foot">
<div>
<label>{{ $t('invoices.notes') }}</label>
<base-text-area
v-model="newInvoice.notes"
rows="3"
cols="50"
@input="$v.newInvoice.notes.$touch()"
/>
<div v-if="$v.newInvoice.notes.$error">
<span v-if="!$v.newInvoice.notes.maxLength" class="text-danger">{{ $t('validation.notes_maxlength') }}</span>
</div>
<label class="mt-3 mb-1 d-block">{{ $t('invoices.invoice_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('invoices.template') }} {{ getTemplateId }} </span>
</base-button>
</div>
<div class="invoice-total">
<div class="section">
<label class="invoice-label">{{ $t('invoices.sub_total') }}</label>
<label class="invoice-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="invoice-label">{{ tax.name }} - {{ tax.percent }}% </label>
<label class="invoice-amount">
<div v-html="$utils.formatMoney(tax.amount, currency)" />
</label>
</div>
<div v-if="discountPerItem === 'NO' || discountPerItem === null" class="section mt-2">
<label class="invoice-label">{{ $t('invoices.discount') }}</label>
<div
class="btn-group discount-drop-down"
role="group"
>
<base-input
v-model="discount"
:invalid="$v.newInvoice.discount_val.$error"
input-class="item-discount"
@input="$v.newInvoice.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"
>
{{ newInvoice.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 newInvoice.taxes"
:index="index"
:total="subtotalWithDiscount"
:key="tax.id"
:tax="tax"
:taxes="newInvoice.taxes"
:currency="currency"
:total-tax="totalSimpleTax"
@remove="removeInvoiceTax"
@update="updateTax"
/>
</div>
<base-popup v-if="taxPerItem === 'NO' || taxPerItem === null" ref="taxModal" class="tax-selector">
<div slot="activator" class="float-right">
+ {{ $t('invoices.add_tax') }}
</div>
<tax-select-popup :taxes="newInvoice.taxes" @select="onSelectTax"/>
</base-popup>
<div class="section border-top mt-3">
<label class="invoice-label">{{ $t('invoices.total') }} {{ $t('invoices.amount') }}:</label>
<label class="invoice-amount total">
<div v-html="$utils.formatMoney(total, currency)" />
</label>
</div>
</div>
</div>
</form>
<base-loader v-else />
</div>
</template>
<script>
import draggable from 'vuedraggable'
import MultiSelect from 'vue-multiselect'
import InvoiceItem from './Item'
import InvoiceStub from '../../stub/invoice'
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 './InvoiceTax'
const { required, between, maxLength } = require('vuelidate/lib/validators')
export default {
components: {
InvoiceItem,
MultiSelect,
Tax,
draggable
},
mixins: [validationMixin],
data () {
return {
newInvoice: {
invoice_date: null,
due_date: null,
invoice_number: null,
user_id: null,
invoice_template_id: 1,
sub_total: null,
total: null,
tax: null,
notes: null,
discount_type: 'fixed',
discount_val: 0,
discount: 0,
reference_number: null,
items: [{
...InvoiceStub,
id: Guid.raw(),
taxes: [{...TaxStub, id: Guid.raw()}]
}],
taxes: []
},
customers: [],
itemList: [],
invoiceTemplates: [],
selectedCurrency: '',
taxPerItem: null,
discountPerItem: null,
initLoading: false,
isLoading: false,
maxDiscount: 0
}
},
validations () {
return {
newInvoice: {
invoice_date: {
required
},
due_date: {
required
},
invoice_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('invoice', [
'getTemplateId',
'selectedCustomer'
]),
currency () {
return this.selectedCurrency
},
subtotalWithDiscount () {
return this.subtotal - this.newInvoice.discount_val
},
total () {
return this.subtotalWithDiscount + this.totalTax
},
subtotal () {
return this.newInvoice.items.reduce(function (a, b) {
return a + b['total']
}, 0)
},
discount: {
get: function () {
return this.newInvoice.discount
},
set: function (newValue) {
if (this.newInvoice.discount_type === 'percentage') {
this.newInvoice.discount_val = (this.subtotal * newValue) / 100
} else {
this.newInvoice.discount_val = newValue * 100
}
this.newInvoice.discount = newValue
}
},
totalSimpleTax () {
return window._.sumBy(this.newInvoice.taxes, function (tax) {
if (!tax.compound_tax) {
return tax.amount
}
return 0
})
},
totalCompoundTax () {
return window._.sumBy(this.newInvoice.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.newInvoice.items, function (tax) {
return tax.tax
})
},
allTaxes () {
let taxes = []
this.newInvoice.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.newInvoice.discount_type === 'percentage') {
this.newInvoice.discount_val = (this.newInvoice.discount * newValue) / 100
}
}
},
created () {
this.loadData()
this.fetchInitialItems()
this.resetSelectedCustomer()
window.hub.$on('newTax', this.onSelectTax)
},
methods: {
...mapActions('modal', [
'openModal'
]),
...mapActions('invoice', [
'addInvoice',
'fetchCreateInvoice',
'fetchInvoice',
'resetSelectedCustomer',
'selectCustomer',
'updateInvoice'
]),
...mapActions('item', [
'fetchItems'
]),
isEmpty (obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
return false
}
}
return true
},
selectFixed () {
if (this.newInvoice.discount_type === 'fixed') {
return
}
this.newInvoice.discount_val = this.newInvoice.discount * 100
this.newInvoice.discount_type = 'fixed'
},
selectPercentage () {
if (this.newInvoice.discount_type === 'percentage') {
return
}
this.newInvoice.discount_val = (this.subtotal * this.newInvoice.discount) / 100
this.newInvoice.discount_type = 'percentage'
},
updateTax (data) {
Object.assign(this.newInvoice.taxes[data.index], {...data.item})
},
async fetchInitialItems () {
await this.fetchItems({
filter: {},
orderByField: '',
orderBy: ''
})
},
async loadData () {
if (this.$route.name === 'invoices.edit') {
this.initLoading = true
let response = await this.fetchInvoice(this.$route.params.id)
if (response.data) {
this.selectCustomer(response.data.invoice.user_id)
this.newInvoice = response.data.invoice
this.discountPerItem = response.data.discount_per_item
this.taxPerItem = response.data.tax_per_item
this.selectedCurrency = this.defaultCurrency
this.invoiceTemplates = response.data.invoiceTemplates
}
this.initLoading = false
return
}
this.initLoading = true
let response = await this.fetchCreateInvoice()
if (response.data) {
this.discountPerItem = response.data.discount_per_item
this.taxPerItem = response.data.tax_per_item
this.selectedCurrency = this.defaultCurrency
this.invoiceTemplates = response.data.invoiceTemplates
let today = new Date()
this.newInvoice.invoice_date = moment(today).toString()
this.newInvoice.due_date = moment(today).add(7, 'days').toString()
this.newInvoice.invoice_number = response.data.nextInvoiceNumber
this.itemList = response.data.items
}
this.initLoading = false
},
removeCustomer () {
this.resetSelectedCustomer()
},
openTemplateModal () {
this.openModal({
'title': 'Choose a template',
'componentName': 'InvoiceTemplate',
'data': this.invoiceTemplates
})
},
addItem () {
this.newInvoice.items.push({...InvoiceStub, id: Guid.raw(), taxes: [{...TaxStub, id: Guid.raw()}]})
},
removeItem (index) {
this.newInvoice.items.splice(index, 1)
},
updateItem (data) {
Object.assign(this.newInvoice.items[data.index], {...data.item})
},
submitInvoiceData () {
if (!this.checkValid()) {
return false
}
this.isLoading = true
let data = {
...this.newInvoice,
invoice_date: moment(this.newInvoice.invoice_date).format('DD/MM/YYYY'),
due_date: moment(this.newInvoice.due_date).format('DD/MM/YYYY'),
sub_total: this.subtotal,
total: this.total,
tax: this.totalTax,
user_id: null,
invoice_template_id: this.getTemplateId
}
if (this.selectedCustomer != null) {
data.user_id = this.selectedCustomer.id
}
if (this.$route.name === 'invoices.edit') {
this.submitUpdate(data)
return
}
this.submitSave(data)
},
submitSave (data) {
this.addInvoice(data).then((res) => {
if (res.data) {
window.toastr['success'](this.$t('invoices.created_message'))
this.$router.push('/admin/invoices')
}
this.isLoading = false
}).catch((err) => {
this.isLoading = false
console.log(err)
})
},
submitUpdate (data) {
this.updateInvoice(data).then((res) => {
this.isLoading = false
if (res.data.success) {
window.toastr['success'](this.$t('invoices.updated_message'))
this.$router.push('/admin/invoices')
}
if (res.data.error === 'invalid_due_amount') {
window.toastr['error'](this.$t('invoices.invalid_due_amount_message'))
}
}).catch((err) => {
this.isLoading = false
console.log(err)
})
},
checkItemsData (index, isValid) {
this.newInvoice.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.newInvoice.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()
},
removeInvoiceTax (index) {
this.newInvoice.taxes.splice(index, 1)
},
checkValid () {
this.$v.newInvoice.$touch()
this.$v.selectedCustomer.$touch()
window.hub.$emit('checkItems')
let isValid = true
this.newInvoice.items.forEach((item) => {
if (!item.valid) {
isValid = false
}
})
if (!this.$v.selectedCustomer.$invalid && this.$v.newInvoice.$invalid === false && isValid === true) {
return true
}
return false
}
}
}
</script>

View File

@ -0,0 +1,589 @@
<template>
<div class="invoice-create-page main-content">
<form action="" @submit.prevent="submitInvoiceData">
<div class="page-header">
<h3 class="page-title">{{ $t('invoices.new_invoice') }}</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/invoices">{{ $tc('invoices.invoice', 2) }}</router-link></li>
<li class="breadcrumb-item">{{ $t('invoices.new_invoice') }}</li>
</ol>
<div class="page-actions row">
<base-button class="mr-3" outline color="theme">
{{ $t('general.download_pdf') }}
</base-button>
<base-button
:loading="isLoading"
icon="save"
color="theme"
type="submit">
{{ $t('invoices.save_invoice') }}
</base-button>
</div>
</div>
<div class="row invoice-input-group">
<div class="col-md-5">
<div
v-if="selectedCustomer"
class="show-customer"
>
<div class="row p-2">
<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>{{ selectedCustomer.billing_address.name }}</label>
<label>{{ selectedCustomer.billing_address.address_street_1 }}</label>
<label>{{ selectedCustomer.billing_address.address_street_2 }}</label>
<label>
{{ selectedCustomer.billing_address.city.name }}, {{ selectedCustomer.billing_address.state.name }} {{ selectedCustomer.billing_address.zip }}
</label>
<label>{{ selectedCustomer.billing_address.country.name }}</label>
<label>{{ 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>{{ selectedCustomer.shipping_address.name }}</label>
<label>{{ selectedCustomer.shipping_address.address_street_1 }}</label>
<label>{{ selectedCustomer.shipping_address.address_street_2 }}</label>
<label v-show="selectedCustomer.shipping_address.city">
{{ selectedCustomer.shipping_address.city.name }}, {{ selectedCustomer.shipping_address.state.name }} {{ selectedCustomer.shipping_address.zip }}
</label>
<label class="country">{{ selectedCustomer.shipping_address.country.name }}</label>
<label class="phone">{{ selectedCustomer.shipping_address.phone }}</label>
</div>
</div>
</div>
</div>
<div class="customer-content">
<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">
<div slot="activator" class="add-customer-action">
<font-awesome-icon icon="user" class="customer-icon"/>
<label>{{ $t('customers.new_customer') }}<span class="text-danger"> * </span></label>
</div>
<customer-select />
</base-popup>
</div>
<div class="col invoice-input">
<div class="row mb-3">
<div class="col">
<label>{{ $t('invoices.invoice_date') }}<span class="text-danger"> * </span></label>
<base-date-picker
v-model="newInvoice.invoice_date"
:calendar-button="true"
calendar-button-icon="calendar"
@change="$v.newInvoice.invoice_date.$touch()"
/>
<span v-if="$v.newInvoice.invoice_date.$error && !$v.newInvoice.invoice_date.required" class="text-danger"> {{ $t('validation.required') }} </span>
</div>
<div class="col">
<label>{{ $t('invoices.due_date') }}<span class="text-danger"> * </span></label>
<base-date-picker
v-model="newInvoice.due_date"
:invalid="$v.newInvoice.due_date.$error"
:calendar-button="true"
calendar-button-icon="calendar"
@change="$v.newInvoice.due_date.$touch()"
/>
<span v-if="$v.newInvoice.due_date.$error && !$v.newInvoice.due_date.required" class="text-danger mt-1"> {{ $t('validation.required') }}</span>
</div>
</div>
<div class="row mt-4">
<div class="col">
<label>{{ $t('invoices.invoice_number') }}<span class="text-danger"> * </span></label>
<base-input
:invalid="$v.newInvoice.invoice_number.$error"
:read-only="true"
v-model="newInvoice.invoice_number"
icon="hashtag"
@input="$v.newInvoice.invoice_number.$touch()"
/>
<span v-show="$v.newInvoice.invoice_number.$error && !$v.newInvoice.invoice_number.required" class="text-danger mt-1"> {{ $tc('validation.required') }} </span>
</div>
<div class="col">
<label>{{ $t('invoices.ref_number') }}</label>
<base-input icon="hashtag" type="number"/>
</div>
</div>
</div>
</div>
<table class="item-table">
<colgroup>
<col style="width: 50%;">
<col style="width: 10%;">
<col style="width: 10%;">
<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('invoices.item.quantity') }}
</span>
</th>
<th class="text-left">
<span class="column-heading">
{{ $t('invoices.item.price') }}
</span>
</th>
<th v-if="discountPerItem === 'YES'" class="text-right">
<span class="column-heading">
{{ $t('invoices.item.discount') }}
</span>
</th>
<th class="text-right">
<span class="column-heading amount-heading">
{{ $t('invoices.item.amount') }}
</span>
</th>
</tr>
</thead>
<tbody class="item-body">
<invoice-item
v-for="(item, index) in newInvoice.items"
:key="'inv-item-' + item.id"
:index="index"
:item-data="item"
:currency="currency"
:tax-per-item="taxPerItem"
:discount-per-item="discountPerItem"
@remove="removeItem"
@update="updateItem"
@itemValidate="checkItemsData"
/>
</tbody>
</table>
<div class="add-item-action" @click="addItem">
<font-awesome-icon icon="shopping-basket" class="mr-2"/>
{{ $t('invoices.add_item') }}
</div>
<div class="invoice-foot">
<div>
<label>{{ $t('invoices.notes') }}</label>
<base-text-area
v-model="newInvoice.notes"
rows="3"
cols="50"
/>
<label class="mt-3 mb-1 d-block">{{ $t('invoices.invoice_template') }} <span class="text-danger"> * </span></label>
<base-button class="btn-template" icon="pencil-alt" right-icon @click="openTemplateModal" >
<span class="mr-4"> {{ $t('invoices.invoice_template') }} {{ getTemplateId }} </span>
</base-button>
</div>
<div class="invoice-total">
<div class="section">
<label class="invoice-label">{{ $t('invoices.sub_total') }}</label>
<label class="invoice-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="invoice-label">{{ tax.name }} - {{ tax.percent }}% </label>
<label class="invoice-amount">
<div v-html="$utils.formatMoney(tax.amount, currency)" />
</label>
</div>
<div v-if="discountPerItem === 'NO' || discountPerItem === null" class="section mt-2">
<label class="invoice-label">{{ $t('invoices.discount') }}</label>
<div
class="btn-group discount-drop-down"
role="group"
>
<base-input
v-model="discount"
input-class="item-discount"
/>
<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"
>
{{ newInvoice.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 newInvoice.taxes"
:index="index"
:total="subtotalWithDiscount"
:key="tax.taxKey"
:tax="tax"
:taxes="newInvoice.taxes"
:currency="currency"
:total-tax="totalSimpleTax"
@remove="removeInvoiceTax"
@update="updateTax"
/>
</div>
<base-popup v-if="taxPerItem === 'NO' || taxPerItem === null" ref="taxModal" class="tax-selector">
<div slot="activator" class="float-right">
+ {{ $t('invoices.add_tax') }}
</div>
<tax-select @select="onSelectTax"/>
</base-popup>
<div class="section border-top mt-3">
<label class="invoice-label">{{ $t('invoices.total') }} {{ $t('invoices.amount') }}:</label>
<label class="invoice-amount total">
<div v-html="$utils.formatMoney(total, currency)" />
</label>
</div>
</div>
</div>
</form>
</div>
</template>
<script>
import MultiSelect from 'vue-multiselect'
import InvoiceItem from './Item'
import InvoiceStub from '../../stub/invoice'
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 './InvoiceTax'
const { required } = require('vuelidate/lib/validators')
export default {
components: {
InvoiceItem,
MultiSelect,
Tax
},
mixins: [validationMixin],
data () {
return {
newInvoice: {
invoice_date: null,
due_date: null,
invoice_number: null,
user_id: null,
invoice_template_id: 1,
sub_total: null,
total: null,
tax: null,
notes: null,
discount_type: 'fixed',
discount_val: 0,
discount: 0,
items: [{
...InvoiceStub,
id: 1
}],
taxes: []
},
invoiceTemplates: [],
selectedCurrency: '',
newItem: {
...InvoiceStub
},
taxPerItem: null,
discountPerItem: null,
isLoading: false
}
},
validations: {
newInvoice: {
invoice_date: {
required
},
due_date: {
required
},
invoice_number: {
required
}
}
},
computed: {
...mapGetters('currency', [
'defaultCurrency'
]),
currency () {
return this.selectedCurrency
},
subtotalWithDiscount () {
return this.subtotal - this.newInvoice.discount_val
},
total () {
return this.subtotalWithDiscount + this.totalTax
},
subtotal () {
return this.newInvoice.items.reduce(function (a, b) {
return a + b['total']
}, 0)
},
discount: {
get: function () {
return this.newInvoice.discount
},
set: function (newValue) {
if (this.newInvoice.discount_type === 'percentage') {
this.newInvoice.discount_val = (this.subtotal * newValue) / 100
} else {
this.newInvoice.discount_val = newValue * 100
}
this.newInvoice.discount = newValue
}
},
totalSimpleTax () {
return window._.sumBy(this.newInvoice.taxes, function (tax) {
if (!tax.compound_tax) {
return tax.amount
}
return 0
})
},
totalCompoundTax () {
return window._.sumBy(this.newInvoice.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.newInvoice.items, function (tax) {
return tax.totalTax
})
},
allTaxes () {
let taxes = []
this.newInvoice.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
},
...mapGetters('customer', [
'selectedCustomer'
]),
...mapGetters('invoice', [
'getTemplateId'
]),
...mapGetters('general', [
'itemDiscount'
])
},
watch: {
selectedCustomer (newVal) {
if (newVal.currency !== null) {
this.selectedCurrency = newVal.currency
} else {
this.selectedCurrency = this.defaultCurrency
}
},
subtotal (newValue) {
if (this.newInvoice.discount_type === 'percentage') {
this.newInvoice.discount_val = (this.newInvoice.discount * newValue) / 100
}
}
},
mounted () {
this.$nextTick(() => {
this.loadData()
})
},
methods: {
...mapActions('modal', [
'openModal'
]),
...mapActions('customer', [
'resetSelectedCustomer'
]),
...mapActions('taxType', {
loadTaxTypes: 'indexLoadData'
}),
...mapActions('invoice', [
'addInvoice',
'fetchInvoice'
]),
selectFixed () {
if (this.newInvoice.discount_type === 'fixed') {
return
}
this.newInvoice.discount_val = this.newInvoice.discount * 100
this.newInvoice.discount_type = 'fixed'
},
selectPercentage () {
if (this.newInvoice.discount_type === 'percentage') {
return
}
this.newInvoice.discount_val = (this.subtotal * this.newInvoice.discount) / 100
this.newInvoice.discount_type = 'percentage'
},
updateTax (data) {
Object.assign(this.newInvoice.taxes[data.index], {...data.item})
},
async loadData () {
let response = await this.fetchInvoice(this.$route.params.id)
this.loadTaxTypes()
if (response.data) {
this.newInvoice = response.data.invoice
this.discountPerItem = response.data.discount_per_item
this.taxPerItem = response.data.tax_per_item
this.selectedCurrency = this.defaultCurrency
this.invoiceTemplates = response.data.invoiceTemplates
}
},
removeCustomer () {
this.resetSelectedCustomer()
},
openTemplateModal () {
this.openModal({
'title': 'Choose a template',
'componentName': 'InvoiceTemplate',
'data': this.invoiceTemplates
})
},
addItem () {
this.newInvoice.items.push({...this.newItem, id: (this.newInvoice.items.length + 1)})
},
removeItem (index) {
this.newInvoice.items.splice(index, 1)
},
updateItem (data) {
Object.assign(this.newInvoice.items[data.index], {...data.item})
},
async submitInvoiceData () {
if (!this.checkValid()) {
return false
}
this.isLoading = true
let data = {
...this.newInvoice,
invoice_date: moment(this.newInvoice.invoice_date).format('DD/MM/YYYY'),
due_date: moment(this.newInvoice.due_date).format('DD/MM/YYYY'),
sub_total: this.subtotal,
total: this.total,
tax: this.totalTax,
user_id: null,
invoice_template_id: this.getTemplateId
}
if (this.selectedCustomer != null) {
data.user_id = this.selectedCustomer.id
}
let response = await this.addInvoice(data)
if (response.data) {
window.toastr['success'](this.$t('invoices.created_message'))
this.isLoading = false
this.$route.push('/admin/invoices')
}
},
checkItemsData (index, isValid) {
this.newInvoice.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.newInvoice.taxes.push({
...TaxStub,
taxKey: Guid.raw(),
name: selectedTax.name,
percent: selectedTax.percent,
compound_tax: selectedTax.compound_tax,
tax_type_id: selectedTax.id,
amount
})
this.$refs.taxModal.close()
},
removeInvoiceTax (index) {
this.newInvoice.taxes.splice(index, 1)
},
checkValid () {
this.$v.newInvoice.$touch()
window.hub.$emit('checkItems')
let isValid = true
this.newInvoice.items.forEach((item) => {
if (!item.valid) {
isValid = false
}
})
if (this.$v.newInvoice.$invalid === false && isValid === true) {
return true
}
return false
}
}
}
</script>

View File

@ -0,0 +1,540 @@
<template>
<div class="invoice-index-page invoices main-content">
<div class="page-header">
<h3 class="page-title"> {{ $t('invoices.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('invoices.invoice', 2) }}
</router-link>
</li>
</ol>
<div class="page-actions row">
<div class="col-xs-2 mr-4">
<base-button
v-show="totalInvoices || 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="/admin/invoices/create">
<base-button size="large" icon="plus" color="theme">
{{ $t('invoices.new_invoice') }}
</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('invoices.status') }}</label>
<base-select
v-model="filters.status"
:options="status"
:group-select="false"
:searchable="true"
:show-labels="false"
:placeholder="$t('general.select_a_status')"
group-values="options"
group-label="label"
track-by="name"
label="name"
@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-invoice">
<label>{{ $t('invoices.invoice_number') }}</label>
<base-input
v-model="filters.invoice_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('invoices.no_invoices') }}</label>
</div>
<div class="row">
<label class="description col mt-1" align="center">{{ $t('invoices.list_of_invoices') }}</label>
</div>
<div class="btn-container">
<base-button
:outline="true"
color="theme"
class="mt-3"
size="large"
@click="$router.push('invoices/create')"
>
{{ $t('invoices.new_invoice') }}
</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>{{ invoices.length }}</b> {{ $t('general.of') }} <b>{{ totalInvoices }}</b></p>
<!-- Tabs -->
<ul class="tabs">
<li class="tab" @click="getStatus('UNPAID')">
<a :class="['tab-link', {'a-active': filters.status.value === 'UNPAID'}]" href="#" >{{ $t('general.due') }}</a>
</li>
<li class="tab" @click="getStatus('DRAFT')">
<a :class="['tab-link', {'a-active': filters.status.value === 'DRAFT'}]" href="#">{{ $t('general.draft') }}</a>
</li>
<li class="tab" @click="getStatus('')">
<a :class="['tab-link', {'a-active': filters.status.value === '' || filters.status.value === null || filters.status.value !== 'DRAFT' && filters.status.value !== 'UNPAID'}]" href="#">{{ $t('general.all') }}</a>
</li>
</ul>
<transition name="fade">
<v-dropdown v-if="selectedInvoices.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="removeMultipleInvoices">
<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"
type="checkbox"
class="custom-control-input"
@change="selectAllInvoices"
>
<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('invoices.date')"
sort-as="invoice_date"
show="formattedInvoiceDate"
/>
<table-column
:label="$t('invoices.customer')"
width="20%"
show="name"
/>
<table-column
:label="$t('invoices.status')"
sort-as="status"
>
<template slot-scope="row" >
<span> {{ $t('invoices.status') }}</span>
<span :class="'inv-status-'+row.status.toLowerCase()">{{ (row.status != 'PARTIALLY_PAID')? row.status : row.status.replace('_', ' ') }}</span>
</template>
</table-column>
<table-column
:label="$t('invoices.paid_status')"
sort-as="paid_status"
>
<template slot-scope="row">
<span>{{ $t('invoices.paid_status') }}</span>
<span :class="'inv-status-'+row.paid_status.toLowerCase()">{{ (row.paid_status != 'PARTIALLY_PAID')? row.paid_status : row.paid_status.replace('_', ' ') }}</span>
</template>
</table-column>
<table-column
:label="$t('invoices.number')"
show="invoice_number"
/>
<table-column
:label="$t('invoices.amount_due')"
sort-as="due_amount"
>
<template slot-scope="row">
<span>{{ $t('invoices.amount_due') }}</span>
<div v-html="$utils.formatMoney(row.due_amount, row.user.currency)"/>
</template>
</table-column>
<table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown no-click"
>
<template slot-scope="row">
<span>{{ $t('invoices.action') }}</span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<router-link :to="{path: `invoices/${row.id}/edit`}" class="dropdown-item">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon"/>
{{ $t('general.edit') }}
</router-link>
<router-link :to="{path: `invoices/${row.id}/view`}" class="dropdown-item">
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
{{ $t('invoices.view') }}
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<a class="dropdown-item" href="#" @click="sendInvoice(row.id)" >
<font-awesome-icon icon="paper-plane" class="dropdown-item-icon" />
{{ $t('invoices.send_invoice') }}
</a>
</v-dropdown-item>
<v-dropdown-item v-if="row.status === 'DRAFT'">
<a class="dropdown-item" href="#" @click="sentInvoice(row.id)">
<font-awesome-icon icon="check-circle" class="dropdown-item-icon" />
{{ $t('invoices.mark_as_sent') }}
</a>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeInvoice(row.id)">
<font-awesome-icon :icon="['fas', 'trash']" class="dropdown-item-icon" />
{{ $t('general.delete') }}
</div>
</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 moment from 'moment'
export default {
components: {
'moon-walker-icon': MoonWalkerIcon
},
data () {
return {
showFilters: false,
currency: null,
status: [
{
label: 'Status',
isDisable: true,
options: [
{ name: 'DRAFT', value: 'DRAFT' },
{ name: 'DUE', value: 'UNPAID' },
{ name: 'SENT', value: 'SENT' },
{ name: 'VIEWED', value: 'VIEWED' },
{ name: 'OVERDUE', value: 'OVERDUE' },
{ name: 'COMPLETED', value: 'COMPLETED' }
]
},
{
label: 'Paid Status',
options: [
{ name: 'UNPAID', value: 'UNPAID' },
{ name: 'PAID', value: 'PAID' },
{ name: 'PARTIALLY PAID', value: 'PARTIALLY_PAID' }
]
}
],
filtersApplied: false,
isRequestOngoing: true,
filters: {
customer: '',
status: { name: 'DUE', value: 'UNPAID' },
from_date: '',
to_date: '',
invoice_number: ''
}
}
},
computed: {
showEmptyScreen () {
return !this.totalInvoices && !this.isRequestOngoing && !this.filtersApplied
},
filterIcon () {
return (this.showFilters) ? 'times' : 'filter'
},
...mapGetters('customer', [
'customers'
]),
...mapGetters('invoice', [
'selectedInvoices',
'totalInvoices',
'invoices',
'selectAllField'
]),
selectField: {
get: function () {
return this.selectedInvoices
},
set: function (val) {
this.selectInvoice(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.selectAllInvoices()
}
},
methods: {
...mapActions('invoice', [
'fetchInvoices',
'getRecord',
'selectInvoice',
'resetSelectedInvoices',
'selectAllInvoices',
'deleteInvoice',
'deleteMultipleInvoices',
'sendEmail',
'markAsSent',
'setSelectAllState'
]),
...mapActions('customer', [
'fetchCustomers'
]),
async sendInvoice (id) {
const data = {
id: id
}
let response = await this.sendEmail(data)
this.refreshTable()
if (response.data) {
window.toastr['success'](this.$tc('invoices.send_invoice'))
}
},
async sentInvoice (id) {
const data = {
id: id
}
let response = await this.markAsSent(data)
this.refreshTable()
if (response.data) {
window.toastr['success'](this.$tc('invoices.mark_as_sent'))
}
},
getStatus (val) {
this.filters.status = {
name: val,
value: val
}
},
refreshTable () {
this.$refs.table.refresh()
},
async fetchData ({ page, filter, sort }) {
let data = {
customer_id: this.filters.customer === '' ? this.filters.customer : this.filters.customer.id,
status: this.filters.status.value,
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'),
invoice_number: this.filters.invoice_number,
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page
}
this.isRequestOngoing = true
let response = await this.fetchInvoices(data)
this.isRequestOngoing = false
this.currency = response.data.currency
return {
data: response.data.invoices.data,
pagination: {
totalPages: response.data.invoices.last_page,
currentPage: page,
count: response.data.invoices.count
}
}
},
setFilters () {
this.filtersApplied = true
this.resetSelectedInvoices()
this.refreshTable()
},
clearFilter () {
if (this.filters.customer) {
this.$refs.customerSelect.$refs.baseSelect.removeElement(this.filters.customer)
}
this.filters = {
customer: '',
status: '',
from_date: '',
to_date: '',
invoice_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 removeInvoice (id) {
this.id = id
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('invoices.confirm_delete'),
icon: 'error',
buttons: true,
dangerMode: true
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteInvoice(this.id)
if (res.data.success) {
window.toastr['success'](this.$tc('invoices.deleted_message'))
return true
}
if (res.data.error === 'payment_attached') {
window.toastr['error'](this.$t('invoices.payment_attached_message'), this.$t('general.action_failed'))
return true
}
window.toastr['error'](res.data.error)
return true
}
this.$refs.table.refresh()
this.filtersApplied = false
this.resetSelectedInvoices()
})
},
async removeMultipleInvoices () {
swal({
title: this.$t('general.are_you_sure'),
text: this.$tc('invoices.confirm_delete', 2),
icon: 'error',
buttons: true,
dangerMode: true
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteMultipleInvoices()
if (res.data.error === 'payment_attached') {
window.toastr['error'](this.$t('invoices.payment_attached_message'), this.$t('general.action_failed'))
return true
}
if (res.data) {
this.$refs.table.refresh()
this.filtersApplied = false
this.resetSelectedInvoices()
window.toastr['success'](this.$tc('invoices.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()
}
}
}
</script>

View File

@ -0,0 +1,83 @@
<template>
<div class="section mt-2">
<label class="invoice-label">
{{ tax.name }} ({{ tax.percent }}%)
</label>
<label class="invoice-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,403 @@
<template>
<tr class="item-row invoice-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 { mapActions, mapGetters } from 'vuex'
import TaxStub from '../../stub/tax'
import InvoiceStub from '../../stub/invoice'
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.defaultCurrenctForInput
}
},
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 = {...InvoiceStub, 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,129 @@
<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('invoices.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="item.description"
:invalid-description="invalidDescription"
:placeholder="$t('invoices.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>
<!-- <textarea type="text" v-autoresize rows="1" class="description-input" v-model="item.description" placeholder="Type Item Description (optional)" /> -->
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
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
}
},
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,165 @@
<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('general.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('invoices.add_new_tax') }}</label>
</button>
</div>
</base-select> <br>
</div>
<div class="text-right tax-amount" v-html="$utils.formatMoney(taxAmount, currency)" />
<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)
}
this.updateTax()
window.hub.$on('newTax', (val) => {
if (!this.selectedTax) {
this.selectedTax = val
this.onSelectTax(val)
}
})
},
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,262 @@
<template>
<div v-if="invoice" class="main-content invoice-view-page">
<div class="page-header">
<h3 class="page-title"> {{ invoice.invoice_number }}</h3>
<div class="page-actions row">
<div class="col-xs-2 mr-3">
<base-button
:loading="isRequestOnGoing"
:disabled="isRequestOnGoing"
:outline="true"
color="theme"
@click="onMarkAsSent"
>
{{ $t('invoices.mark_as_sent') }}
</base-button>
</div>
<router-link :to="`/admin/payments/${$route.params.id}/create`">
<base-button
color="theme"
>
{{ $t('payments.record_payment') }}
</base-button>
</router-link>
<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/invoices/${$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="removeInvoice($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="invoice-sidebar">
<div 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_invoice_date"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="invoice_date"
@change="onSearched"
>
<label class="inv-label" for="filter_invoice_date">{{ $t('invoices.invoice_date') }}</label>
</div>
<div class="filter-items">
<input
id="filter_due_date"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="due_date"
@change="onSearched"
>
<label class="inv-label" for="filter_due_date">{{ $t('invoices.due_date') }}</label>
</div>
<div class="filter-items">
<input
id="filter_invoice_number"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="invoice_number"
@change="onSearched"
>
<label class="inv-label" for="filter_invoice_number">{{ $t('invoices.invoice_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>
<base-loader v-if="isSearching" />
<div v-else class="side-content">
<router-link
v-for="(invoice,index) in invoices"
:to="`/admin/invoices/${invoice.id}/view`"
:key="index"
class="side-invoice"
>
<div class="left">
<div class="inv-name">{{ invoice.user.name }}</div>
<div class="inv-number">{{ invoice.invoice_number }}</div>
<div :class="'inv-status-'+invoice.status.toLowerCase()" class="inv-status">{{ invoice.status }}</div>
</div>
<div class="right">
<div class="inv-amount" v-html="$utils.formatMoney(invoice.due_amount, invoice.user.currency)" />
<div class="inv-date">{{ invoice.formattedInvoiceDate }}</div>
</div>
</router-link>
<p v-if="!invoices.length" class="no-result">
{{ $t('invoices.no_matching_invoices') }}
</p>
</div>
</div>
<div class="invoice-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,
invoices: [],
invoice: 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.fetchInvoice()
}
},
mounted () {
this.loadInvoices()
this.onSearched = _.debounce(this.onSearched, 500)
},
methods: {
...mapActions('invoice', [
'fetchInvoices',
'fetchViewInvoice',
'getRecord',
'searchInvoice',
'markAsSent',
'deleteInvoice',
'selectInvoice'
]),
async loadInvoices () {
let response = await this.fetchInvoices()
if (response.data) {
this.invoices = response.data.invoices.data
}
this.fetchInvoice()
},
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.searchInvoice(data)
this.isSearching = false
if (response.data) {
this.invoices = response.data.invoices.data
}
},
async fetchInvoice () {
let invoice = await this.fetchViewInvoice(this.$route.params.id)
if (invoice.data) {
this.invoice = invoice.data.invoice
this.shareableLink = invoice.data.shareable_link
this.currency = invoice.data.invoice.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.invoice.id})
this.isRequestOnGoing = false
if (response.data) {
window.toastr['success'](this.$tc('invoices.marked_as_sent_message'))
}
},
async removeInvoice (id) {
this.selectInvoice([parseInt(id)])
this.id = id
swal({
title: 'Deleted',
text: 'you will not be able to recover this invoice!',
icon: 'error',
buttons: true,
dangerMode: true
}).then(async (willDelete) => {
if (willDelete) {
let request = await this.deleteInvoice(this.id)
if (request.data.success) {
window.toastr['success'](this.$tc('invoices.deleted_message', 1))
this.$router.push('/admin/invoices/')
} else if (request.data.error) {
window.toastr['error'](request.data.message)
}
}
})
}
}
}
</script>