mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-31 21:51:10 -04:00
init crater
This commit is contained in:
711
resources/assets/js/views/invoices/Create.vue
Normal file
711
resources/assets/js/views/invoices/Create.vue
Normal 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>
|
||||
589
resources/assets/js/views/invoices/Edit.vue
Normal file
589
resources/assets/js/views/invoices/Edit.vue
Normal 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>
|
||||
540
resources/assets/js/views/invoices/Index.vue
Normal file
540
resources/assets/js/views/invoices/Index.vue
Normal 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>
|
||||
83
resources/assets/js/views/invoices/InvoiceTax.vue
Normal file
83
resources/assets/js/views/invoices/InvoiceTax.vue
Normal 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>
|
||||
403
resources/assets/js/views/invoices/Item.vue
Normal file
403
resources/assets/js/views/invoices/Item.vue
Normal 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>
|
||||
129
resources/assets/js/views/invoices/ItemSelect.vue
Normal file
129
resources/assets/js/views/invoices/ItemSelect.vue
Normal 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>
|
||||
165
resources/assets/js/views/invoices/Tax.vue
Normal file
165
resources/assets/js/views/invoices/Tax.vue
Normal 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>
|
||||
262
resources/assets/js/views/invoices/View.vue
Normal file
262
resources/assets/js/views/invoices/View.vue
Normal 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>
|
||||
Reference in New Issue
Block a user