mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-31 21:51:10 -04:00
build version 400
This commit is contained in:
File diff suppressed because it is too large
Load Diff
213
resources/assets/js/views/invoices/CustomerSelect.vue
Normal file
213
resources/assets/js/views/invoices/CustomerSelect.vue
Normal file
@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="col-span-5 pr-0">
|
||||
<div
|
||||
v-if="selectedCustomer"
|
||||
class="flex flex-col p-4 bg-white border border-gray-200 border-solid"
|
||||
style="min-height: 170px"
|
||||
>
|
||||
<div class="relative flex justify-between mb-2">
|
||||
<label class="flex-1 font-medium">{{ selectedCustomer.name }}</label>
|
||||
|
||||
<a
|
||||
class="relative my-0 ml-0 mr-6 text-sm font-medium cursor-pointer text-primary-500"
|
||||
@click.prevent="editCustomer"
|
||||
>
|
||||
{{ $t('general.edit') }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="relative my-0 ml-2 mr-6 text-sm font-medium cursor-pointer text-primary-500"
|
||||
@click.prevent="resetSelectedCustomer"
|
||||
>
|
||||
{{ $t('general.deselect') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mt-1">
|
||||
<div v-if="selectedCustomer.billing_address">
|
||||
<div class="flex flex-col">
|
||||
<label
|
||||
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
|
||||
>
|
||||
{{ $t('general.bill_to') }}
|
||||
</label>
|
||||
<div class="flex flex-col flex-1 p-0">
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.name"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.address_street_1"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.address_street_1 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.address_street_2"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.address_street_2 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="
|
||||
selectedCustomer.billing_address.city &&
|
||||
selectedCustomer.billing_address.state
|
||||
"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.city }},
|
||||
{{ selectedCustomer.billing_address.state }}
|
||||
{{ selectedCustomer.billing_address.zip }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.country"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.country.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing_address.phone"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedCustomer.shipping_address" class="col col-6">
|
||||
<div class="flex flex-col">
|
||||
<label
|
||||
class="mb-1 text-sm font-medium text-gray-500 uppercase whitespace-no-wrap"
|
||||
>
|
||||
{{ $t('general.ship_to') }}
|
||||
</label>
|
||||
<div class="flex flex-col flex-1 p-0">
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.name"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.address_street_1"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.address_street_1 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.address_street_2"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.address_street_2 }}
|
||||
</label>
|
||||
<label
|
||||
v-if="
|
||||
selectedCustomer.shipping_address.city &&
|
||||
selectedCustomer.shipping_address
|
||||
"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.city }},
|
||||
{{ selectedCustomer.shipping_address.state }}
|
||||
{{ selectedCustomer.shipping_address.zip }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.country"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.country.name }}
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping_address.phone"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping_address.phone }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<sw-popup
|
||||
:class="[
|
||||
'add-customer p-0',
|
||||
{
|
||||
'border border-solid border-danger rounded': valid.$error,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div
|
||||
slot="activator"
|
||||
class="relative flex justify-center px-0 py-16 bg-white border border-gray-200 border-solid rounded-md"
|
||||
style="min-height: 170px"
|
||||
>
|
||||
<user-icon
|
||||
class="flex justify-center w-10 h-10 p-2 mr-5 text-sm text-white bg-gray-200 rounded-full font-base"
|
||||
/>
|
||||
<div class="mt-1">
|
||||
<label class="text-lg">
|
||||
{{ $t('customers.new_customer') }}
|
||||
<span class="text-danger"> * </span>
|
||||
</label>
|
||||
<p v-if="valid.$error && !valid.required" class="text-danger">
|
||||
{{ $t('validation.required') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<customer-select-popup :user-id="customerId" type="invoice" />
|
||||
</sw-popup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { UserIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
valid: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
customerId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters('invoice', ['getTemplateId', 'selectedCustomer']),
|
||||
},
|
||||
|
||||
created() {
|
||||
this.resetSelectedCustomer()
|
||||
if (this.customerId) {
|
||||
this.selectCustomer(this.customerId)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('invoice', ['resetSelectedCustomer', 'selectCustomer']),
|
||||
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
editCustomer() {
|
||||
this.openModal({
|
||||
title: this.$t('customers.edit_customer'),
|
||||
componentName: 'CustomerModal',
|
||||
id: this.selectedCustomer.id,
|
||||
data: this.selectedCustomer,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,50 +1,49 @@
|
||||
<template>
|
||||
<div class="section mt-2">
|
||||
<label class="invoice-label">
|
||||
<div class="flex items-center justify-between w-full mt-2 text-sm">
|
||||
<label class="font-semibold leading-5 text-gray-500 uppercase">
|
||||
{{ tax.name }} ({{ tax.percent }}%)
|
||||
</label>
|
||||
<label class="invoice-amount">
|
||||
<label class="flex items-center justify-center text-lg text-black">
|
||||
<div v-html="$utils.formatMoney(tax.amount, currency)" />
|
||||
|
||||
<font-awesome-icon
|
||||
class="ml-2"
|
||||
icon="trash-alt"
|
||||
@click="$emit('remove', index)"
|
||||
/>
|
||||
<trash-icon class="h-5 ml-2" @click="$emit('remove', index)" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TrashIcon } from '@vue-hero-icons/solid'
|
||||
export default {
|
||||
components: {
|
||||
TrashIcon,
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
tax: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
taxAmount () {
|
||||
taxAmount() {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
@ -54,30 +53,26 @@ export default {
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
handler: 'updateTax',
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
handler: 'updateTax',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateTax () {
|
||||
updateTax() {
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
index: this.index,
|
||||
item: {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
amount: this.taxAmount,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@ -1,23 +1,22 @@
|
||||
<template>
|
||||
<tr class="item-row invoice-item-row">
|
||||
<td colspan="5">
|
||||
<table class="full-width">
|
||||
<tr class="box-border bg-white border border-gray-200 border-solid rounded-b">
|
||||
<td colspan="5" class="p-0 text-left align-top">
|
||||
<table class="w-full">
|
||||
<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%;">
|
||||
<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"
|
||||
/>
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex justify-start">
|
||||
<div
|
||||
class="flex items-center justify-center w-12 h-5 mt-2 text-gray-400 cursor-move handle"
|
||||
>
|
||||
<drag-icon />
|
||||
</div>
|
||||
<item-select
|
||||
ref="itemSelect"
|
||||
@ -34,88 +33,94 @@
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<base-input
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<sw-input
|
||||
v-model="item.quantity"
|
||||
:invalid="$v.item.quantity.$error"
|
||||
:is-input-group="!!item.unit_name"
|
||||
:input-group-text="item.unit_name"
|
||||
type="text"
|
||||
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>
|
||||
<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
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex-auto flex-fill bd-highlight">
|
||||
<div class="relative w-full">
|
||||
<sw-money
|
||||
v-model="price"
|
||||
v-bind="customerCurrency"
|
||||
class="input-field"
|
||||
:currency="customerCurrency"
|
||||
:invalid="$v.item.price.$error"
|
||||
@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>
|
||||
<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
|
||||
<td
|
||||
v-if="discountPerItem === 'YES'"
|
||||
class="px-5 py-4 text-left align-top"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-auto" role="group">
|
||||
<sw-input
|
||||
v-model="discount"
|
||||
:invalid="$v.item.discount_val.$error"
|
||||
input-class="item-discount"
|
||||
class="border-r-0 rounded-tr-none rounded-br-none"
|
||||
@input="$v.item.discount_val.$touch()"
|
||||
/>
|
||||
<v-dropdown :show-arrow="false" theme-light>
|
||||
<button
|
||||
<sw-dropdown>
|
||||
<sw-button
|
||||
slot="activator"
|
||||
type="button"
|
||||
class="btn item-dropdown dropdown-toggle"
|
||||
class="flex items-center px-5 py-1 text-sm font-medium leading-none text-center text-gray-500 whitespace-no-wrap border border-gray-300 border-solid rounded rounded-tl-none rounded-bl-none dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
style="height: 43px"
|
||||
variant="white"
|
||||
>
|
||||
{{ 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>
|
||||
<span class="flex items-center">
|
||||
{{
|
||||
item.discount_type == 'fixed' ? currency.symbol : '%'
|
||||
}}
|
||||
<chevron-down-icon class="h-5" />
|
||||
</span>
|
||||
</sw-button>
|
||||
|
||||
<sw-dropdown-item @click="selectFixed">
|
||||
{{ $t('general.fixed') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</div>
|
||||
<!-- <div v-if="$v.item.discount.$error"> discount error </div> -->
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="item-amount">
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<div class="flex items-center justify-end text-sm">
|
||||
<span>
|
||||
<div v-html="$utils.formatMoney(total, currency)" />
|
||||
</span>
|
||||
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
<div
|
||||
class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer"
|
||||
>
|
||||
<trash-icon
|
||||
v-if="showRemoveItemIcon"
|
||||
class="remove-icon"
|
||||
icon="trash-alt"
|
||||
class="h-5 text-gray-700"
|
||||
@click="removeItem"
|
||||
/>
|
||||
</div>
|
||||
@ -123,8 +128,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="taxPerItem === 'YES'" class="tax-tr">
|
||||
<td />
|
||||
<td colspan="4">
|
||||
<td class="px-5 py-4 text-left align-top" />
|
||||
<td colspan="4" class="px-5 py-4 text-left align-top">
|
||||
<tax
|
||||
v-for="(tax, index) in item.taxes"
|
||||
:key="tax.id"
|
||||
@ -145,98 +150,102 @@
|
||||
</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')
|
||||
import { TrashIcon, ViewGridIcon, ChevronDownIcon } from '@vue-hero-icons/solid'
|
||||
import DragIcon from '@/components/icon/DragIcon'
|
||||
const {
|
||||
required,
|
||||
minValue,
|
||||
between,
|
||||
maxLength,
|
||||
} = require('vuelidate/lib/validators')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tax,
|
||||
ItemSelect
|
||||
ItemSelect,
|
||||
TrashIcon,
|
||||
ViewGridIcon,
|
||||
ChevronDownIcon,
|
||||
DragIcon,
|
||||
},
|
||||
mixins: [validationMixin],
|
||||
props: {
|
||||
itemData: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
discountPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
invoiceItems: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
isClosePopup: false,
|
||||
itemSelect: null,
|
||||
item: {...this.itemData},
|
||||
item: { ...this.itemData },
|
||||
maxDiscount: 0,
|
||||
money: {
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
prefix: '$ ',
|
||||
precision: 2,
|
||||
masked: false
|
||||
masked: false,
|
||||
},
|
||||
isSelected: false
|
||||
isSelected: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
]),
|
||||
...mapGetters('modal', [
|
||||
'modalActive'
|
||||
]),
|
||||
...mapGetters('currency', [
|
||||
'defaultCurrencyForInput'
|
||||
]),
|
||||
customerCurrency () {
|
||||
...mapGetters('item', ['items']),
|
||||
...mapGetters('modal', ['modalActive']),
|
||||
...mapGetters('company', ['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
|
||||
masked: false,
|
||||
}
|
||||
} else {
|
||||
return this.defaultCurrenctForInput
|
||||
}
|
||||
},
|
||||
showRemoveItemIcon () {
|
||||
showRemoveItemIcon() {
|
||||
if (this.invoiceItems.length == 1) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
subtotal () {
|
||||
subtotal() {
|
||||
return this.item.price * this.item.quantity
|
||||
},
|
||||
discount: {
|
||||
@ -251,12 +260,12 @@ export default {
|
||||
}
|
||||
|
||||
this.item.discount = newValue
|
||||
}
|
||||
},
|
||||
},
|
||||
total () {
|
||||
total() {
|
||||
return this.subtotal - this.item.discount_val
|
||||
},
|
||||
totalSimpleTax () {
|
||||
totalSimpleTax() {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
@ -265,7 +274,7 @@ export default {
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalCompoundTax () {
|
||||
totalCompoundTax() {
|
||||
return window._.sumBy(this.item.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
@ -274,7 +283,7 @@ export default {
|
||||
return 0
|
||||
})
|
||||
},
|
||||
totalTax () {
|
||||
totalTax() {
|
||||
return this.totalSimpleTax + this.totalCompoundTax
|
||||
},
|
||||
price: {
|
||||
@ -292,51 +301,54 @@ export default {
|
||||
} else {
|
||||
this.item.price = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: {
|
||||
handler: 'updateItem',
|
||||
deep: true
|
||||
deep: true,
|
||||
},
|
||||
subtotal (newValue) {
|
||||
subtotal(newValue) {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
this.item.discount_val = (this.item.discount * newValue) / 100
|
||||
}
|
||||
},
|
||||
modalActive (val) {
|
||||
modalActive(val) {
|
||||
if (!val) {
|
||||
this.isSelected = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
validations () {
|
||||
validations() {
|
||||
return {
|
||||
item: {
|
||||
name: {
|
||||
required
|
||||
required,
|
||||
},
|
||||
quantity: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(20)
|
||||
minValue: minValue(0),
|
||||
maxLength: maxLength(20),
|
||||
},
|
||||
price: {
|
||||
required,
|
||||
minValue: minValue(1),
|
||||
maxLength: maxLength(20)
|
||||
maxLength: maxLength(20),
|
||||
},
|
||||
discount_val: {
|
||||
between: between(0, this.maxDiscount)
|
||||
between: between(0, this.maxDiscount),
|
||||
},
|
||||
description: {
|
||||
maxLength: maxLength(255)
|
||||
}
|
||||
}
|
||||
maxLength: maxLength(255),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
created () {
|
||||
mounted() {
|
||||
this.$v.item.$reset()
|
||||
},
|
||||
created() {
|
||||
window.hub.$on('checkItems', this.validateItem)
|
||||
window.hub.$on('newItem', (val) => {
|
||||
if (this.taxPerItem === 'YES') {
|
||||
@ -348,52 +360,54 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
updateTax (data) {
|
||||
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.item.taxes.push({ ...TaxStub, id: Guid.raw() })
|
||||
}
|
||||
|
||||
this.updateItem()
|
||||
},
|
||||
removeTax (index) {
|
||||
removeTax(index) {
|
||||
this.item.taxes.splice(index, 1)
|
||||
|
||||
this.updateItem()
|
||||
},
|
||||
taxWithPercentage ({ name, percent }) {
|
||||
taxWithPercentage({ name, percent }) {
|
||||
return `${name} (${percent}%)`
|
||||
},
|
||||
searchVal (val) {
|
||||
searchVal(val) {
|
||||
this.item.name = val
|
||||
},
|
||||
deselectItem () {
|
||||
this.item = {...InvoiceStub, id: this.item.id, taxes: [{...TaxStub, id: Guid.raw()}]}
|
||||
deselectItem() {
|
||||
this.item = {
|
||||
...InvoiceStub,
|
||||
id: this.item.id,
|
||||
taxes: [{ ...TaxStub, id: Guid.raw() }],
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.itemSelect.$refs.baseSelect.$refs.search.focus()
|
||||
})
|
||||
},
|
||||
onSelectItem (item) {
|
||||
onSelectItem(item) {
|
||||
this.item.name = item.name
|
||||
this.item.price = item.price
|
||||
this.item.item_id = item.id
|
||||
this.item.description = item.description
|
||||
this.item.unit_name = item.unit_name
|
||||
|
||||
if (this.taxPerItem === 'YES' && item.taxes) {
|
||||
let index = 0
|
||||
item.taxes.forEach(tax => {
|
||||
this.updateTax({index, item: { ...tax }})
|
||||
item.taxes.forEach((tax) => {
|
||||
this.updateTax({ index, item: { ...tax } })
|
||||
index++
|
||||
})
|
||||
}
|
||||
// if (this.item.taxes.length) {
|
||||
// this.item.taxes = {...item.taxes}
|
||||
// }
|
||||
},
|
||||
selectFixed () {
|
||||
selectFixed() {
|
||||
if (this.item.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
@ -401,7 +415,7 @@ export default {
|
||||
this.item.discount_val = this.item.discount * 100
|
||||
this.item.discount_type = 'fixed'
|
||||
},
|
||||
selectPercentage () {
|
||||
selectPercentage() {
|
||||
if (this.item.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
@ -410,24 +424,24 @@ export default {
|
||||
|
||||
this.item.discount_type = 'percentage'
|
||||
},
|
||||
updateItem () {
|
||||
updateItem() {
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
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]
|
||||
}
|
||||
taxes: [...this.item.taxes],
|
||||
},
|
||||
})
|
||||
},
|
||||
removeItem () {
|
||||
removeItem() {
|
||||
this.$emit('remove', this.index)
|
||||
},
|
||||
validateItem () {
|
||||
validateItem() {
|
||||
this.$v.item.$touch()
|
||||
|
||||
if (this.item !== null) {
|
||||
@ -435,7 +449,7 @@ export default {
|
||||
} else {
|
||||
this.$emit('itemValidate', this.index, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
<template>
|
||||
<div class="item-selector">
|
||||
<div v-if="item.item_id" class="selected-item">
|
||||
<div class="flex-1 text-sm">
|
||||
<div
|
||||
v-if="item.item_id"
|
||||
class="relative flex items-center h-10 pl-2 bg-gray-100 border border-gray-200 border-solid rounded"
|
||||
>
|
||||
{{ item.name }}
|
||||
|
||||
<span class="deselect-icon" @click="deselectItem">
|
||||
<font-awesome-icon icon="times-circle" />
|
||||
<span
|
||||
class="absolute text-gray-400 cursor-pointer"
|
||||
style="top: 8px; right: 10px"
|
||||
@click="deselectItem"
|
||||
>
|
||||
<x-circle-icon class="h-5" />
|
||||
</span>
|
||||
</div>
|
||||
<base-select
|
||||
<sw-select
|
||||
v-else
|
||||
ref="baseSelect"
|
||||
v-model="itemSelect"
|
||||
@ -24,93 +31,107 @@
|
||||
@select="onSelect"
|
||||
>
|
||||
<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
|
||||
type="button"
|
||||
class="flex items-center justify-center w-full p-3 bg-gray-200 border-none outline-none"
|
||||
@click="openItemModal"
|
||||
>
|
||||
<shopping-cart-icon
|
||||
class="h-5 mr-2 -ml-2 text-center text-primary-400"
|
||||
/>
|
||||
<label class="ml-2 text-sm leading-none text-primary-400">{{
|
||||
$t('general.add_new_item')
|
||||
}}</label>
|
||||
</button>
|
||||
</div>
|
||||
</base-select>
|
||||
<div class="item-description">
|
||||
<base-text-area
|
||||
</sw-select>
|
||||
<div class="w-full pt-1 text-xs text-light">
|
||||
<sw-textarea
|
||||
v-autoresize
|
||||
v-model="item.description"
|
||||
:invalid-description="invalidDescription"
|
||||
:placeholder="$t('invoices.item.type_item_description')"
|
||||
type="text"
|
||||
rows="1"
|
||||
class="description-input"
|
||||
variant="inv-desc"
|
||||
class="w-full text-xs text-gray-600 border-none resize-none"
|
||||
@input="$emit('onDesriptionInput')"
|
||||
/>
|
||||
<div v-if="invalidDescription">
|
||||
<span class="text-danger">{{ $tc('validation.description_maxlength') }}</span>
|
||||
<span class="text-xs 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'
|
||||
import { XCircleIcon, ShoppingCartIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
XCircleIcon,
|
||||
ShoppingCartIcon,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
invalidDescription: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
taxPerItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
itemSelect: null,
|
||||
loading: false
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('item', [
|
||||
'items'
|
||||
])
|
||||
...mapGetters('item', ['items']),
|
||||
},
|
||||
watch: {
|
||||
invalidDescription (newValue) {
|
||||
invalidDescription(newValue) {
|
||||
console.log(newValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
...mapActions('item', [
|
||||
'fetchItems'
|
||||
]),
|
||||
async searchItems (search) {
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
...mapActions('item', ['fetchItems']),
|
||||
|
||||
async searchItems(search) {
|
||||
let data = {
|
||||
search,
|
||||
filter: {
|
||||
name: search,
|
||||
unit: '',
|
||||
price: ''
|
||||
price: '',
|
||||
},
|
||||
orderByField: '',
|
||||
orderBy: '',
|
||||
page: 1
|
||||
page: 1,
|
||||
}
|
||||
|
||||
if (this.item) {
|
||||
data.item_id = this.item.item_id
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
@ -119,27 +140,31 @@ export default {
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
onTextChange (val) {
|
||||
|
||||
onTextChange(val) {
|
||||
this.searchItems(val)
|
||||
|
||||
this.$emit('search', val)
|
||||
},
|
||||
openItemModal () {
|
||||
|
||||
openItemModal() {
|
||||
this.$emit('onSelectItem')
|
||||
this.openModal({
|
||||
'title': this.$t('items.add_item'),
|
||||
'componentName': 'ItemModal',
|
||||
'data': {taxPerItem: this.taxPerItem, taxes: this.taxes}
|
||||
title: this.$t('items.add_item'),
|
||||
componentName: 'ItemModal',
|
||||
data: { taxPerItem: this.taxPerItem, taxes: this.taxes },
|
||||
})
|
||||
},
|
||||
|
||||
onSelect(val) {
|
||||
this.$emit('select', val)
|
||||
this.fetchItems()
|
||||
},
|
||||
deselectItem () {
|
||||
|
||||
deselectItem() {
|
||||
this.itemSelect = null
|
||||
this.$emit('deselect')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="tax-row">
|
||||
<div class="d-flex align-items-center tax-select">
|
||||
<label class="bd-highlight pr-2 mb-0" align="right">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center" style="flex: 4">
|
||||
<label class="pr-2 mb-0" align="right">
|
||||
{{ $t('general.tax') }}
|
||||
</label>
|
||||
<base-select
|
||||
<sw-select
|
||||
v-model="selectedTax"
|
||||
:options="filteredTypes"
|
||||
:allow-empty="false"
|
||||
@ -16,19 +16,29 @@
|
||||
@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
|
||||
type="button"
|
||||
class="flex items-center justify-center w-full px-2 py-2 bg-gray-200 border-none outline-none"
|
||||
@click="openTaxModal"
|
||||
>
|
||||
<check-circle-icon class="h-5 text-primary-400" />
|
||||
<label class="ml-2 text-sm leading-none text-primary-400">{{
|
||||
$t('invoices.add_new_tax')
|
||||
}}</label>
|
||||
</button>
|
||||
</div>
|
||||
</base-select> <br>
|
||||
</sw-select>
|
||||
<br />
|
||||
</div>
|
||||
<div class="text-right tax-amount" v-html="$utils.formatMoney(taxAmount, currency)" />
|
||||
<div class="remove-icon-wrapper">
|
||||
<font-awesome-icon
|
||||
<div
|
||||
class="text-sm text-right"
|
||||
style="flex: 3"
|
||||
v-html="$utils.formatMoney(taxAmount, currency)"
|
||||
/>
|
||||
<div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer">
|
||||
<trash-icon
|
||||
v-if="taxes.length && index !== taxes.length - 1"
|
||||
class="remove-icon"
|
||||
icon="trash-alt"
|
||||
class="h-5 text-gray-700"
|
||||
@click="removeTax"
|
||||
/>
|
||||
</div>
|
||||
@ -37,49 +47,52 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { CheckCircleIcon, TrashIcon } from '@vue-hero-icons/solid'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CheckCircleIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxData: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: []
|
||||
default: [],
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
tax: {...this.taxData},
|
||||
selectedTax: null
|
||||
tax: { ...this.taxData },
|
||||
selectedTax: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('taxType', [
|
||||
'taxTypes'
|
||||
]),
|
||||
filteredTypes () {
|
||||
const clonedTypes = this.taxTypes.map(a => ({...a}))
|
||||
...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)
|
||||
let found = this.taxes.find((tax) => tax.tax_type_id === taxType.id)
|
||||
|
||||
if (found) {
|
||||
taxType.$isDisabled = true
|
||||
@ -90,7 +103,7 @@ export default {
|
||||
return taxType
|
||||
})
|
||||
},
|
||||
taxAmount () {
|
||||
taxAmount() {
|
||||
if (this.tax.compound_tax && this.total) {
|
||||
return ((this.total + this.totalTax) * this.tax.percent) / 100
|
||||
}
|
||||
@ -100,19 +113,21 @@ export default {
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
total: {
|
||||
handler: 'updateTax'
|
||||
handler: 'updateTax',
|
||||
},
|
||||
totalTax: {
|
||||
handler: 'updateTax'
|
||||
}
|
||||
handler: 'updateTax',
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (this.taxData.tax_type_id > 0) {
|
||||
this.selectedTax = this.taxTypes.find(_type => _type.id === this.taxData.tax_type_id)
|
||||
this.selectedTax = this.taxTypes.find(
|
||||
(_type) => _type.id === this.taxData.tax_type_id
|
||||
)
|
||||
}
|
||||
|
||||
this.updateTax()
|
||||
@ -124,13 +139,11 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions('modal', [
|
||||
'openModal'
|
||||
]),
|
||||
customLabel ({ name, percent }) {
|
||||
...mapActions('modal', ['openModal']),
|
||||
customLabel({ name, percent }) {
|
||||
return `${name} - ${percent}%`
|
||||
},
|
||||
onSelectTax (val) {
|
||||
onSelectTax(val) {
|
||||
this.tax.percent = val.percent
|
||||
this.tax.tax_type_id = val.id
|
||||
this.tax.compound_tax = val.compound_tax
|
||||
@ -138,28 +151,28 @@ export default {
|
||||
|
||||
this.updateTax()
|
||||
},
|
||||
updateTax () {
|
||||
updateTax() {
|
||||
if (this.tax.tax_type_id === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('update', {
|
||||
'index': this.index,
|
||||
'item': {
|
||||
index: this.index,
|
||||
item: {
|
||||
...this.tax,
|
||||
amount: this.taxAmount
|
||||
}
|
||||
amount: this.taxAmount,
|
||||
},
|
||||
})
|
||||
},
|
||||
removeTax () {
|
||||
removeTax() {
|
||||
this.$emit('remove', this.index, this.tax)
|
||||
},
|
||||
openTaxModal () {
|
||||
openTaxModal() {
|
||||
this.openModal({
|
||||
'title': this.$t('settings.tax_types.add_tax'),
|
||||
'componentName': 'TaxTypeModal'
|
||||
title: this.$t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,203 +1,259 @@
|
||||
<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
|
||||
<base-page v-if="invoice" class="xl:pl-96">
|
||||
<sw-page-header :title="pageTitle">
|
||||
<template slot="actions">
|
||||
<div class="mr-3 text-sm">
|
||||
<sw-button
|
||||
v-if="invoice.status === 'DRAFT'"
|
||||
:loading="isMarkingAsSent"
|
||||
:disabled="isMarkingAsSent"
|
||||
:outline="true"
|
||||
color="theme"
|
||||
variant="primary-outline"
|
||||
@click="onMarkAsSent"
|
||||
>
|
||||
{{ $t('invoices.mark_as_sent') }}
|
||||
</base-button>
|
||||
</sw-button>
|
||||
</div>
|
||||
<base-button
|
||||
<sw-button
|
||||
v-if="invoice.status === 'DRAFT'"
|
||||
:loading="isSendingEmail"
|
||||
:disabled="isSendingEmail"
|
||||
color="theme"
|
||||
variant="primary"
|
||||
class="text-sm"
|
||||
@click="onSendInvoice"
|
||||
>
|
||||
{{ $t('invoices.send_invoice') }}
|
||||
</base-button>
|
||||
<router-link
|
||||
v-if="invoice.status === 'SENT'"
|
||||
</sw-button>
|
||||
<sw-button
|
||||
v-if="
|
||||
invoice.status === 'SENT' ||
|
||||
invoice.status === 'OVERDUE' ||
|
||||
invoice.status === 'VIEWED'
|
||||
"
|
||||
tag-name="router-link"
|
||||
:to="`/admin/payments/${$route.params.id}/create`"
|
||||
variant="primary"
|
||||
class="text-sm"
|
||||
>
|
||||
<base-button color="theme">
|
||||
{{ $t('payments.record_payment') }}
|
||||
</base-button>
|
||||
</router-link>
|
||||
<v-dropdown
|
||||
:close-on-select="true"
|
||||
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>
|
||||
<div class="dropdown-item" @click="copyPdfUrl">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'link']"
|
||||
class="dropdown-item-icon"
|
||||
/>
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</div>
|
||||
<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
|
||||
{{ $t('payments.record_payment') }}
|
||||
</sw-button>
|
||||
<sw-dropdown class="ml-3">
|
||||
<sw-button slot="activator" variant="primary" class="h-10">
|
||||
<dots-horizontal-icon class="h-5" />
|
||||
</sw-button>
|
||||
|
||||
<sw-dropdown-item @click="copyPdfUrl">
|
||||
<link-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.copy_pdf_url') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item
|
||||
tag-name="router-link"
|
||||
:to="`/admin/invoices/${$route.params.id}/edit`"
|
||||
>
|
||||
<pencil-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.edit') }}
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item @click="removeInvoice($route.params.id)">
|
||||
<trash-icon class="h-5 mr-3 text-gray-600" />
|
||||
{{ $t('general.delete') }}
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
</template>
|
||||
</sw-page-header>
|
||||
|
||||
<!-- sidebar -->
|
||||
<div
|
||||
class="fixed top-0 left-0 hidden h-full pt-16 pb-5 ml-56 bg-white xl:ml-64 w-88 xl:block"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-4 pt-8 pb-2 border border-gray-200 border-solid height-full"
|
||||
>
|
||||
<sw-input
|
||||
v-model="searchData.searchText"
|
||||
:placeholder="$t('general.search')"
|
||||
input-class="inv-search"
|
||||
icon="search"
|
||||
class="mb-6"
|
||||
type="text"
|
||||
align-icon="right"
|
||||
variant="gray"
|
||||
@input="onSearch"
|
||||
/>
|
||||
<div class="btn-group ml-3" role="group" aria-label="First group">
|
||||
<v-dropdown
|
||||
>
|
||||
<search-icon slot="rightIcon" class="h-5" />
|
||||
</sw-input>
|
||||
|
||||
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
|
||||
<sw-dropdown
|
||||
:close-on-select="false"
|
||||
align="left"
|
||||
class="filter-container"
|
||||
position="bottom-start"
|
||||
>
|
||||
<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">
|
||||
<sw-button slot="activator" size="md" variant="gray-light">
|
||||
<filter-icon class="h-5" />
|
||||
</sw-button>
|
||||
|
||||
<div class="px-2 py-1 mb-2 border-b border-gray-200 border-solid">
|
||||
{{ $t('general.sort_by') }}
|
||||
</div>
|
||||
<div class="filter-items">
|
||||
<input
|
||||
id="filter_invoice_date"
|
||||
v-model="searchData.orderByField"
|
||||
type="radio"
|
||||
name="filter"
|
||||
class="inv-radio"
|
||||
value="invoice_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
<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="onSearch"
|
||||
/>
|
||||
<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="onSearch"
|
||||
/>
|
||||
<label class="inv-label" for="filter_invoice_number">{{
|
||||
$t('invoices.invoice_number')
|
||||
}}</label>
|
||||
</div>
|
||||
</v-dropdown>
|
||||
<base-button
|
||||
|
||||
<sw-dropdown-item class="flex px-1 py-1 cursor-pointer">
|
||||
<sw-input-group class="-mt-2 text-sm font-normal">
|
||||
<sw-radio
|
||||
id="filter_invoice_date"
|
||||
v-model="searchData.orderByField"
|
||||
:label="$t('invoices.invoice_date')"
|
||||
name="filter"
|
||||
size="sm"
|
||||
value="invoice_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item class="flex px-1 py-1 cursor-pointer">
|
||||
<sw-input-group class="-mt-2 font-normal">
|
||||
<sw-radio
|
||||
id="filter_due_date"
|
||||
:label="$t('invoices.due_date')"
|
||||
v-model="searchData.orderByField"
|
||||
name="filter"
|
||||
size="sm"
|
||||
value="due_date"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
|
||||
<sw-dropdown-item class="flex px-1 py-1 cursor-pointer">
|
||||
<sw-input-group class="-mt-2 font-normal">
|
||||
<sw-radio
|
||||
id="filter_invoice_number"
|
||||
v-model="searchData.orderByField"
|
||||
size="sm"
|
||||
type="radio"
|
||||
name="filter"
|
||||
:label="$t('invoices.invoice_number')"
|
||||
value="invoice_number"
|
||||
@change="onSearch"
|
||||
/>
|
||||
</sw-input-group>
|
||||
</sw-dropdown-item>
|
||||
</sw-dropdown>
|
||||
|
||||
<sw-button
|
||||
class="ml-1"
|
||||
v-tooltip.top-center="{ content: getOrderName }"
|
||||
class="inv-button inv-filter-sorting-btn"
|
||||
color="default"
|
||||
size="medium"
|
||||
size="md"
|
||||
variant="gray-light"
|
||||
@click="sortData"
|
||||
>
|
||||
<font-awesome-icon v-if="getOrderBy" icon="sort-amount-up" />
|
||||
<font-awesome-icon v-else icon="sort-amount-down" />
|
||||
</base-button>
|
||||
<sort-ascending-icon v-if="getOrderBy" class="h-5" />
|
||||
<sort-descending-icon v-else class="h-5" />
|
||||
</sw-button>
|
||||
</div>
|
||||
</div>
|
||||
<base-loader v-if="isSearching" />
|
||||
<div v-else class="side-content">
|
||||
|
||||
<base-loader v-if="isSearching" :show-bg-overlay="true" />
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="h-full pb-32 overflow-y-scroll border-l border-gray-200 border-solid sw-scroll"
|
||||
>
|
||||
<router-link
|
||||
v-for="(invoice, index) in invoices"
|
||||
:to="`/admin/invoices/${invoice.id}/view`"
|
||||
:id="'invoice-' + invoice.id"
|
||||
:key="index"
|
||||
class="side-invoice"
|
||||
:class="[
|
||||
'flex justify-between p-4 items-center cursor-pointer hover:bg-gray-100 border-l-4 border-transparent',
|
||||
{
|
||||
'bg-gray-100 border-l-4 border-primary-500 border-solid': hasActiveUrl(
|
||||
invoice.id
|
||||
),
|
||||
},
|
||||
]"
|
||||
style="border-bottom: 1px solid rgba(185, 193, 209, 0.41)"
|
||||
>
|
||||
<div class="left">
|
||||
<div class="inv-name">{{ invoice.user.name }}</div>
|
||||
<div class="inv-number">{{ invoice.invoice_number }}</div>
|
||||
<div class="flex-2">
|
||||
<div
|
||||
:class="'inv-status-' + invoice.status.toLowerCase()"
|
||||
class="inv-status"
|
||||
class="pr-2 mb-2 text-sm not-italic font-normal leading-5 text-black capitalize truncate"
|
||||
>
|
||||
{{ invoice.user.name }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-1 mb-2 text-xs not-italic font-medium leading-5 text-gray-600"
|
||||
>
|
||||
{{ invoice.invoice_number }}
|
||||
</div>
|
||||
|
||||
<sw-badge
|
||||
class="px-1 text-xs"
|
||||
:bg-color="$utils.getBadgeStatusColor(invoice.status).bgColor"
|
||||
:color="$utils.getBadgeStatusColor(invoice.status).color"
|
||||
:font-size="$utils.getBadgeStatusColor(invoice.status).fontSize"
|
||||
>
|
||||
{{ invoice.status }}
|
||||
</div>
|
||||
</sw-badge>
|
||||
</div>
|
||||
<div class="right">
|
||||
|
||||
<div class="flex-1 whitespace-no-wrap right">
|
||||
<div
|
||||
class="inv-amount"
|
||||
class="mb-2 text-xl not-italic font-semibold leading-8 text-right text-gray-900"
|
||||
v-html="
|
||||
$utils.formatMoney(invoice.due_amount, invoice.user.currency)
|
||||
"
|
||||
/>
|
||||
<div class="inv-date">{{ invoice.formattedInvoiceDate }}</div>
|
||||
<div
|
||||
class="text-sm not-italic font-normal leading-5 text-right text-gray-600"
|
||||
>
|
||||
{{ invoice.formattedInvoiceDate }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<p v-if="!invoices.length" class="no-result">
|
||||
|
||||
<p
|
||||
v-if="!invoices.length"
|
||||
class="flex justify-center px-4 mt-5 text-sm text-gray-600"
|
||||
>
|
||||
{{ $t('invoices.no_matching_invoices') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-view-page-container">
|
||||
<iframe :src="`${shareableLink}`" class="frame-style" />
|
||||
|
||||
<div
|
||||
class="flex flex-col min-h-0 mt-8 overflow-hidden"
|
||||
style="height: 75vh"
|
||||
>
|
||||
<iframe
|
||||
:src="`${shareableLink}`"
|
||||
class="flex-1 border border-gray-400 border-solid rounded-md frame-style"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</base-page>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import {
|
||||
DotsHorizontalIcon,
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
LinkIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
} from '@vue-hero-icons/solid'
|
||||
|
||||
const _ = require('lodash')
|
||||
export default {
|
||||
data () {
|
||||
components: {
|
||||
DotsHorizontalIcon,
|
||||
FilterIcon,
|
||||
SortAscendingIcon,
|
||||
SortDescendingIcon,
|
||||
SearchIcon,
|
||||
LinkIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
count: null,
|
||||
@ -207,37 +263,49 @@ export default {
|
||||
searchData: {
|
||||
orderBy: null,
|
||||
orderByField: null,
|
||||
searchText: null
|
||||
searchText: null,
|
||||
},
|
||||
isRequestOnGoing: false,
|
||||
isSearching: false,
|
||||
isSendingEmail: false,
|
||||
isMarkingAsSent: false
|
||||
isMarkingAsSent: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getOrderBy () {
|
||||
if (this.searchData.orderBy === 'asc' || this.searchData.orderBy == null) {
|
||||
pageTitle() {
|
||||
return this.invoice.invoice_number
|
||||
},
|
||||
getOrderBy() {
|
||||
if (
|
||||
this.searchData.orderBy === 'asc' ||
|
||||
this.searchData.orderBy == null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
getOrderName () {
|
||||
getOrderName() {
|
||||
if (this.getOrderBy) {
|
||||
return this.$t('general.ascending')
|
||||
}
|
||||
return this.$t('general.descending')
|
||||
},
|
||||
shareableLink () {
|
||||
shareableLink() {
|
||||
return `/invoices/pdf/${this.invoice.unique_hash}`
|
||||
}
|
||||
},
|
||||
getCurrentInvoiceId() {
|
||||
if (this.invoice && this.invoice.id) {
|
||||
return this.invoice.id
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route (to, from) {
|
||||
$route(to, from) {
|
||||
this.loadInvoice()
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.loadInvoices()
|
||||
this.loadInvoice()
|
||||
this.onSearch = _.debounce(this.onSearch, 500)
|
||||
@ -251,32 +319,60 @@ export default {
|
||||
'sendEmail',
|
||||
'deleteInvoice',
|
||||
'selectInvoice',
|
||||
'fetchViewInvoice'
|
||||
'fetchInvoice',
|
||||
]),
|
||||
async loadInvoices () {
|
||||
let response = await this.fetchInvoices()
|
||||
|
||||
...mapActions('modal', ['openModal']),
|
||||
|
||||
hasActiveUrl(id) {
|
||||
return this.$route.params.id == id
|
||||
},
|
||||
|
||||
async loadInvoices() {
|
||||
let response = await this.fetchInvoices({ limit: 'all' })
|
||||
if (response.data) {
|
||||
this.invoices = response.data.invoices.data
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.scrollToInvoice()
|
||||
}, 500)
|
||||
},
|
||||
async loadInvoice () {
|
||||
let response = await this.fetchViewInvoice(this.$route.params.id)
|
||||
scrollToInvoice() {
|
||||
const el = document.getElementById(`invoice-${this.$route.params.id}`)
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth' })
|
||||
el.classList.add('shake')
|
||||
}
|
||||
},
|
||||
async loadInvoice() {
|
||||
let response = await this.fetchInvoice(this.$route.params.id)
|
||||
|
||||
if (response.data) {
|
||||
this.invoice = response.data.invoice
|
||||
}
|
||||
},
|
||||
async onSearch () {
|
||||
async onSearch() {
|
||||
let data = ''
|
||||
if (this.searchData.searchText !== '' && this.searchData.searchText !== null && this.searchData.searchText !== undefined) {
|
||||
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) {
|
||||
if (
|
||||
this.searchData.orderBy !== null &&
|
||||
this.searchData.orderBy !== undefined
|
||||
) {
|
||||
data += `orderBy=${this.searchData.orderBy}&`
|
||||
}
|
||||
|
||||
if (this.searchData.orderByField !== null && this.searchData.orderByField !== undefined) {
|
||||
if (
|
||||
this.searchData.orderByField !== null &&
|
||||
this.searchData.orderByField !== undefined
|
||||
) {
|
||||
data += `orderByField=${this.searchData.orderByField}`
|
||||
}
|
||||
this.isSearching = true
|
||||
@ -286,7 +382,7 @@ export default {
|
||||
this.invoices = response.data.invoices.data
|
||||
}
|
||||
},
|
||||
sortData () {
|
||||
sortData() {
|
||||
if (this.searchData.orderBy === 'asc') {
|
||||
this.searchData.orderBy = 'desc'
|
||||
this.onSearch()
|
||||
@ -296,77 +392,68 @@ export default {
|
||||
this.onSearch()
|
||||
return true
|
||||
},
|
||||
async onMarkAsSent () {
|
||||
window.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('invoices.invoice_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
this.isMarkingAsSent = true
|
||||
let response = await this.markAsSent({id: this.invoice.id})
|
||||
this.isMarkingAsSent = false
|
||||
if (response.data) {
|
||||
window.toastr['success'](this.$tc('invoices.marked_as_sent_message'))
|
||||
async onMarkAsSent() {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: this.$t('invoices.invoice_mark_as_sent'),
|
||||
icon: '/assets/icon/check-circle-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (value) => {
|
||||
if (value) {
|
||||
this.isMarkingAsSent = true
|
||||
let response = await this.markAsSent({
|
||||
id: this.invoice.id,
|
||||
status: 'SENT',
|
||||
})
|
||||
this.isMarkingAsSent = false
|
||||
if (response.data) {
|
||||
this.invoice.status = 'SENT'
|
||||
window.toastr['success'](
|
||||
this.$tc('invoices.marked_as_sent_message')
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async onSendInvoice() {
|
||||
this.openModal({
|
||||
title: this.$t('invoices.send_invoice'),
|
||||
componentName: 'SendInvoiceModal',
|
||||
id: this.invoice.id,
|
||||
data: this.invoice,
|
||||
})
|
||||
},
|
||||
async onSendInvoice () {
|
||||
window.swal({
|
||||
title: this.$tc('general.are_you_sure'),
|
||||
text: this.$tc('invoices.confirm_send_invoice'),
|
||||
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.invoice.id})
|
||||
this.isSendingEmail = false
|
||||
if (response.data.success) {
|
||||
window.toastr['success'](this.$tc('invoices.send_invoice_successfully'))
|
||||
return true
|
||||
}
|
||||
if (response.data.error === 'user_email_does_not_exist') {
|
||||
window.toastr['error'](this.$tc('invoices.user_email_does_not_exist'))
|
||||
return false
|
||||
}
|
||||
window.toastr['error'](this.$tc('invoices.something_went_wrong'))
|
||||
}
|
||||
})
|
||||
},
|
||||
copyPdfUrl () {
|
||||
copyPdfUrl() {
|
||||
let pdfUrl = `${window.location.origin}/invoices/pdf/${this.invoice.unique_hash}`
|
||||
|
||||
let response = this.$utils.copyTextToClipboard(pdfUrl)
|
||||
|
||||
window.toastr['success'](this.$tc('Copied PDF url to clipboard!'))
|
||||
|
||||
window.toastr['success'](this.$t('general.copied_pdf_url_clipboard'))
|
||||
},
|
||||
async removeInvoice (id) {
|
||||
this.selectInvoice([parseInt(id)])
|
||||
this.id = id
|
||||
window.swal({
|
||||
title: 'Deleted',
|
||||
text: 'you will not be able to recover this invoice!',
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true
|
||||
}).then(async (value) => {
|
||||
if (value) {
|
||||
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)
|
||||
async removeInvoice(id) {
|
||||
window
|
||||
.swal({
|
||||
title: this.$t('general.are_you_sure'),
|
||||
text: 'you will not be able to recover this invoice!',
|
||||
icon: '/assets/icon/trash-solid.svg',
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(async (value) => {
|
||||
if (value) {
|
||||
let request = await this.deleteInvoice({ ids: [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