Fix Invoice/Estimate template issues and Add Payment Receipt, Custom Payment Modes and Item units

This commit is contained in:
Jay Makwana
2020-01-05 07:22:36 +00:00
committed by Mohit Panjwani
parent 56a955befd
commit 4c33a5d88c
112 changed files with 5050 additions and 331 deletions

View File

@ -85,7 +85,8 @@
</div>
<div class="customer-content mb-1">
<label class="email">{{ selectedCustomer.name }}</label>
<label class="action" @click="removeCustomer">{{ $t('general.remove') }}</label>
<label class="action" @click="editCustomer">{{ $t('general.edit') }}</label>
<label class="action" @click="removeCustomer">{{ $t('general.deselect') }}</label>
</div>
</div>
@ -195,6 +196,7 @@
:index="index"
:item-data="item"
:currency="currency"
:estimate-items="newEstimate.items"
:tax-per-item="taxPerItem"
:discount-per-item="discountPerItem"
@remove="removeItem"
@ -589,6 +591,14 @@ export default {
removeCustomer () {
this.resetSelectedCustomer()
},
editCustomer () {
this.openModal({
'title': this.$t('customers.edit_customer'),
'componentName': 'CustomerModal',
'id': this.selectedCustomer.id,
'data': this.selectedCustomer
})
},
openTemplateModal () {
this.openModal({
'title': this.$t('general.choose_template'),

View File

@ -24,6 +24,8 @@
:invalid="$v.item.name.$error"
:invalid-description="$v.item.description.$error"
:item="item"
:tax-per-item="taxPerItem"
:taxes="item.taxes"
@search="searchVal"
@select="onSelectItem"
@deselect="deselectItem"
@ -108,7 +110,7 @@
<div class="remove-icon-wrapper">
<font-awesome-icon
v-if="index > 0"
v-if="isShowRemoveItemIcon"
class="remove-icon"
icon="trash-alt"
@click="removeItem"
@ -180,6 +182,10 @@ export default {
discountPerItem: {
type: String,
default: ''
},
estimateItems: {
type: Array,
required: true
}
},
data () {
@ -221,6 +227,12 @@ export default {
return this.defaultCurrencyForInput
}
},
isShowRemoveItemIcon () {
if (this.estimateItems.length == 1) {
return false
}
return true
},
subtotal () {
return this.item.price * this.item.quantity
},
@ -324,6 +336,9 @@ export default {
created () {
window.hub.$on('checkItems', this.validateItem)
window.hub.$on('newItem', (val) => {
if (this.taxPerItem === 'YES') {
this.item.taxes = val.taxes
}
if (!this.item.item_id && this.modalActive && this.isSelected) {
this.onSelectItem(val)
}
@ -363,7 +378,13 @@ export default {
this.item.price = item.price
this.item.item_id = item.id
this.item.description = item.description
if (this.taxPerItem === 'YES' && item.taxes) {
let index = 0
item.taxes.forEach(tax => {
this.updateTax({index, item: { ...tax }})
index++
})
}
// if (this.item.taxes.length) {
// this.item.taxes = {...item.taxes}
// }

View File

@ -68,6 +68,14 @@ export default {
type: Boolean,
required: false,
default: false
},
taxPerItem: {
type: String,
default: ''
},
taxes: {
type: Array,
default: null
}
},
data () {
@ -129,7 +137,8 @@ export default {
this.$emit('onSelectItem')
this.openModal({
'title': 'Add Item',
'componentName': 'ItemModal'
'componentName': 'ItemModal',
'data': {taxPerItem: this.taxPerItem, taxes: this.taxes}
})
},
deselectItem () {

View File

@ -69,7 +69,9 @@
<font-awesome-icon icon="filter" />
</base-button>
</a>
<div class="filter-title">
{{ $t('general.sort_by') }}
</div>
<div class="filter-items">
<input
id="filter_estimate_date"
@ -107,7 +109,7 @@
<label class="inv-label" for="filter_estimate_number">{{ $t('estimates.estimate_number') }}</label>
</div>
</v-dropdown>
<base-button class="inv-button inv-filter-sorting-btn" color="default" size="medium" @click="sortData">
<base-button v-tooltip.top-center="{ content: getOrderName }" 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>
@ -172,7 +174,12 @@ export default {
}
return false
},
getOrderName () {
if (this.getOrderBy) {
return this.$t('general.ascending')
}
return this.$t('general.descending')
},
shareableLink () {
return `/estimates/pdf/${this.estimate.unique_hash}`
}

View File

@ -83,7 +83,8 @@
</div>
<div class="customer-content mb-1">
<label class="email">{{ selectedCustomer.name }}</label>
<label class="action" @click="removeCustomer">{{ $t('general.remove') }}</label>
<label class="action" @click="editCustomer">{{ $t('general.edit') }}</label>
<label class="action" @click="removeCustomer">{{ $t('general.deselect') }}</label>
</div>
</div>
@ -193,6 +194,7 @@
:key="item.id"
:index="index"
:item-data="item"
:invoice-items="newInvoice.items"
:currency="currency"
:tax-per-item="taxPerItem"
:discount-per-item="discountPerItem"
@ -589,6 +591,14 @@ export default {
removeCustomer () {
this.resetSelectedCustomer()
},
editCustomer () {
this.openModal({
'title': this.$t('customers.edit_customer'),
'componentName': 'CustomerModal',
'id': this.selectedCustomer.id,
'data': this.selectedCustomer
})
},
openTemplateModal () {
this.openModal({
'title': this.$t('general.choose_template'),

View File

@ -259,6 +259,18 @@
{{ $t('invoices.mark_as_sent') }}
</a>
</v-dropdown-item>
<v-dropdown-item v-if="row.status === 'SENT' || row.status === 'VIEWED' || row.status === 'OVERDUE'">
<router-link :to="`/admin/payments/${row.id}/create`" class="dropdown-item">
<font-awesome-icon :icon="['fas', 'credit-card']" class="dropdown-item-icon"/>
{{ $t('payments.record_payment') }}
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<a class="dropdown-item" href="#/" @click="onCloneInvoice(row.id)">
<font-awesome-icon icon="copy" class="dropdown-item-icon" />
{{ $t('invoices.clone_invoice') }}
</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" />
@ -378,7 +390,8 @@ export default {
'deleteMultipleInvoices',
'sendEmail',
'markAsSent',
'setSelectAllState'
'setSelectAllState',
'cloneInvoice'
]),
...mapActions('customer', [
'fetchCustomers'
@ -429,6 +442,27 @@ export default {
}
})
},
async onCloneInvoice (id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('invoices.confirm_clone'),
icon: '/assets/icon/check-circle-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
const data = {
id: id
}
let response = await this.cloneInvoice(data)
this.refreshTable()
if (response.data) {
window.toastr['success'](this.$tc('invoices.cloned_successfully'))
this.$router.push(`/admin/invoices/${response.data.invoice.id}/edit`)
}
}
})
},
getStatus (val) {
this.filters.status = {
name: val,

View File

@ -24,6 +24,8 @@
:invalid="$v.item.name.$error"
:invalid-description="$v.item.description.$error"
:item="item"
:tax-per-item="taxPerItem"
:taxes="item.taxes"
@search="searchVal"
@select="onSelectItem"
@deselect="deselectItem"
@ -109,7 +111,7 @@
<div class="remove-icon-wrapper">
<font-awesome-icon
v-if="index > 0"
v-if="showRemoveItemIcon"
class="remove-icon"
icon="trash-alt"
@click="removeItem"
@ -181,6 +183,10 @@ export default {
discountPerItem: {
type: String,
default: ''
},
invoiceItems: {
type: Array,
default: null
}
},
data () {
@ -222,6 +228,12 @@ export default {
return this.defaultCurrenctForInput
}
},
showRemoveItemIcon () {
if (this.invoiceItems.length == 1) {
return false
}
return true
},
subtotal () {
return this.item.price * this.item.quantity
},
@ -325,6 +337,9 @@ export default {
created () {
window.hub.$on('checkItems', this.validateItem)
window.hub.$on('newItem', (val) => {
if (this.taxPerItem === 'YES') {
this.item.taxes = val.taxes
}
if (!this.item.item_id && this.modalActive && this.isSelected) {
this.onSelectItem(val)
}
@ -364,7 +379,13 @@ export default {
this.item.price = item.price
this.item.item_id = item.id
this.item.description = item.description
if (this.taxPerItem === 'YES' && item.taxes) {
let index = 0
item.taxes.forEach(tax => {
this.updateTax({index, item: { ...tax }})
index++
})
}
// if (this.item.taxes.length) {
// this.item.taxes = {...item.taxes}
// }

View File

@ -66,6 +66,14 @@ export default {
type: Boolean,
required: false,
default: false
},
taxPerItem: {
type: String,
default: ''
},
taxes: {
type: Array,
default: null
}
},
data () {
@ -118,7 +126,8 @@ export default {
this.$emit('onSelectItem')
this.openModal({
'title': 'Add Item',
'componentName': 'ItemModal'
'componentName': 'ItemModal',
'data': {taxPerItem: this.taxPerItem, taxes: this.taxes}
})
},
deselectItem () {

View File

@ -73,7 +73,9 @@
<font-awesome-icon icon="filter" />
</base-button>
</a>
<div class="filter-title">
{{ $t('general.sort_by') }}
</div>
<div class="filter-items">
<input
id="filter_invoice_date"
@ -111,7 +113,7 @@
<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">
<base-button v-tooltip.top-center="{ content: getOrderName }" 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>
@ -168,13 +170,18 @@ export default {
}
},
computed: {
getOrderBy () {
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
return true
}
return false
},
getOrderName () {
if (this.getOrderBy) {
return this.$t('general.ascending')
}
return this.$t('general.descending')
},
shareableLink () {
return `/invoices/pdf/${this.invoice.unique_hash}`
}

View File

@ -50,11 +50,31 @@
<label>{{ $t('items.unit') }}</label>
<base-select
v-model="formData.unit"
:options="units"
:options="itemUnits"
:searchable="true"
:show-labels="false"
:placeholder="$t('items.select_a_unit')"
label="name"
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="addItemUnit">
<font-awesome-icon class="icon" icon="cart-plus" />
<label>{{ $t('settings.customization.items.add_item_unit') }}</label>
</button>
</div>
</base-select>
</div>
<div v-if="isTaxPerItem" class="form-group">
<label>{{ $t('items.taxes') }}</label>
<base-select
v-model="formData.taxes"
:options="getTaxTypes"
:searchable="true"
:show-labels="false"
:allow-empty="true"
:multiple="true"
track-by="tax_type_id"
label="tax_name"
/>
</div>
<div class="form-group">
@ -66,7 +86,9 @@
@input="$v.formData.description.$touch()"
/>
<div v-if="$v.formData.description.$error">
<span v-if="!$v.formData.description.maxLength" class="text-danger">{{ $t('validation.description_maxlength') }}</span>
<span v-if="!$v.formData.description.maxLength" class="text-danger">
{{ $t('validation.description_maxlength') }}
</span>
</div>
</div>
<div class="form-group">
@ -102,24 +124,17 @@ export default {
return {
isLoading: false,
title: 'Add Item',
units: [
{ name: 'box', value: 'box' },
{ name: 'cm', value: 'cm' },
{ name: 'dz', value: 'dz' },
{ name: 'ft', value: 'ft' },
{ name: 'g', value: 'g' },
{ name: 'in', value: 'in' },
{ name: 'kg', value: 'kg' },
{ name: 'km', value: 'km' },
{ name: 'lb', value: 'lb' },
{ name: 'mg', value: 'mg' },
{ name: 'pc', value: 'pc' }
],
units: [],
taxes: [],
taxPerItem: '',
formData: {
name: '',
description: '',
price: '',
unit: null
unit_id: null,
unit: null,
taxes: [],
tax_per_item: false
},
money: {
decimal: '.',
@ -134,6 +149,9 @@ export default {
...mapGetters('currency', [
'defaultCurrencyForInput'
]),
...mapGetters('item', [
'itemUnits'
]),
price: {
get: function () {
return this.formData.price / 100
@ -142,14 +160,26 @@ export default {
this.formData.price = newValue * 100
}
},
...mapGetters('taxType', [
'taxTypes'
]),
isEdit () {
if (this.$route.name === 'items.edit') {
return true
}
return false
},
isTaxPerItem () {
return this.taxPerItem === 'YES' ? 1 : 0
},
getTaxTypes () {
return this.taxTypes.map(tax => {
return {...tax, tax_type_id: tax.id, tax_name: tax.name + ' (' + tax.percent + '%)'}
})
}
},
created () {
this.setTaxPerItem()
if (this.isEdit) {
this.loadEditData()
}
@ -177,10 +207,26 @@ export default {
'fetchItem',
'updateItem'
]),
...mapActions('modal', [
'openModal'
]),
async setTaxPerItem () {
let res = await axios.get('/api/settings/get-setting?key=tax_per_item')
if (res.data && res.data.tax_per_item === 'YES') {
this.taxPerItem = 'YES'
} else {
this.taxPerItem = 'FALSE'
}
},
async loadEditData () {
let response = await this.fetchItem(this.$route.params.id)
this.formData = response.data.item
this.formData.unit = this.units.find(_unit => response.data.item.unit === _unit.name)
this.formData = {...response.data.item, unit: null}
this.formData.taxes = response.data.item.taxes.map(tax => {
return {...tax, tax_name: tax.name + ' (' + tax.percent + '%)'}
})
this.formData.unit = this.itemUnits.find(_unit => response.data.item.unit_id === _unit.id)
this.fractional_price = response.data.item.price
},
async submitItem () {
@ -189,30 +235,40 @@ export default {
return false
}
if (this.formData.unit) {
this.formData.unit = this.formData.unit.name
this.formData.unit_id = this.formData.unit.id
}
let response
if (this.isEdit) {
this.isLoading = true
let response = await this.updateItem(this.formData)
if (response.data) {
this.isLoading = false
window.toastr['success'](this.$tc('items.updated_message'))
this.$router.push('/admin/items')
return true
}
window.toastr['error'](response.data.error)
response = await this.updateItem(this.formData)
} else {
this.isLoading = true
let response = await this.addItem(this.formData)
if (response.data) {
window.toastr['success'](this.$tc('items.created_message'))
this.$router.push('/admin/items')
this.isLoading = false
return true
let data = {
...this.formData,
taxes: this.formData.taxes.map(tax => {
return {
tax_type_id: tax.id,
amount: ((this.formData.price * tax.percent) / 100),
percent: tax.percent,
name: tax.name,
collective_tax: 0
}
})
}
window.toastr['success'](response.data.success)
response = await this.addItem(data)
}
if (response.data) {
this.isLoading = false
window.toastr['success'](this.$tc('items.updated_message'))
this.$router.push('/admin/items')
return true
}
window.toastr['error'](response.data.error)
},
async addItemUnit () {
this.openModal({
'title': 'Add Item Unit',
'componentName': 'ItemUnit'
})
}
}
}

View File

@ -64,7 +64,7 @@
<label class="form-label"> {{ $tc('items.unit') }} </label>
<base-select
v-model="filters.unit"
:options="units"
:options="itemUnits"
:searchable="true"
:show-labels="false"
:placeholder="$t('items.select_a_unit')"
@ -169,7 +169,7 @@
/>
<table-column
:label="$t('items.unit')"
show="unit"
show="unit_name"
/>
<table-column
:label="$t('items.price')"
@ -235,19 +235,6 @@ export default {
id: null,
showFilters: false,
sortedBy: 'created_at',
units: [
{ name: 'box', value: 'box' },
{ name: 'cm', value: 'cm' },
{ name: 'dz', value: 'dz' },
{ name: 'ft', value: 'ft' },
{ name: 'g', value: 'g' },
{ name: 'in', value: 'in' },
{ name: 'kg', value: 'kg' },
{ name: 'km', value: 'km' },
{ name: 'lb', value: 'lb' },
{ name: 'mg', value: 'mg' },
{ name: 'pc', value: 'pc' }
],
isRequestOngoing: true,
filtersApplied: false,
filters: {
@ -262,7 +249,8 @@ export default {
'items',
'selectedItems',
'totalItems',
'selectAllField'
'selectAllField',
'itemUnits'
]),
...mapGetters('currency', [
'defaultCurrency'
@ -296,6 +284,7 @@ export default {
deep: true
}
},
destroyed () {
if (this.selectAllField) {
this.selectAllItems()
@ -316,7 +305,7 @@ export default {
async fetchData ({ page, filter, sort }) {
let data = {
search: this.filters.name !== null ? this.filters.name : '',
unit: this.filters.unit !== null ? this.filters.unit.name : '',
unit_id: this.filters.unit !== null ? this.filters.unit.id : '',
price: this.filters.price * 100,
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
@ -395,7 +384,7 @@ export default {
}).then(async (willDelete) => {
if (willDelete) {
let res = await this.deleteMultipleItems()
if (res.data.success) {
if (res.data.success || res.data.items) {
window.toastr['success'](this.$tc('items.deleted_message', 2))
this.$refs.table.refresh()
} else if (res.data.error) {

View File

@ -109,12 +109,20 @@
<div class="form-group">
<label class="form-label">{{ $t('payments.payment_mode') }}</label>
<base-select
v-model="formData.payment_mode"
:options="getPaymentMode"
v-model="formData.payment_method"
:options="paymentModes"
:searchable="true"
:show-labels="false"
:placeholder="$t('payments.select_payment_mode')"
/>
label="name"
>
<div slot="afterList">
<button type="button" class="list-add-button" @click="addPaymentMode">
<font-awesome-icon class="icon" icon="cart-plus" />
<label>{{ $t('settings.customization.payments.add_payment_mode') }}</label>
</button>
</div>
</base-select>
</div>
</div>
<div class="col-sm-12 ">
@ -166,9 +174,10 @@ export default {
payment_number: null,
payment_date: null,
amount: 0,
payment_mode: null,
payment_method: null,
invoice_id: null,
notes: null
notes: null,
payment_method_id: null
},
money: {
decimal: '.',
@ -215,9 +224,9 @@ export default {
...mapGetters('currency', [
'defaultCurrencyForInput'
]),
getPaymentMode () {
return ['Cash', 'Check', 'Credit Card', 'Bank Transfer']
},
...mapGetters('payment', [
'paymentModes'
]),
amount: {
get: function () {
return this.formData.amount / 100
@ -286,14 +295,23 @@ export default {
'fetchCreatePayment',
'addPayment',
'updatePayment',
'fetchPayment'
'fetchEditPaymentData'
]),
...mapActions('modal', [
'openModal'
]),
invoiceWithAmount ({ invoice_number, due_amount }) {
return `${invoice_number} (${this.$utils.formatGraphMoney(due_amount, this.customer.currency)})`
},
async addPaymentMode () {
this.openModal({
'title': 'Add Payment Mode',
'componentName': 'PaymentMode'
})
},
async loadData () {
if (this.isEdit) {
let response = await this.fetchPayment(this.$route.params.id)
let response = await this.fetchEditPaymentData(this.$route.params.id)
this.customerList = response.data.customers
this.formData = { ...response.data.payment }
this.customer = response.data.payment.user
@ -301,6 +319,7 @@ export default {
this.formData.amount = parseFloat(response.data.payment.amount)
this.paymentPrefix = response.data.payment_prefix
this.paymentNumAttribute = response.data.nextPaymentNumber
this.formData.payment_method = response.data.payment.payment_method
if (response.data.payment.invoice !== null) {
this.maxPayableAmount = parseInt(response.data.payment.amount) + parseInt(response.data.payment.invoice.due_amount)
this.invoice = response.data.payment.invoice
@ -344,6 +363,7 @@ export default {
let data = {
editData: {
...this.formData,
payment_method_id: this.formData.payment_method.id,
payment_date: moment(this.formData.payment_date).format('DD/MM/YYYY')
},
id: this.$route.params.id
@ -371,6 +391,7 @@ export default {
} else {
let data = {
...this.formData,
payment_method_id: this.formData.payment_method.id,
payment_date: moment(this.formData.payment_date).format('DD/MM/YYYY')
}
this.isLoading = true

View File

@ -67,10 +67,11 @@
<label class="form-label">{{ $t('payments.payment_mode') }}</label>
<base-select
v-model="filters.payment_mode"
:options="payment_mode"
:options="paymentModes"
:searchable="true"
:show-labels="false"
:placeholder="$t('payments.payment_mode')"
label="name"
/>
</div>
</div>
@ -203,6 +204,14 @@
{{ $t('general.edit') }}
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<router-link :to="{path: `payments/${row.id}/view`}" class="dropdown-item">
<font-awesome-icon icon="eye" class="dropdown-item-icon" />
{{ $t('general.view') }}
</router-link>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removePayment(row.id)">
@ -237,7 +246,6 @@ export default {
sortedBy: 'created_at',
filtersApplied: false,
isRequestOngoing: true,
payment_mode: ['Cash', 'Check', 'Credit Card', 'Bank Transfer'],
filters: {
customer: null,
payment_mode: '',
@ -259,7 +267,8 @@ export default {
'selectedPayments',
'totalPayments',
'payments',
'selectAllField'
'selectAllField',
'paymentModes'
]),
selectField: {
get: function () {
@ -308,7 +317,7 @@ export default {
let data = {
customer_id: this.filters.customer !== null ? this.filters.customer.id : '',
payment_number: this.filters.payment_number,
payment_mode: this.filters.payment_mode ? this.filters.payment_mode : '',
payment_method_id: this.filters.payment_mode ? this.filters.payment_mode.id : '',
orderByField: sort.fieldName || 'created_at',
orderBy: sort.order || 'desc',
page

View File

@ -0,0 +1,286 @@
<template>
<div v-if="payment" class="main-content payment-view-page">
<div class="page-header">
<h3 class="page-title"> {{ payment.payment_number }}</h3>
<div class="page-actions row">
<base-button
:loading="isSendingEmail"
:disabled="isSendingEmail"
:outline="true"
color="theme"
@click="onPaymentSend"
>
{{ $t('payments.send_payment_receipt') }}
</base-button>
<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/payments/${$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="removePayment($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="payment-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="onSearch"
/>
<div
class="btn-group ml-3"
role="group"
aria-label="First group"
>
<v-dropdown :close-on-select="false" align="left" class="filter-container">
<a slot="activator" href="#">
<base-button class="inv-button inv-filter-fields-btn" color="default" size="medium">
<font-awesome-icon icon="filter" />
</base-button>
</a>
<div class="filter-title">
{{ $t('general.sort_by') }}
</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="onSearch"
>
<label class="inv-label" for="filter_invoice_number">{{ $t('invoices.title') }}</label>
</div>
<div class="filter-items">
<input
id="filter_payment_date"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="payment_date"
@change="onSearch"
>
<label class="inv-label" for="filter_payment_date">{{ $t('payments.date') }}</label>
</div>
<div class="filter-items">
<input
id="filter_payment_number"
v-model="searchData.orderByField"
type="radio"
name="filter"
class="inv-radio"
value="payment_number"
@change="onSearch"
>
<label class="inv-label" for="filter_payment_number">{{ $t('payments.payment_number') }}</label>
</div>
</v-dropdown>
<base-button v-tooltip.top-center="{ content: getOrderName }" 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="(payment,index) in payments"
:to="`/admin/payments/${payment.id}/view`"
:key="index"
class="side-payment"
>
<div class="left">
<div class="inv-name">{{ payment.user.name }}</div>
<div class="inv-number">{{ payment.payment_number }}</div>
<div class="inv-number">{{ payment.invoice_number }}</div>
</div>
<div class="right">
<div class="inv-amount" v-html="$utils.formatMoney(payment.amount, payment.user.currency)" />
<div class="inv-date">{{ payment.formattedPaymentDate }}</div>
<!-- <div class="inv-number">{{ payment.payment_method.name }}</div> -->
</div>
</router-link>
<p v-if="!payments.length" class="no-result">
{{ $t('payments.no_matching_invoices') }}
</p>
</div>
</div>
<div class="payment-view-page-container" >
<iframe :src="`${shareableLink}`" class="frame-style"/>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
const _ = require('lodash')
export default {
data () {
return {
id: null,
count: null,
payments: [],
payment: null,
currency: null,
searchData: {
orderBy: null,
orderByField: null,
searchText: null
},
isRequestOnGoing: false,
isSearching: false,
isSendingEmail: false,
isMarkingAsSent: false
}
},
computed: {
getOrderBy () {
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
return true
}
return false
},
getOrderName () {
if (this.getOrderBy) {
return this.$t('general.ascending')
}
return this.$t('general.descending')
},
shareableLink () {
return `/payments/pdf/${this.payment.unique_hash}`
}
},
watch: {
$route (to, from) {
this.loadPayment()
}
},
created () {
this.loadPayments()
this.loadPayment()
this.onSearch = _.debounce(this.onSearch, 500)
},
methods: {
// ...mapActions('invoice', [
// 'fetchInvoices',
// 'getRecord',
// 'searchInvoice',
// 'markAsSent',
// 'sendEmail',
// 'deleteInvoice',
// 'fetchViewInvoice'
// ]),
...mapActions('payment', [
'fetchPayments',
'fetchPayment',
'sendEmail',
'deletePayment',
'searchPayment'
]),
async loadPayments () {
let response = await this.fetchPayments()
if (response.data) {
this.payments = response.data.payments.data
}
},
async loadPayment () {
let response = await this.fetchPayment(this.$route.params.id)
if (response.data) {
this.payment = response.data.payment
}
},
async onSearch () {
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.searchPayment(data)
this.isSearching = false
if (response.data) {
this.payments = response.data.payments.data
}
},
sortData () {
if (this.searchData.orderBy === 'asc') {
this.searchData.orderBy = 'desc'
this.onSearch()
return true
}
this.searchData.orderBy = 'asc'
this.onSearch()
return true
},
async onPaymentSend () {
window.swal({
title: this.$tc('general.are_you_sure'),
text: this.$tc('payments.confirm_send_payment'),
icon: '/assets/icon/paper-plane-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
this.isSendingEmail = true
let response = await this.sendEmail({id: this.payment.id})
this.isSendingEmail = false
if (response.data.success) {
window.toastr['success'](this.$tc('payments.send_payment_successfully'))
return true
}
if (response.data.error === 'user_email_does_not_exist') {
window.toastr['error'](this.$tc('payments.user_email_does_not_exist'))
return false
}
window.toastr['error'](this.$tc('payments.something_went_wrong'))
}
})
},
async removePayment (id) {
this.id = id
window.swal({
title: 'Deleted',
text: 'you will not be able to recover this payment!',
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
let request = await this.deletePayment(this.id)
if (request.data.success) {
window.toastr['success'](this.$tc('payments.deleted_message', 1))
this.$router.push('/admin/payments')
} else if (request.data.error) {
window.toastr['error'](request.data.message)
}
}
})
}
}
}
</script>

View File

@ -11,12 +11,15 @@
<li class="tab" @click="setActiveTab('PAYMENTS')">
<a :class="['tab-link', {'a-active': activeTab === 'PAYMENTS'}]" href="#">{{ $t('settings.customization.payments.title') }}</a>
</li>
<li class="tab" @click="setActiveTab('ITEMS')">
<a :class="['tab-link', {'a-active': activeTab === 'ITEMS'}]" href="#">{{ $t('settings.customization.items.title') }}</a>
</li>
</ul>
<!-- Invoices Tab -->
<transition name="fade-customize">
<div v-if="activeTab === 'INVOICES'" class="invoice-tab">
<form action="" class="form-section" @submit.prevent="updateInvoiceSetting">
<form action="" class="mt-3" @submit.prevent="updateInvoiceSetting">
<div class="row">
<div class="col-md-12 mb-4">
<label class="input-label">{{ $t('settings.customization.invoices.invoice_prefix') }}</label>
@ -32,7 +35,7 @@
<span v-if="!$v.invoices.invoice_prefix.alpha" class="text-danger">{{ $t('validation.characters_only') }}</span>
</div>
</div>
<div class="row mb-3">
<div class="row pb-3">
<div class="col-md-12">
<base-button
icon="save"
@ -43,25 +46,23 @@
</base-button>
</div>
</div>
<hr>
</form>
<div class="col-md-12 mt-3">
<div class="page-header">
<h3 class="page-title">
{{ $t('settings.customization.invoices.invoice_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="invoiceAutogenerate"
class="btn-switch"
@change="setInvoiceSetting"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.customization.invoices.autogenerate_invoice_number') }} </p>
<p class="box-desc"> {{ $t('settings.customization.invoices.invoice_setting_description') }} </p>
</div>
<hr>
<div class="page-header pt-3">
<h3 class="page-title">
{{ $t('settings.customization.invoices.invoice_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="invoiceAutogenerate"
class="btn-switch"
@change="setInvoiceSetting"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.customization.invoices.autogenerate_invoice_number') }} </p>
<p class="box-desc"> {{ $t('settings.customization.invoices.invoice_setting_description') }} </p>
</div>
</div>
</div>
@ -71,7 +72,7 @@
<!-- Estimates Tab -->
<transition name="fade-customize">
<div v-if="activeTab === 'ESTIMATES'" class="estimate-tab">
<form action="" class="form-section" @submit.prevent="updateEstimateSetting">
<form action="" class="mt-3" @submit.prevent="updateEstimateSetting">
<div class="row">
<div class="col-md-12 mb-4">
<label class="input-label">{{ $t('settings.customization.estimates.estimate_prefix') }}</label>
@ -87,7 +88,7 @@
<span v-if="!$v.estimates.estimate_prefix.alpha" class="text-danger">{{ $t('validation.characters_only') }}</span>
</div>
</div>
<div class="row mb-3">
<div class="row pb-3">
<div class="col-md-12">
<base-button
icon="save"
@ -100,23 +101,21 @@
</div>
<hr>
</form>
<div class="col-md-12 mt-3">
<div class="page-header">
<h3 class="page-title">
{{ $t('settings.customization.estimates.estimate_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="estimateAutogenerate"
class="btn-switch"
@change="setEstimateSetting"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.customization.estimates.autogenerate_estimate_number') }} </p>
<p class="box-desc"> {{ $t('settings.customization.estimates.estimate_setting_description') }} </p>
</div>
<div class="page-header pt-3">
<h3 class="page-title">
{{ $t('settings.customization.estimates.estimate_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="estimateAutogenerate"
class="btn-switch"
@change="setEstimateSetting"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.customization.estimates.autogenerate_estimate_number') }} </p>
<p class="box-desc"> {{ $t('settings.customization.estimates.estimate_setting_description') }} </p>
</div>
</div>
</div>
@ -126,7 +125,66 @@
<!-- Payments Tab -->
<transition name="fade-customize">
<div v-if="activeTab === 'PAYMENTS'" class="payment-tab">
<form action="" class="form-section" @submit.prevent="updatePaymentSetting">
<div class="page-header">
<div class="row">
<div class="col-md-8">
<!-- <h3 class="page-title">
{{ $t('settings.customization.payments.payment_mode') }}
</h3> -->
</div>
<div class="col-md-4 d-flex flex-row-reverse">
<base-button
outline
class="add-new-tax"
color="theme"
@click="addPaymentMode"
>
{{ $t('settings.customization.payments.add_payment_mode') }}
</base-button>
</div>
</div>
</div>
<table-component
ref="table"
:show-filter="false"
:data="paymentModes"
table-class="table tax-table"
class="mb-3"
>
<table-column
:sortable="true"
:label="$t('settings.customization.payments.payment_mode')"
show="name"
/>
<table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<div class="dropdown-item" @click="editPaymentMode(row)">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</div>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removePaymentMode(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>
<hr>
<form action="" class="pt-3" @submit.prevent="updatePaymentSetting">
<div class="row">
<div class="col-md-12 mb-4">
<label class="input-label">{{ $t('settings.customization.payments.payment_prefix') }}</label>
@ -142,7 +200,7 @@
<span v-if="!$v.payments.payment_prefix.alpha" class="text-danger">{{ $t('validation.characters_only') }}</span>
</div>
</div>
<div class="row mb-3">
<div class="row pb-3">
<div class="col-md-12">
<base-button
icon="save"
@ -155,33 +213,97 @@
</div>
</form>
<hr>
<div class="col-md-12 mt-4">
<div class="page-header">
<h3 class="page-title">
{{ $t('settings.customization.payments.payment_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="paymentAutogenerate"
class="btn-switch"
@change="setPaymentSetting"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.customization.payments.autogenerate_payment_number') }} </p>
<p class="box-desc"> {{ $t('settings.customization.payments.payment_setting_description') }} </p>
</div>
<div class="page-header pt-3">
<h3 class="page-title">
{{ $t('settings.customization.payments.payment_settings') }}
</h3>
<div class="flex-box">
<div class="left">
<base-switch
v-model="paymentAutogenerate"
class="btn-switch"
@change="setPaymentSetting"
/>
</div>
<div class="right ml-15">
<p class="box-title"> {{ $t('settings.customization.payments.autogenerate_payment_number') }} </p>
<p class="box-desc"> {{ $t('settings.customization.payments.payment_setting_description') }} </p>
</div>
</div>
</div>
</div>
</transition>
<!-- Items Tab -->
<transition name="fade-customize">
<div v-if="activeTab === 'ITEMS'" class="item-tab">
<div class="page-header">
<div class="row">
<div class="col-md-8">
<!-- <h3 class="page-title">
{{ $t('settings.customization.items.title') }}
</h3> -->
</div>
<div class="col-md-4 d-flex flex-row-reverse">
<base-button
outline
class="add-new-tax"
color="theme"
@click="addItemUnit"
>
{{ $t('settings.customization.items.add_item_unit') }}
</base-button>
</div>
</div>
</div>
<table-component
ref="itemTable"
:show-filter="false"
:data="itemUnits"
table-class="table tax-table"
class="mb-3"
>
<table-column
:sortable="true"
:label="$t('settings.customization.items.units')"
show="name"
/>
<table-column
:sortable="false"
:filterable="false"
cell-class="action-dropdown"
>
<template slot-scope="row">
<span>{{ $t('settings.tax_types.action') }}</span>
<v-dropdown>
<a slot="activator" href="#">
<dot-icon />
</a>
<v-dropdown-item>
<div class="dropdown-item" @click="editItemUnit(row)">
<font-awesome-icon :icon="['fas', 'pencil-alt']" class="dropdown-item-icon" />
{{ $t('general.edit') }}
</div>
</v-dropdown-item>
<v-dropdown-item>
<div class="dropdown-item" @click="removeItemUnit(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>
</transition>
</div>
</div>
</template>
<script>
import { validationMixin } from 'vuelidate'
import { mapActions, mapGetters } from 'vuex'
const { required, maxLength, alpha } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
@ -204,9 +326,20 @@ export default {
payments: {
payment_prefix: null
},
items: {
units: []
},
currentData: null
}
},
computed: {
...mapGetters('item', [
'itemUnits'
]),
...mapGetters('payment', [
'paymentModes'
])
},
watch: {
activeTab () {
this.loadData()
@ -239,6 +372,15 @@ export default {
this.loadData()
},
methods: {
...mapActions('modal', [
'openModal'
]),
...mapActions('payment', [
'deletePaymentMode'
]),
...mapActions('item', [
'deleteItemUnit'
]),
async setInvoiceSetting () {
let data = {
key: 'invoice_auto_generate',
@ -259,6 +401,78 @@ export default {
window.toastr['success'](this.$t('general.setting_updated'))
}
},
async addItemUnit () {
this.openModal({
'title': 'Add Item Unit',
'componentName': 'ItemUnit'
})
this.$refs.itemTable.refresh()
},
async editItemUnit (data) {
this.openModal({
'title': 'Edit Item Unit',
'componentName': 'ItemUnit',
'id': data.id,
'data': data
})
this.$refs.itemTable.refresh()
},
async removeItemUnit (id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.customization.items.item_unit_confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
let response = await this.deleteItemUnit(id)
if (response.data.success) {
window.toastr['success'](this.$t('settings.customization.items.deleted_message'))
this.id = null
this.$refs.itemTable.refresh()
return true
}
window.toastr['error'](this.$t('settings.customization.items.already_in_use'))
}
})
},
async addPaymentMode () {
this.openModal({
'title': 'Add Payment Mode',
'componentName': 'PaymentMode'
})
this.$refs.table.refresh()
},
async editPaymentMode (data) {
this.openModal({
'title': 'Edit Payment Mode',
'componentName': 'PaymentMode',
'id': data.id,
'data': data
})
this.$refs.table.refresh()
},
removePaymentMode (id) {
swal({
title: this.$t('general.are_you_sure'),
text: this.$t('settings.customization.payments.payment_mode_confirm_delete'),
icon: '/assets/icon/trash-solid.svg',
buttons: true,
dangerMode: true
}).then(async (value) => {
if (value) {
let response = await this.deletePaymentMode(id)
if (response.data.success) {
window.toastr['success'](this.$t('settings.customization.payments.deleted_message'))
this.id = null
this.$refs.table.refresh()
return true
}
window.toastr['error'](this.$t('settings.customization.payments.already_in_use'))
}
})
},
changeToUppercase (currentTab) {
if (currentTab === 'INVOICES') {
this.invoices.invoice_prefix = this.invoices.invoice_prefix.toUpperCase()

View File

@ -15,7 +15,19 @@
:mail-drivers="mail_drivers"
@on-change-driver="(val) => mail_driver = mailConfigData.mail_driver = val"
@submit-data="saveEmailConfig"
/>
>
<base-button
:loading="loading"
outline
class="pull-right mt-4 ml-2"
icon="check"
color="theme"
type="button"
@click="openMailTestModal"
>
{{ $t('general.test_mail_conf') }}
</base-button>
</component>
</div>
</div>
</div>
@ -27,6 +39,7 @@ import Smtp from './mailDriver/Smtp'
import Mailgun from './mailDriver/Mailgun'
import Ses from './mailDriver/Ses'
import Basic from './mailDriver/Basic'
import { mapActions } from 'vuex'
export default {
components: {
@ -50,6 +63,9 @@ export default {
this.loadData()
},
methods: {
...mapActions('modal', [
'openModal'
]),
async loadData () {
this.loading = true
@ -79,6 +95,12 @@ export default {
} catch (e) {
window.toastr['error']('Something went wrong')
}
},
openMailTestModal () {
this.openModal({
'title': 'Test Mail Configuration',
'componentName': 'MailTestModal'
})
}
}
}

View File

@ -73,15 +73,18 @@
</div>
</div>
</div>
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<div class="d-flex">
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<slot/>
</div>
</form>
</template>
<script>

View File

@ -167,15 +167,18 @@
</div>
</div>
</div>
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<div class="d-flex">
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<slot/>
</div>
</form>
</template>
<script>

View File

@ -146,15 +146,18 @@
</div>
</div>
</div>
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<div class="d-flex">
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<slot/>
</div>
</form>
</template>
<script>

View File

@ -146,15 +146,18 @@
</div>
</div>
</div>
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<div class="d-flex">
<base-button
:loading="loading"
class="pull-right mt-4"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
<slot/>
</div>
</form>
</template>
<script>

View File

@ -54,7 +54,6 @@
:placeholder="$t('general.select_country')"
track-by="id"
label="name"
@input="fetchState()"
/>
<div v-if="$v.companyData.country_id.$error">
<span v-if="!$v.companyData.country_id.required" class="text-danger">{{ $tc('validation.required') }}</span>