320 lines
7.6 KiB
Vue
320 lines
7.6 KiB
Vue
<script lang="ts" setup>
|
|
import { computed, watch } from 'vue';
|
|
import { precisionRound } from 'src/utils/arithmetic';
|
|
import ThaiBahtText from 'thai-baht-text';
|
|
import { toWords } from 'number-to-words';
|
|
|
|
import TableComponents from 'src/components/TableComponents.vue';
|
|
import { QuotationPayload } from 'src/stores/quotations/types';
|
|
import { formatNumberDecimal } from 'stores/utils';
|
|
import { QTableProps } from 'quasar';
|
|
|
|
defineProps<{
|
|
agentPrice: boolean;
|
|
}>();
|
|
|
|
defineEmits<{
|
|
(e: 'delete', index: number): void;
|
|
}>();
|
|
|
|
const rows = defineModel<
|
|
Required<QuotationPayload['productServiceList'][number]>[]
|
|
>('rows', { required: true });
|
|
|
|
const summaryPrice = defineModel<{
|
|
totalPrice: number;
|
|
totalDiscount: number;
|
|
vat: number;
|
|
finalPrice: number;
|
|
}>('summaryPrice', {
|
|
required: true,
|
|
default: {
|
|
totalPrice: 0,
|
|
totalDiscount: 0,
|
|
vat: 0,
|
|
finalPrice: 0,
|
|
},
|
|
});
|
|
|
|
function calcPrice(c: (typeof rows.value)[number]) {
|
|
const price = precisionRound(c.pricePerUnit * c.amount);
|
|
const discount = precisionRound(price * (c.discount || 0));
|
|
const vat = precisionRound((price - discount) * c.vat);
|
|
|
|
return precisionRound(price - discount + vat);
|
|
}
|
|
|
|
const summary = computed(() =>
|
|
rows.value.reduce(
|
|
(a, c) => {
|
|
const price = precisionRound(c.pricePerUnit * c.amount);
|
|
const discount = precisionRound(price - (c.discount || 0));
|
|
const vat = precisionRound((price - discount) * c.vat);
|
|
|
|
a.totalPrice = precisionRound(a.totalPrice + price);
|
|
a.totalDiscount = precisionRound(a.totalDiscount + discount);
|
|
a.vat = precisionRound(a.vat + vat);
|
|
a.finalPrice = precisionRound(a.totalPrice - a.totalDiscount + a.vat);
|
|
|
|
return a;
|
|
},
|
|
{
|
|
totalPrice: 0,
|
|
totalDiscount: 0,
|
|
vat: 0,
|
|
finalPrice: 0,
|
|
},
|
|
),
|
|
);
|
|
|
|
const columns = [
|
|
{
|
|
name: 'order',
|
|
align: 'center',
|
|
label: 'general.order',
|
|
field: 'order',
|
|
},
|
|
{
|
|
name: 'code',
|
|
align: 'center',
|
|
label: 'productService.group.code',
|
|
field: (v) => v.product.code,
|
|
},
|
|
{
|
|
name: 'name',
|
|
align: 'left',
|
|
label: 'productService.service.list',
|
|
field: (v) => v.product.name,
|
|
},
|
|
{
|
|
name: 'amount',
|
|
align: 'center',
|
|
label: 'general.amount',
|
|
field: 'amount',
|
|
},
|
|
{
|
|
name: 'pricePerUnit',
|
|
align: 'right',
|
|
label: 'quotation.pricePerUnit',
|
|
field: 'pricePerUnit',
|
|
},
|
|
{
|
|
name: 'discount',
|
|
align: 'center',
|
|
label: 'general.discount',
|
|
field: 'discount',
|
|
},
|
|
{
|
|
name: 'tax',
|
|
align: 'center',
|
|
label: 'quotation.tax',
|
|
field: 'tax',
|
|
},
|
|
{
|
|
name: 'sumPrice',
|
|
align: 'right',
|
|
label: 'quotation.sumPrice',
|
|
field: 'sumPrice',
|
|
},
|
|
{
|
|
name: 'action',
|
|
align: 'left',
|
|
label: '',
|
|
field: 'action',
|
|
},
|
|
] satisfies QTableProps['columns'];
|
|
|
|
const EngBahtText = (number: number) => {
|
|
const [baht, satang] = number.toString().split('.');
|
|
return `${toWords(baht)} Baht${satang && ` and ${toWords(satang)} Satang`}`;
|
|
};
|
|
|
|
watch(
|
|
() => summary.value,
|
|
() => {
|
|
summaryPrice.value = summary.value;
|
|
},
|
|
);
|
|
</script>
|
|
<template>
|
|
<div class="column">
|
|
<div class="full-width">
|
|
<TableComponents
|
|
flat
|
|
bordered
|
|
hidePagination
|
|
button-delete
|
|
:columns="columns"
|
|
:rows="rows"
|
|
:customColumn="[
|
|
'name',
|
|
'amount',
|
|
'pricePerUnit',
|
|
'discount',
|
|
'tax',
|
|
'sumPrice',
|
|
]"
|
|
@delete="(i) => $emit('delete', i)"
|
|
>
|
|
<template v-slot:body-cell-name="{ props }">
|
|
<q-td>
|
|
<q-avatar class="q-mr-sm" size="md">
|
|
<q-icon
|
|
class="full-width full-height"
|
|
name="mdi-shopping-outline"
|
|
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
|
/>
|
|
</q-avatar>
|
|
{{ props.row.product.name }}
|
|
</q-td>
|
|
</template>
|
|
|
|
<template v-slot:body-cell-amount="{ props }">
|
|
<q-td align="center">
|
|
<q-input
|
|
dense
|
|
outlined
|
|
type="number"
|
|
style="width: 70px"
|
|
min="0"
|
|
v-model="props.row.amount"
|
|
/>
|
|
</q-td>
|
|
</template>
|
|
|
|
<template v-slot:body-cell-pricePerUnit="{ props }">
|
|
<q-td align="right">
|
|
{{
|
|
formatNumberDecimal(
|
|
(props.row.pricePerUnit = agentPrice
|
|
? props.row.product.agentPrice
|
|
: props.row.product.price),
|
|
2,
|
|
)
|
|
}}
|
|
</q-td>
|
|
</template>
|
|
|
|
<template v-slot:body-cell-discount="{ props }">
|
|
<q-td align="center">
|
|
<q-input
|
|
dense
|
|
min="0"
|
|
outlined
|
|
type="number"
|
|
style="width: 70px"
|
|
v-model="props.row.discount"
|
|
/>
|
|
</q-td>
|
|
</template>
|
|
|
|
<template v-slot:body-cell-tax="{ props }">
|
|
<q-td align="center">
|
|
<q-input
|
|
dense
|
|
min="0"
|
|
outlined
|
|
type="number"
|
|
readonly
|
|
class="bordered rounded"
|
|
style="width: 70px"
|
|
v-model="props.row.vat"
|
|
>
|
|
<template v-slot:append>
|
|
<span class="text-caption no-padding">%</span>
|
|
</template>
|
|
</q-input>
|
|
</q-td>
|
|
</template>
|
|
|
|
<template v-slot:body-cell-sumPrice="{ props }">
|
|
<q-td align="right">
|
|
{{ formatNumberDecimal(calcPrice(props.row), 2) }}
|
|
</q-td>
|
|
</template>
|
|
|
|
<template #button>
|
|
<q-btn
|
|
icon="mdi-monitor"
|
|
size="sm"
|
|
class="rounded q-mr-xs"
|
|
padding="4px 8px"
|
|
dense
|
|
flat
|
|
style="
|
|
background-color: hsla(var(--positive-bg) / 0.1);
|
|
color: hsl(var(--positive-bg));
|
|
"
|
|
/>
|
|
</template>
|
|
</TableComponents>
|
|
</div>
|
|
|
|
<div
|
|
class="column q-ml-auto text-caption app-text-muted-2 q-pt-md"
|
|
style="width: 15vw"
|
|
>
|
|
<div class="row">
|
|
{{ $t('quotation.allProductPrice') }}
|
|
<span class="q-ml-auto">
|
|
{{ formatNumberDecimal(summary.totalPrice, 2) }} ฿
|
|
</span>
|
|
</div>
|
|
<div class="row">
|
|
{{ $t('general.discount') }}
|
|
<span class="q-ml-auto">
|
|
{{ formatNumberDecimal(summary.totalDiscount, 2) }} ฿
|
|
</span>
|
|
</div>
|
|
<div class="row">
|
|
{{ $t('quotation.tax') }}
|
|
<span class="q-ml-auto">
|
|
{{ formatNumberDecimal(summary.vat, 2) }} ฿
|
|
</span>
|
|
</div>
|
|
<q-separator spaced="md" />
|
|
<div class="row text-bold text-body2" style="color: var(--foreground)">
|
|
{{ $t('quotation.totalPrice') }}
|
|
<span class="q-ml-auto">
|
|
{{ formatNumberDecimal(summary.finalPrice, 2) }} ฿
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<span
|
|
v-if="summary.finalPrice"
|
|
class="text-caption app-text-muted-2 flex self-end"
|
|
>
|
|
({{
|
|
$i18n.locale === 'eng'
|
|
? EngBahtText(summary.finalPrice)
|
|
: ThaiBahtText(summary.finalPrice)
|
|
}})
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.worker-item {
|
|
--side-color: var(--brand-1);
|
|
position: relative;
|
|
overflow-x: hidden;
|
|
|
|
&.worker-item__female {
|
|
--side-color: hsl(var(--gender-female));
|
|
}
|
|
|
|
&.worker-item__male {
|
|
--side-color: hsl(var(--gender-male));
|
|
}
|
|
}
|
|
|
|
.worker-item::before {
|
|
position: absolute;
|
|
content: ' ';
|
|
left: 0;
|
|
width: 7px;
|
|
top: 0;
|
|
bottom: 0;
|
|
background: var(--side-color);
|
|
}
|
|
</style>
|