mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-28 04:01:10 -04:00
v5.0.0 update
This commit is contained in:
@ -0,0 +1,513 @@
|
||||
<template>
|
||||
<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%; min-width: 280px" />
|
||||
<col style="width: 10%; min-width: 120px" />
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
<col
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
style="width: 15%; min-width: 160px"
|
||||
/>
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="px-5 py-4 text-left align-top">
|
||||
<div class="flex justify-start">
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-5
|
||||
h-5
|
||||
mt-2
|
||||
text-gray-300
|
||||
cursor-move
|
||||
handle
|
||||
mr-2
|
||||
"
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
<BaseItemSelect
|
||||
type="Invoice"
|
||||
:item="itemData"
|
||||
:invalid="v$.name.$error"
|
||||
:invalid-description="v$.description.$error"
|
||||
:taxes="itemData.taxes"
|
||||
:index="index"
|
||||
:store-prop="storeProp"
|
||||
:store="store"
|
||||
@search="searchVal"
|
||||
@select="onSelectItem"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<BaseInput
|
||||
v-model="quantity"
|
||||
:invalid="v$.quantity.$error"
|
||||
:content-loading="loading"
|
||||
type="number"
|
||||
small
|
||||
min="1"
|
||||
@change="syncItemToStore()"
|
||||
@input="v$.quantity.$touch()"
|
||||
/>
|
||||
</td>
|
||||
<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">
|
||||
<BaseMoney
|
||||
:key="selectedCurrency"
|
||||
v-model="price"
|
||||
:invalid="v$.price.$error"
|
||||
:content-loading="loading"
|
||||
:currency="selectedCurrency"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
class="px-5 py-4 text-left align-top"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex" style="width: 120px" role="group">
|
||||
<BaseInput
|
||||
v-model="discount"
|
||||
:invalid="v$.discount_val.$error"
|
||||
:content-loading="loading"
|
||||
class="
|
||||
border-r-0
|
||||
focus:border-r-2
|
||||
rounded-tr-sm rounded-br-sm
|
||||
h-[38px]
|
||||
"
|
||||
/>
|
||||
<BaseDropdown position="bottom-end">
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
:content-loading="loading"
|
||||
class="rounded-tr-md rounded-br-md !p-2 rounded-none"
|
||||
type="button"
|
||||
variant="white"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
{{
|
||||
itemData.discount_type == 'fixed'
|
||||
? currency.symbol
|
||||
: '%'
|
||||
}}
|
||||
|
||||
<BaseIcon
|
||||
name="ChevronDownIcon"
|
||||
class="w-4 h-4 text-gray-500 ml-1"
|
||||
/>
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem @click="selectFixed">
|
||||
{{ $t('general.fixed') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem @click="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-right align-top">
|
||||
<div class="flex items-center justify-end text-sm">
|
||||
<span>
|
||||
<BaseContentPlaceholders v-if="loading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<BaseFormatMoney
|
||||
v-else
|
||||
:amount="total"
|
||||
:currency="selectedCurrency"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex items-center justify-center w-6 h-10 mx-2">
|
||||
<BaseIcon
|
||||
v-if="showRemoveButton"
|
||||
class="h-5 text-gray-700 cursor-pointer"
|
||||
name="TrashIcon"
|
||||
@click="store.removeItem(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="store[storeProp].tax_per_item === 'YES'">
|
||||
<td class="px-5 py-4 text-left align-top" />
|
||||
<td colspan="4" class="px-5 py-4 text-left align-top">
|
||||
<BaseContentPlaceholders v-if="loading">
|
||||
<BaseContentPlaceholdersText
|
||||
:lines="1"
|
||||
class="w-24 h-8 rounded-md border"
|
||||
/>
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<ItemTax
|
||||
v-for="(tax, index1) in itemData.taxes"
|
||||
v-else
|
||||
:key="tax.id"
|
||||
:index="index1"
|
||||
:item-index="index"
|
||||
:tax-data="tax"
|
||||
:taxes="itemData.taxes"
|
||||
:discounted-total="total"
|
||||
:total-tax="totalSimpleTax"
|
||||
:total="subtotal"
|
||||
:currency="currency"
|
||||
:update-items="syncItemToStore"
|
||||
:ability="abilities.CREATE_INVOICE"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
@update="updateTax"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, inject } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Guid from 'guid'
|
||||
import TaxStub from '@/scripts/stub/tax'
|
||||
import ItemTax from './CreateItemRowTax.vue'
|
||||
import { sumBy } from 'lodash'
|
||||
import abilities from '@/scripts/stub/abilities'
|
||||
import {
|
||||
required,
|
||||
between,
|
||||
maxLength,
|
||||
helpers,
|
||||
minValue,
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useItemStore } from '@/scripts/stores/item'
|
||||
import DragIcon from '@/scripts/components/icons/DragIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemData: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
invoiceItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
itemValidationScope: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update', 'remove', 'itemValidate'])
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const itemStore = useItemStore()
|
||||
|
||||
let route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const quantity = computed({
|
||||
get: () => {
|
||||
return props.itemData.quantity
|
||||
},
|
||||
set: (newValue) => {
|
||||
updateItemAttribute('quantity', parseInt(newValue))
|
||||
},
|
||||
})
|
||||
|
||||
const price = computed({
|
||||
get: () => {
|
||||
const price = props.itemData.price
|
||||
|
||||
if (parseFloat(price) > 0) {
|
||||
return price / 100
|
||||
}
|
||||
|
||||
return price
|
||||
},
|
||||
|
||||
set: (newValue) => {
|
||||
if (parseFloat(newValue) > 0) {
|
||||
let price = Math.round(newValue * 100)
|
||||
|
||||
updateItemAttribute('price', price)
|
||||
} else {
|
||||
updateItemAttribute('price', newValue)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const subtotal = computed(() => props.itemData.price * props.itemData.quantity)
|
||||
|
||||
const discount = computed({
|
||||
get: () => {
|
||||
return props.itemData.discount
|
||||
},
|
||||
set: (newValue) => {
|
||||
if (props.itemData.discount_type === 'percentage') {
|
||||
updateItemAttribute('discount_val', (subtotal.value * newValue) / 100)
|
||||
} else {
|
||||
updateItemAttribute('discount_val', Math.round(newValue * 100))
|
||||
}
|
||||
|
||||
updateItemAttribute('discount', newValue)
|
||||
},
|
||||
})
|
||||
|
||||
const total = computed(() => {
|
||||
return subtotal.value - props.itemData.discount_val
|
||||
})
|
||||
|
||||
const selectedCurrency = computed(() => {
|
||||
if (props.currency) {
|
||||
return props.currency
|
||||
} else {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
|
||||
const showRemoveButton = computed(() => {
|
||||
if (props.store[props.storeProp].items.length == 1) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const totalSimpleTax = computed(() => {
|
||||
return Math.round(
|
||||
sumBy(props.itemData.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const totalCompoundTax = computed(() => {
|
||||
return Math.round(
|
||||
sumBy(props.itemData.taxes, function (tax) {
|
||||
if (tax.compound_tax) {
|
||||
return tax.amount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const totalTax = computed(() => totalSimpleTax.value + totalCompoundTax.value)
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
quantity: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minValue: helpers.withMessage(
|
||||
t('validation.qty_must_greater_than_zero'),
|
||||
minValue(1)
|
||||
),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.amount_maxlength'),
|
||||
maxLength(20)
|
||||
),
|
||||
},
|
||||
price: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minValue: helpers.withMessage(
|
||||
t('validation.number_length_minvalue'),
|
||||
minValue(1)
|
||||
),
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.price_maxlength'),
|
||||
maxLength(20)
|
||||
),
|
||||
},
|
||||
discount_val: {
|
||||
between: helpers.withMessage(
|
||||
t('validation.discount_maxlength'),
|
||||
between(
|
||||
0,
|
||||
computed(() => subtotal.value)
|
||||
)
|
||||
),
|
||||
},
|
||||
description: {
|
||||
maxLength: helpers.withMessage(
|
||||
t('validation.notes_maxlength'),
|
||||
maxLength(65000)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const v$ = useVuelidate(
|
||||
rules,
|
||||
computed(() => props.store[props.storeProp].items[props.index]),
|
||||
{ $scope: props.itemValidationScope }
|
||||
)
|
||||
|
||||
//
|
||||
// if (
|
||||
// route.params.id &&
|
||||
// (props.store[props.storeProp].tax_per_item === 'YES' || 'NO')
|
||||
// ) {
|
||||
// if (props.store[props.storeProp].items[props.index].taxes === undefined) {
|
||||
// props.store.$patch((state) => {
|
||||
// state[props.storeProp].items[props.index].taxes = [
|
||||
// { ...TaxStub, id: Guid.raw() },
|
||||
// ]
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
function updateTax(data) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index]['taxes'][data.index] = data.item
|
||||
})
|
||||
|
||||
let lastTax = props.itemData.taxes[props.itemData.taxes.length - 1]
|
||||
|
||||
if (lastTax?.tax_type_id !== 0) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index].taxes.push({
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
syncItemToStore()
|
||||
}
|
||||
|
||||
function searchVal(val) {
|
||||
updateItemAttribute('name', val)
|
||||
}
|
||||
|
||||
function onSelectItem(itm) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index].name = itm.name
|
||||
state[props.storeProp].items[props.index].price = itm.price
|
||||
state[props.storeProp].items[props.index].item_id = itm.id
|
||||
state[props.storeProp].items[props.index].description = itm.description
|
||||
|
||||
if (itm.unit) {
|
||||
state[props.storeProp].items[props.index].unit_name = itm.unit.name
|
||||
}
|
||||
|
||||
if (props.store[props.storeProp].tax_per_item === 'YES' && itm.taxes) {
|
||||
let index = 0
|
||||
|
||||
itm.taxes.forEach((tax) => {
|
||||
updateTax({ index, item: { ...tax } })
|
||||
index++
|
||||
})
|
||||
}
|
||||
|
||||
if (state[props.storeProp].exchange_rate) {
|
||||
state[props.storeProp].items[props.index].price /=
|
||||
state[props.storeProp].exchange_rate
|
||||
}
|
||||
})
|
||||
|
||||
itemStore.fetchItems()
|
||||
syncItemToStore()
|
||||
}
|
||||
|
||||
function selectFixed() {
|
||||
if (props.itemData.discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
|
||||
updateItemAttribute('discount_val', Math.round(props.itemData.discount * 100))
|
||||
updateItemAttribute('discount_type', 'fixed')
|
||||
}
|
||||
|
||||
function selectPercentage() {
|
||||
if (props.itemData.discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
|
||||
updateItemAttribute(
|
||||
'discount_val',
|
||||
(subtotal.value * props.itemData.discount) / 100
|
||||
)
|
||||
|
||||
updateItemAttribute('discount_type', 'percentage')
|
||||
}
|
||||
|
||||
function syncItemToStore() {
|
||||
let itemTaxes = props.store[props.storeProp]?.items[props.index]?.taxes
|
||||
|
||||
if (!itemTaxes) {
|
||||
itemTaxes = []
|
||||
}
|
||||
|
||||
let data = {
|
||||
...props.store[props.storeProp].items[props.index],
|
||||
index: props.index,
|
||||
total: total.value,
|
||||
sub_total: subtotal.value,
|
||||
totalSimpleTax: totalSimpleTax.value,
|
||||
totalCompoundTax: totalCompoundTax.value,
|
||||
totalTax: totalTax.value,
|
||||
tax: totalTax.value,
|
||||
taxes: [...itemTaxes],
|
||||
}
|
||||
|
||||
props.store.updateItem(data)
|
||||
}
|
||||
|
||||
function updateItemAttribute(attribute, value) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.index][attribute] = value
|
||||
})
|
||||
|
||||
syncItemToStore()
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center text-base" style="flex: 4">
|
||||
<label class="pr-2 mb-0" align="right">
|
||||
{{ $t('invoices.item.tax') }}
|
||||
</label>
|
||||
|
||||
<BaseMultiselect
|
||||
v-model="selectedTax"
|
||||
value-prop="id"
|
||||
:options="filteredTypes"
|
||||
:placeholder="$t('general.select_a_tax')"
|
||||
open-direction="top"
|
||||
track-by="name"
|
||||
searchable
|
||||
object
|
||||
label="name"
|
||||
@update:modelValue="(val) => onSelectTax(val)"
|
||||
>
|
||||
<template #singlelabel="{ value }">
|
||||
<div class="absolute left-3.5">
|
||||
{{ value.name }} - {{ value.percent }} %
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #option="{ option }">
|
||||
{{ option.name }} - {{ option.percent }} %
|
||||
</template>
|
||||
|
||||
<template v-if="userStore.hasAbilities(ability)" #action>
|
||||
<button
|
||||
type="button"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
px-2
|
||||
cursor-pointer
|
||||
py-2
|
||||
bg-gray-200
|
||||
border-none
|
||||
outline-none
|
||||
"
|
||||
@click="openTaxModal"
|
||||
>
|
||||
<BaseIcon name="CheckCircleIcon" class="h-5 text-primary-400" />
|
||||
|
||||
<label
|
||||
class="ml-2 text-sm leading-none text-primary-400 cursor-pointer"
|
||||
>{{ $t('invoices.add_new_tax') }}</label
|
||||
>
|
||||
</button>
|
||||
</template>
|
||||
</BaseMultiselect>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-right" style="flex: 3">
|
||||
<BaseFormatMoney :amount="taxAmount" :currency="currency" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center w-6 h-10 mx-2 cursor-pointer">
|
||||
<BaseIcon
|
||||
v-if="taxes.length && index !== taxes.length - 1"
|
||||
name="TrashIcon"
|
||||
class="h-5 text-gray-700 cursor-pointer"
|
||||
@click="removeTax(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, inject, reactive, watch } from 'vue'
|
||||
import { useTaxTypeStore } from '@/scripts/stores/tax-type'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/scripts/stores/user'
|
||||
|
||||
const props = defineProps({
|
||||
ability: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
taxData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalTax: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
updateItems: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['remove', 'update'])
|
||||
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const modalStore = useModalStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const selectedTax = ref(null)
|
||||
const localTax = reactive({ ...props.taxData })
|
||||
const utils = inject('utils')
|
||||
const { t } = useI18n()
|
||||
|
||||
const filteredTypes = computed(() => {
|
||||
const clonedTypes = taxTypeStore.taxTypes.map((a) => ({ ...a }))
|
||||
|
||||
return clonedTypes.map((taxType) => {
|
||||
let found = props.taxes.find((tax) => tax.tax_type_id === taxType.id)
|
||||
|
||||
if (found) {
|
||||
taxType.disabled = true
|
||||
} else {
|
||||
taxType.disabled = false
|
||||
}
|
||||
|
||||
return taxType
|
||||
})
|
||||
})
|
||||
|
||||
const taxAmount = computed(() => {
|
||||
if (localTax.compound_tax && props.total) {
|
||||
return ((props.total + props.totalTax) * localTax.percent) / 100
|
||||
}
|
||||
|
||||
if (props.total && localTax.percent) {
|
||||
return (props.total * localTax.percent) / 100
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.total,
|
||||
() => {
|
||||
updateRowTax()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.totalTax,
|
||||
() => {
|
||||
updateRowTax()
|
||||
}
|
||||
)
|
||||
|
||||
// Set SelectedTax
|
||||
if (props.taxData.tax_type_id > 0) {
|
||||
selectedTax.value = taxTypeStore.taxTypes.find(
|
||||
(_type) => _type.id === props.taxData.tax_type_id
|
||||
)
|
||||
}
|
||||
|
||||
updateRowTax()
|
||||
|
||||
function onSelectTax(val) {
|
||||
localTax.percent = val.percent
|
||||
localTax.tax_type_id = val.id
|
||||
localTax.compound_tax = val.compound_tax
|
||||
localTax.name = val.name
|
||||
|
||||
updateRowTax()
|
||||
}
|
||||
|
||||
function updateRowTax() {
|
||||
if (localTax.tax_type_id === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('update', {
|
||||
index: props.index,
|
||||
item: {
|
||||
...localTax,
|
||||
amount: taxAmount.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openTaxModal() {
|
||||
let data = {
|
||||
itemIndex: props.itemIndex,
|
||||
taxIndex: props.index,
|
||||
}
|
||||
|
||||
modalStore.openModal({
|
||||
title: t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
data: data,
|
||||
size: 'sm',
|
||||
})
|
||||
}
|
||||
|
||||
function removeTax(index) {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].items[props.itemIndex].taxes.splice(index, 1)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<table class="text-center item-table min-w-full">
|
||||
<colgroup>
|
||||
<col style="width: 40%; min-width: 280px" />
|
||||
<col style="width: 10%; min-width: 120px" />
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
<col
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
style="width: 15%; min-width: 160px"
|
||||
/>
|
||||
<col style="width: 15%; min-width: 120px" />
|
||||
</colgroup>
|
||||
<thead class="bg-white border border-gray-200 border-solid">
|
||||
<tr>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else class="pl-7">
|
||||
{{ $tc('items.item', 2) }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-right text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else>
|
||||
{{ $t('invoices.item.quantity') }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else>
|
||||
{{ $t('invoices.item.price') }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
v-if="store[storeProp].discount_per_item === 'YES'"
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-left text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else>
|
||||
{{ $t('invoices.item.discount') }}
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="
|
||||
px-5
|
||||
py-3
|
||||
text-sm
|
||||
not-italic
|
||||
font-medium
|
||||
leading-5
|
||||
text-right text-gray-700
|
||||
border-t border-b border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<span v-else class="pr-10 column-heading">
|
||||
{{ $t('invoices.item.amount') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable
|
||||
v-model="store[storeProp].items"
|
||||
item-key="id"
|
||||
tag="tbody"
|
||||
handle=".handle"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<Item
|
||||
:key="element.id"
|
||||
:index="index"
|
||||
:item-data="element"
|
||||
:loading="isLoading"
|
||||
:currency="defaultCurrency"
|
||||
:item-validation-scope="itemValidationScope"
|
||||
:invoice-items="store[storeProp].items"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</table>
|
||||
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
px-6
|
||||
py-3
|
||||
text-base
|
||||
border border-t-0 border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
text-primary-400
|
||||
hover:bg-primary-100
|
||||
"
|
||||
@click="store.addItem"
|
||||
>
|
||||
<BaseIcon name="PlusCircleIcon" class="mr-2" />
|
||||
{{ $t('general.add_new_item') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { computed } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import Item from './CreateItemRow.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String, null],
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
itemValidationScope: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const defaultCurrency = computed(() => {
|
||||
if (props.currency) {
|
||||
return props.currency
|
||||
} else {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="mb-6">
|
||||
<div
|
||||
class="z-20 text-sm font-semibold leading-5 text-primary-400 float-right"
|
||||
>
|
||||
<SelectNotePopup :type="type" @select="onSelectNote" />
|
||||
</div>
|
||||
<label class="text-primary-800 font-medium mb-4 text-sm">
|
||||
{{ $t('invoices.notes') }}
|
||||
</label>
|
||||
<BaseCustomInput
|
||||
v-model="store[storeProp].notes"
|
||||
:content-loading="store.isFetchingInitialSettings"
|
||||
:fields="fields"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import SelectNotePopup from '../SelectNotePopup.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fields: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
function onSelectNote(data) {
|
||||
props.store[props.storeProp].notes = '' + data.notes
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<div
|
||||
class="
|
||||
px-5
|
||||
py-4
|
||||
mt-6
|
||||
bg-white
|
||||
border border-gray-200 border-solid
|
||||
rounded
|
||||
md:min-w-[390px]
|
||||
min-w-[300px]
|
||||
lg:mt-7
|
||||
"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="text-sm font-semibold leading-5 text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t('estimates.sub_total') }}
|
||||
</label>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<label
|
||||
v-else
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
m-0
|
||||
text-lg text-black
|
||||
uppercase
|
||||
"
|
||||
>
|
||||
<BaseFormatMoney
|
||||
:amount="store.getSubTotal"
|
||||
:currency="defaultCurrency"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="tax in itemWiseTaxes"
|
||||
:key="tax.tax_type_id"
|
||||
class="flex items-center justify-between w-full"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else-if="store[storeProp].tax_per_item === 'YES'"
|
||||
class="m-0 text-sm font-semibold leading-5 text-gray-500 uppercase"
|
||||
>
|
||||
{{ tax.name }} - {{ tax.percent }}%
|
||||
</label>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<label
|
||||
v-else-if="store[storeProp].tax_per_item === 'YES'"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
m-0
|
||||
text-lg text-black
|
||||
uppercase
|
||||
"
|
||||
>
|
||||
<BaseFormatMoney :amount="tax.amount" :currency="defaultCurrency" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].discount_per_item === 'NO' ||
|
||||
store[storeProp].discount_per_item === null
|
||||
"
|
||||
class="flex items-center justify-between w-full mt-2"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="text-sm font-semibold leading-5 text-gray-400 uppercase"
|
||||
>
|
||||
{{ $t('estimates.discount') }}
|
||||
</label>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText
|
||||
:lines="1"
|
||||
class="w-24 h-8 rounded-md border"
|
||||
/>
|
||||
</BaseContentPlaceholders>
|
||||
<div v-else class="flex" style="width: 140px" role="group">
|
||||
<BaseInput
|
||||
v-model="totalDiscount"
|
||||
class="
|
||||
border-r-0
|
||||
focus:border-r-2
|
||||
rounded-tr-sm rounded-br-sm
|
||||
h-[38px]
|
||||
"
|
||||
/>
|
||||
<BaseDropdown position="bottom-end">
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
class="rounded-tr-md rounded-br-md p-2 rounded-none"
|
||||
type="button"
|
||||
variant="white"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
{{
|
||||
store[storeProp].discount_type == 'fixed'
|
||||
? defaultCurrency.symbol
|
||||
: '%'
|
||||
}}
|
||||
|
||||
<BaseIcon
|
||||
name="ChevronDownIcon"
|
||||
class="w-4 h-4 text-gray-500 ml-1"
|
||||
/>
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<BaseDropdownItem @click="selectFixed">
|
||||
{{ $t('general.fixed') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<BaseDropdownItem @click="selectPercentage">
|
||||
{{ $t('general.percentage') }}
|
||||
</BaseDropdownItem>
|
||||
</BaseDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].tax_per_item === 'NO' ||
|
||||
store[storeProp].tax_per_item === null
|
||||
"
|
||||
>
|
||||
<Tax
|
||||
v-for="(tax, index) in taxes"
|
||||
:key="tax.id"
|
||||
:index="index"
|
||||
:tax="tax"
|
||||
:taxes="taxes"
|
||||
:currency="currency"
|
||||
:store="store"
|
||||
@remove="removeTax"
|
||||
@update="updateTax"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].tax_per_item === 'NO' ||
|
||||
store[storeProp].tax_per_item === null
|
||||
"
|
||||
ref="taxModal"
|
||||
class="float-right pt-2 pb-4"
|
||||
>
|
||||
<SelectTaxPopup
|
||||
:store-prop="storeProp"
|
||||
:store="store"
|
||||
:type="taxPopupType"
|
||||
@select:taxType="onSelectTax"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-between
|
||||
w-full
|
||||
pt-2
|
||||
mt-5
|
||||
border-t border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="m-0 text-sm font-semibold leading-5 text-gray-400 uppercase"
|
||||
>{{ $t('estimates.total') }} {{ $t('estimates.amount') }}:</label
|
||||
>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
text-lg
|
||||
uppercase
|
||||
text-primary-400
|
||||
"
|
||||
>
|
||||
<BaseFormatMoney :amount="store.getTotal" :currency="defaultCurrency" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import Guid from 'guid'
|
||||
import Tax from './CreateTotalTaxes.vue'
|
||||
import TaxStub from '@/scripts/stub/tax'
|
||||
import SelectTaxPopup from './SelectTaxPopup.vue'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
|
||||
const taxModal = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
taxPopupType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const utils = inject('$utils')
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const totalDiscount = computed({
|
||||
get: () => {
|
||||
return props.store[props.storeProp].discount
|
||||
},
|
||||
set: (newValue) => {
|
||||
if (props.store[props.storeProp].discount_type === 'percentage') {
|
||||
props.store[props.storeProp].discount_val = Math.round(
|
||||
(props.store.getSubTotal * newValue) / 100
|
||||
)
|
||||
} else {
|
||||
props.store[props.storeProp].discount_val = Math.round(newValue * 100)
|
||||
}
|
||||
props.store[props.storeProp].discount = newValue
|
||||
},
|
||||
})
|
||||
|
||||
const taxes = computed({
|
||||
get: () => props.store[props.storeProp].taxes,
|
||||
set: (value) => {
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].taxes = value
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const itemWiseTaxes = computed(() => {
|
||||
let taxes = []
|
||||
props.store[props.storeProp].items.forEach((item) => {
|
||||
if (item.taxes) {
|
||||
item.taxes.forEach((tax) => {
|
||||
let found = taxes.find((_tax) => {
|
||||
return _tax.tax_type_id === tax.tax_type_id
|
||||
})
|
||||
if (found) {
|
||||
found.amount += tax.amount
|
||||
} else if (tax.tax_type_id) {
|
||||
taxes.push({
|
||||
tax_type_id: tax.tax_type_id,
|
||||
amount: tax.amount,
|
||||
percent: tax.percent,
|
||||
name: tax.name,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return taxes
|
||||
})
|
||||
|
||||
const defaultCurrency = computed(() => {
|
||||
if (props.currency) {
|
||||
return props.currency
|
||||
} else {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
|
||||
function selectFixed() {
|
||||
if (props.store[props.storeProp].discount_type === 'fixed') {
|
||||
return
|
||||
}
|
||||
props.store[props.storeProp].discount_val = Math.round(
|
||||
props.store[props.storeProp].discount * 100
|
||||
)
|
||||
props.store[props.storeProp].discount_type = 'fixed'
|
||||
}
|
||||
|
||||
function selectPercentage() {
|
||||
if (props.store[props.storeProp].discount_type === 'percentage') {
|
||||
return
|
||||
}
|
||||
props.store[props.storeProp].discount_val =
|
||||
(props.store.getSubTotal * props.store[props.storeProp].discount) / 100
|
||||
props.store[props.storeProp].discount_type = 'percentage'
|
||||
}
|
||||
|
||||
function onSelectTax(selectedTax) {
|
||||
let amount = 0
|
||||
if (selectedTax.compound_tax && props.store.getSubtotalWithDiscount) {
|
||||
amount = Math.round(
|
||||
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
|
||||
selectedTax.percent) /
|
||||
100
|
||||
)
|
||||
} else if (props.store.getSubtotalWithDiscount && selectedTax.percent) {
|
||||
amount = Math.round(
|
||||
(props.store.getSubtotalWithDiscount * selectedTax.percent) / 100
|
||||
)
|
||||
}
|
||||
|
||||
let data = {
|
||||
...TaxStub,
|
||||
id: Guid.raw(),
|
||||
name: selectedTax.name,
|
||||
percent: selectedTax.percent,
|
||||
compound_tax: selectedTax.compound_tax,
|
||||
tax_type_id: selectedTax.id,
|
||||
amount,
|
||||
}
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].taxes.push({ ...data })
|
||||
})
|
||||
}
|
||||
|
||||
function updateTax(data) {
|
||||
const tax = props.store[props.storeProp].taxes.find(
|
||||
(tax) => tax.id === data.id
|
||||
)
|
||||
if (tax) {
|
||||
Object.assign(tax, { ...data })
|
||||
}
|
||||
}
|
||||
|
||||
function removeTax(id) {
|
||||
const index = props.store[props.storeProp].taxes.findIndex(
|
||||
(tax) => tax.id === id
|
||||
)
|
||||
|
||||
props.store.$patch((state) => {
|
||||
state[props.storeProp].taxes.splice(index, 1)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<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="flex items-center justify-center text-lg text-black">
|
||||
<BaseFormatMoney :amount="tax.amount" :currency="currency" />
|
||||
|
||||
<BaseIcon
|
||||
name="TrashIcon"
|
||||
class="h-5 ml-2 cursor-pointer"
|
||||
@click="$emit('remove', tax.id)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch, inject, watchEffect } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
tax: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
taxes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
required: true,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
data: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update', 'remove'])
|
||||
|
||||
const utils = inject('$utils')
|
||||
|
||||
const taxAmount = computed(() => {
|
||||
if (props.tax.compound_tax && props.store.getSubtotalWithDiscount) {
|
||||
return Math.round(
|
||||
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
|
||||
props.tax.percent) /
|
||||
100
|
||||
)
|
||||
}
|
||||
if (props.store.getSubtotalWithDiscount && props.tax.percent) {
|
||||
return Math.round(
|
||||
(props.store.getSubtotalWithDiscount * props.tax.percent) / 100
|
||||
)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.store.getSubtotalWithDiscount) {
|
||||
updateTax()
|
||||
}
|
||||
if (props.store.getTotalSimpleTax) {
|
||||
updateTax()
|
||||
}
|
||||
})
|
||||
|
||||
function updateTax() {
|
||||
emit('update', {
|
||||
...props.tax,
|
||||
amount: taxAmount.value,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<BaseInputGroup
|
||||
v-if="store.showExchangeRate && selectedCurrency"
|
||||
:content-loading="isFetching && !isEdit"
|
||||
:label="$t('settings.exchange_rate.exchange_rate')"
|
||||
:error="v.exchange_rate.$error && v.exchange_rate.$errors[0].$message"
|
||||
required
|
||||
>
|
||||
<template #labelRight>
|
||||
<div v-if="hasActiveProvider && isEdit">
|
||||
<BaseIcon
|
||||
v-tooltip="{ content: 'Fetch Latest Exchange rate' }"
|
||||
name="RefreshIcon"
|
||||
:class="`h-4 w-4 text-primary-500 cursor-pointer outline-none ${
|
||||
isFetching
|
||||
? ' animate-spin transform rotate-180 cursor-not-allowed pointer-events-none '
|
||||
: ''
|
||||
}`"
|
||||
@click="getCurrenctExchangeRate(customerCurrency)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<BaseInput
|
||||
v-model="store[storeProp].exchange_rate"
|
||||
:content-loading="isFetching && !isEdit"
|
||||
:addon="`1 ${selectedCurrency.code} =`"
|
||||
:disabled="isFetching"
|
||||
@input="v.exchange_rate.$touch()"
|
||||
>
|
||||
<template #right>
|
||||
<span class="text-gray-500 sm:text-sm">
|
||||
{{ companyCurrency.code }}
|
||||
</span>
|
||||
</template>
|
||||
</BaseInput>
|
||||
<span class="text-gray-400 text-xs mt-2 font-light">
|
||||
{{
|
||||
$t('settings.exchange_rate.exchange_help_text', {
|
||||
currency: selectedCurrency.code,
|
||||
baseCurrency: companyCurrency.code,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch, computed, ref, onMounted } from 'vue'
|
||||
import { useGlobalStore } from '@/scripts/stores/global'
|
||||
import { useCompanyStore } from '@/scripts/stores/company'
|
||||
import { useExchangeRateStore } from '@/scripts/stores/exchange-rate'
|
||||
|
||||
const props = defineProps({
|
||||
v: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
customerCurrency: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
const globalStore = useGlobalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const exchangeRateStore = useExchangeRateStore()
|
||||
const hasActiveProvider = ref(false)
|
||||
let isFetching = ref(false)
|
||||
|
||||
globalStore.fetchCurrencies()
|
||||
|
||||
const companyCurrency = computed(() => {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
})
|
||||
|
||||
const selectedCurrency = computed(() => {
|
||||
return globalStore.currencies.find(
|
||||
(c) => c.id === props.store[props.storeProp].currency_id
|
||||
)
|
||||
})
|
||||
|
||||
const isCurrencyDiffrent = computed(() => {
|
||||
return companyCurrency.value.id !== props.customerCurrency
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.store[props.storeProp].customer,
|
||||
(v) => {
|
||||
setCustomerCurrency(v)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.store[props.storeProp].currency_id,
|
||||
(v) => {
|
||||
onChangeCurrency(v)
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.customerCurrency,
|
||||
(v) => {
|
||||
if (v && props.isEdit) {
|
||||
checkForActiveProvider(v)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function checkForActiveProvider() {
|
||||
if (isCurrencyDiffrent.value) {
|
||||
exchangeRateStore
|
||||
.checkForActiveProvider(props.customerCurrency)
|
||||
.then((res) => {
|
||||
if (res.data.success) {
|
||||
hasActiveProvider.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setCustomerCurrency(v) {
|
||||
if (v) {
|
||||
props.store[props.storeProp].currency_id = v.currency.id
|
||||
} else {
|
||||
props.store[props.storeProp].currency_id = companyCurrency.value.id
|
||||
}
|
||||
}
|
||||
|
||||
async function onChangeCurrency(v) {
|
||||
if (v !== companyCurrency.value.id) {
|
||||
if (!props.isEdit) {
|
||||
await getCurrenctExchangeRate(v)
|
||||
}
|
||||
|
||||
props.store.showExchangeRate = true
|
||||
} else {
|
||||
props.store.showExchangeRate = false
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrenctExchangeRate(v) {
|
||||
isFetching.value = true
|
||||
exchangeRateStore
|
||||
.getCurrentExchangeRate(v)
|
||||
.then((res) => {
|
||||
if (res.data && !res.data.error) {
|
||||
props.store[props.storeProp].exchange_rate = res.data.exchangeRate[0]
|
||||
} else {
|
||||
props.store[props.storeProp].exchange_rate = ''
|
||||
}
|
||||
isFetching.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
isFetching.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="w-full mt-4 tax-select">
|
||||
<Popover v-slot="{ isOpen }" class="relative">
|
||||
<PopoverButton
|
||||
:class="isOpen ? '' : 'text-opacity-90'"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
text-sm
|
||||
font-medium
|
||||
text-primary-400
|
||||
focus:outline-none focus:border-none
|
||||
"
|
||||
>
|
||||
<BaseIcon
|
||||
name="PlusIcon"
|
||||
class="w-4 h-4 font-medium text-primary-400"
|
||||
/>
|
||||
{{ $t('settings.tax_types.add_tax') }}
|
||||
</PopoverButton>
|
||||
|
||||
<!-- Tax Select Popup -->
|
||||
<div class="relative w-full max-w-md px-4">
|
||||
<transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="translate-y-1 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-1 opacity-0"
|
||||
>
|
||||
<PopoverPanel
|
||||
v-slot="{ close }"
|
||||
style="min-width: 350px; margin-left: 62px; top: -28px"
|
||||
class="absolute z-10 px-4 py-2 transform -translate-x-full sm:px-0"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
overflow-hidden
|
||||
rounded-md
|
||||
shadow-lg
|
||||
ring-1 ring-black ring-opacity-5
|
||||
"
|
||||
>
|
||||
<!-- Tax Search Input -->
|
||||
|
||||
<div class="relative bg-white">
|
||||
<div class="relative p-4">
|
||||
<BaseInput
|
||||
v-model="textSearch"
|
||||
:placeholder="$t('general.search')"
|
||||
type="text"
|
||||
class="text-black"
|
||||
>
|
||||
</BaseInput>
|
||||
</div>
|
||||
|
||||
<!-- List of Taxes -->
|
||||
<div
|
||||
v-if="filteredTaxType.length > 0"
|
||||
class="
|
||||
relative
|
||||
flex flex-col
|
||||
overflow-auto
|
||||
list
|
||||
max-h-36
|
||||
border-t border-gray-200
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(taxType, index) in filteredTaxType"
|
||||
:key="index"
|
||||
:class="{
|
||||
'bg-gray-100 cursor-not-allowed opacity-50 pointer-events-none':
|
||||
taxes.find((val) => {
|
||||
return val.tax_type_id === taxType.id
|
||||
}),
|
||||
}"
|
||||
tabindex="2"
|
||||
class="
|
||||
px-6
|
||||
py-4
|
||||
border-b border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
hover:bg-gray-100 hover:cursor-pointer
|
||||
last:border-b-0
|
||||
"
|
||||
@click="selectTaxType(taxType, close)"
|
||||
>
|
||||
<div class="flex justify-between px-2">
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
text-base
|
||||
font-semibold
|
||||
leading-tight
|
||||
text-gray-700
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
{{ taxType.name }}
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
text-base
|
||||
font-semibold
|
||||
text-gray-700
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
{{ taxType.percent }} %
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex justify-center p-5 text-gray-400">
|
||||
<label class="text-base text-gray-500 cursor-pointer">
|
||||
{{ $t('general.no_tax_found') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add new Tax action -->
|
||||
<button
|
||||
v-if="userStore.hasAbilities(abilities.CREATE_TAX_TYPE)"
|
||||
type="button"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
h-10
|
||||
px-2
|
||||
py-3
|
||||
bg-gray-200
|
||||
border-none
|
||||
outline-none
|
||||
"
|
||||
@click="openTaxTypeModal"
|
||||
>
|
||||
<BaseIcon name="CheckCircleIcon" class="text-primary-400" />
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
ml-3
|
||||
text-sm
|
||||
leading-none
|
||||
cursor-pointer
|
||||
font-base
|
||||
text-primary-400
|
||||
"
|
||||
>
|
||||
{{ $t('estimates.add_new_tax') }}
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</transition>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||
import { computed, ref, inject, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useEstimateStore } from '@/scripts/stores/estimate'
|
||||
import { useInvoiceStore } from '@/scripts/stores/invoice'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useTaxTypeStore } from '@/scripts/stores/tax-type'
|
||||
import { useUserStore } from '@/scripts/stores/user'
|
||||
import abilities from '@/scripts/stub/abilities'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select:taxType'])
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
const textSearch = ref(null)
|
||||
|
||||
const filteredTaxType = computed(() => {
|
||||
if (textSearch.value) {
|
||||
return taxTypeStore.taxTypes.filter(function (el) {
|
||||
return (
|
||||
el.name.toLowerCase().indexOf(textSearch.value.toLowerCase()) !== -1
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return taxTypeStore.taxTypes
|
||||
}
|
||||
})
|
||||
|
||||
const taxes = computed(() => {
|
||||
return props.store[props.storeProp].taxes
|
||||
})
|
||||
|
||||
function selectTaxType(data, close) {
|
||||
emit('select:taxType', { ...data })
|
||||
close()
|
||||
}
|
||||
|
||||
function openTaxTypeModal() {
|
||||
modalStore.openModal({
|
||||
title: t('settings.tax_types.add_tax'),
|
||||
componentName: 'TaxTypeModal',
|
||||
size: 'sm',
|
||||
refreshData: (data) => emit('select:taxType', data),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<label class="flex text-primary-800 font-medium text-sm mb-2">
|
||||
{{ $t('general.select_template') }}
|
||||
<span class="text-sm text-red-500"> *</span>
|
||||
</label>
|
||||
<BaseButton
|
||||
type="button"
|
||||
class="flex justify-center w-full text-sm lg:w-auto hover:bg-gray-200"
|
||||
variant="gray"
|
||||
@click="openTemplateModal"
|
||||
>
|
||||
<template #right="slotProps">
|
||||
<BaseIcon name="PencilIcon" :class="slotProps.class" />
|
||||
</template>
|
||||
{{ store[storeProp].template_name }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function openTemplateModal() {
|
||||
modalStore.openModal({
|
||||
title: t('general.choose_template'),
|
||||
componentName: 'SelectTemplate',
|
||||
data: {
|
||||
templates: props.store.templates,
|
||||
store: props.store,
|
||||
storeProp: props.storeProp,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user