Files
crater/resources/scripts/components/estimate-invoice-common/CreateItemRow.vue
2021-12-08 16:27:18 +05:30

515 lines
14 KiB
Vue

<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="0.1"
step="any"
@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', parseFloat(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(0.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>