fix/refactor: quotation installment (#121)

* refactor/feat: i18n

* chore: clean log

* refactor: type

* refactor: installment and product table state relation

* refactor: handle split custom

---------

Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com>
This commit is contained in:
puriphatt 2024-12-06 11:01:52 +07:00 committed by GitHub
parent 57aabf1deb
commit 1b4c06b182
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 357 additions and 77 deletions

View file

@ -32,7 +32,12 @@ import { runOcr, parseResultMRZ } from 'src/utils/ocr';
// NOTE Import Types
import { View } from './types.ts';
import { QuotationPayload } from 'src/stores/quotations/types';
import {
PayCondition,
PaySplit,
ProductServiceList,
QuotationPayload,
} from 'src/stores/quotations/types';
import { EmployeeWorker } from 'src/stores/quotations/types';
import { Employee } from 'src/stores/employee/types';
import { Receipt } from 'src/stores/payment/types';
@ -136,6 +141,7 @@ const templateFormOption = ref<{ label: string; value: string }[]>([]);
const refSelectZoneEmployee = ref<InstanceType<typeof SelectZone>>();
const mrz = ref<Awaited<ReturnType<typeof parseResultMRZ>>>();
const toggleWorker = ref(true);
const tempTableProduct = ref<ProductServiceList[]>([]);
const tempPaySplitCount = ref(0);
const tempPaySplit = ref<
{ no: number; amount: number; name?: string; invoice?: boolean }[]
@ -165,6 +171,7 @@ const workerList = ref<Employee[]>([]);
const selectedProductGroup = ref('');
const selectedInstallmentNo = ref<number[]>([]);
const installmentAmount = ref<number>(0);
const selectedInstallment = ref();
const agentPrice = ref(false);
@ -492,7 +499,10 @@ async function convertInvoiceToSubmit() {
if (quotationFormData.value.id) {
invoiceFormData.value = {
installmentNo: selectedInstallmentNo.value,
amount: summaryPrice.value.finalPrice,
amount:
quotationFormData.value.payCondition === 'SplitCustom'
? installmentAmount.value
: summaryPrice.value.finalPrice,
quotationId: quotationFormData.value.id,
};
@ -668,9 +678,164 @@ function triggerProductServiceDialog() {
pageState.productServiceModal = true;
}
function handlePaySplitDiscount() {
if (readonly.value) return;
if (quotationFormData.value.payCondition === 'Split') {
// Calculate average discount
const paySplitArray = JSON.parse(JSON.stringify(tempPaySplit.value));
const numSplits = paySplitArray.length;
let totalDiscount = quotationFormData.value.discount || 0;
const averageDiscount = totalDiscount / numSplits;
// Adjust each amount
paySplitArray.forEach((split: PaySplit) => {
if (split.amount < averageDiscount) {
totalDiscount -= split.amount; // Subtract the full amount from totalDiscount
split.amount = 0; // Set the amount to 0
} else {
split.amount -= averageDiscount; // Subtract the average discount
totalDiscount -= averageDiscount; // Adjust remaining totalDiscount
}
});
// Distribute any remaining discount
if (totalDiscount > 0) {
paySplitArray.forEach((split: PaySplit) => {
if (totalDiscount === 0) return;
const deduction = Math.min(split.amount, totalDiscount);
split.amount -= deduction;
totalDiscount -= deduction;
});
}
// Update paySplit in quotationFormData
quotationFormData.value.paySplit = paySplitArray;
}
}
function handleChangePayType(type: PayCondition) {
if (
type === 'Split' &&
tempPaySplitCount.value &&
tempPaySplit.value &&
tempTableProduct.value
) {
quotationFormData.value.paySplitCount = tempPaySplitCount.value;
quotationFormData.value.paySplit = JSON.parse(
JSON.stringify(tempPaySplit.value),
);
productServiceList.value = productServiceList.value.map((item, index) => ({
...item,
installmentNo: tempTableProduct.value[index].installmentNo || 0,
}));
}
}
function handleUpdateProductTable(
data: QuotationPayload['productServiceList'][number],
opt?: {
newInstallmentNo: number;
},
) {
// calc price
const calc = (c: QuotationPayload['productServiceList'][number]) => {
const pricePerUnit = c.pricePerUnit || 0;
const discount = c.discount || 0;
return precisionRound(
pricePerUnit * c.amount -
discount +
(c.product.calcVat
? (pricePerUnit * c.amount - discount) * (config.value?.vat || 0.07)
: 0),
);
};
// installment
if (opt && opt.newInstallmentNo) {
const oldInstallmentNo = JSON.parse(JSON.stringify(data.installmentNo));
const oldPaySplit = quotationFormData.value.paySplit.find(
(v) => v.no === oldInstallmentNo,
);
const newPaySplit = quotationFormData.value.paySplit.find(
(v) => v.no === opt.newInstallmentNo,
);
if (oldPaySplit) oldPaySplit.amount = oldPaySplit.amount - calc(data);
if (newPaySplit) newPaySplit.amount = newPaySplit.amount + calc(data);
return;
}
// re calc price
const currTempPaySplit = tempPaySplit.value.find(
(v) => v.no === data.installmentNo,
);
const currPaySplit = quotationFormData.value.paySplit.find(
(v) => v.no === data.installmentNo,
);
const targetPaySplit = productService.value.filter(
(v) => v.installmentNo === data.installmentNo,
);
let targetPaySplitAmount: number[] = [];
targetPaySplitAmount = targetPaySplit.map((c) => {
return calc(c);
});
const totalSum = targetPaySplitAmount.reduce((sum, value) => sum + value, 0);
if (currTempPaySplit && currPaySplit) {
currTempPaySplit.amount = totalSum;
currPaySplit.amount = totalSum;
}
}
function toggleDeleteProduct(index: number) {
const currProduct = productServiceList.value[index];
let currPaySplit = quotationFormData.value.paySplit.find(
(v) => v.no === currProduct.installmentNo,
);
let currTempPaySplit = tempPaySplit.value.find(
(v) => v.no === currProduct.installmentNo,
);
// cal curr amount
if (currPaySplit && currTempPaySplit) {
const price = agentPrice.value
? currProduct.product.agentPrice
: currProduct.product.price;
const pricePerUnit = currProduct.product.vatIncluded
? precisionRound(price / (1 + (config.value?.vat || 0.07)))
: price;
const vat = precisionRound(
(pricePerUnit * currProduct.amount - currProduct.discount) *
(config.value?.vat || 0.07),
);
const finalPrice = precisionRound(
pricePerUnit * currProduct.amount +
vat -
Number(currProduct.discount || 0),
);
currTempPaySplit.amount = currPaySplit.amount - finalPrice;
currPaySplit.amount = currTempPaySplit.amount;
}
// product display
productServiceList.value.splice(index, 1);
productServiceList.value = [...productServiceList.value]; //
productServiceList.value = [...productServiceList.value];
// find split count
tempPaySplitCount.value = Math.max(
...productServiceList.value.map((v) => v.installmentNo || 0),
);
quotationFormData.value.paySplitCount = tempPaySplitCount.value;
// make split.length === split count
tempPaySplit.value.splice(quotationFormData.value.paySplitCount);
quotationFormData.value.paySplit.splice(
quotationFormData.value.paySplitCount,
);
}
async function assignWorkerToSelectedWorker() {
@ -678,6 +843,7 @@ async function assignWorkerToSelectedWorker() {
}
function convertToTable(nodes: Node[]) {
// TODO:
const _recursive = (v: Node): Node | Node[] => {
if (v.checked && v.children) return v.children.flatMap(_recursive);
if (v.checked) return v;
@ -703,6 +869,7 @@ function convertToTable(nodes: Node[]) {
});
productServiceList.value = list;
tempTableProduct.value = JSON.parse(JSON.stringify(list));
quotationFormData.value.paySplit = Array.apply(
null,
@ -907,7 +1074,18 @@ const productServiceNodes = ref<ProductTree>([]);
watch(
() => productServiceList.value,
() => {
productServiceNodes.value = quotationProductTree(productServiceList.value);
productServiceNodes.value = quotationProductTree(
productServiceList.value,
agentPrice.value,
config.value?.vat,
);
},
);
watch(
() => quotationFormData.value.discount,
() => {
handlePaySplitDiscount();
},
);
@ -1041,7 +1219,6 @@ async function getWorkerFromCriteria(
if (!ret) return false; // error, do not close dialog
// TODO: Merge employee into worker list
const deduplicate = ret.result.filter(
(a) => !selectedWorker.value.find((b) => a.id === b.id),
);
@ -1358,6 +1535,7 @@ async function getWorkerFromCriteria(
:installment-input="
quotationFormData.payCondition === 'SplitCustom'
"
:max-installment="quotationFormData.paySplitCount"
:readonly="readonly"
:agent-price="agentPrice"
:employee-rows="
@ -1386,8 +1564,11 @@ async function getWorkerFromCriteria(
@delete="toggleDeleteProduct"
:rows="productService"
@update:rows="
(v) => view === View.Quotation && (productServiceList = v)
(v) => {
view === View.Quotation && (productServiceList = v);
}
"
@update-table="handleUpdateProductTable"
/>
</div>
</q-expansion-item>
@ -1420,9 +1601,8 @@ async function getWorkerFromCriteria(
<template v-if="true">
<QuotationFormInfo
:view="view"
:installment-amount="installmentAmount"
:installment-no="selectedInstallmentNo"
:pay-split-count-fixed="tempPaySplitCount"
:pay-split-fixed="tempPaySplit"
v-model:pay-type="quotationFormData.payCondition"
v-model:pay-bank="payBank"
v-model:pay-split-count="quotationFormData.paySplitCount"
@ -1432,6 +1612,7 @@ async function getWorkerFromCriteria(
v-model:pay-bill-date="quotationFormData.payBillDate"
v-model:summary-price="summaryPrice"
class="q-mb-md"
@change-pay-type="handleChangePayType"
/>
</template>
</div>
@ -1642,9 +1823,24 @@ async function getWorkerFromCriteria(
selectedInstallmentNo = selectedInstallment.map(
(value: any) => value.no,
);
installmentAmount = selectedInstallment.reduce(
(total: number, value: any) => total + value.amount,
0,
);
}
"
row-key="no"
:no-data-label="$t('general.noDataTable')"
:rows-per-page-options="[0]"
hide-pagination
:selected-rows-label="
(n) =>
$t('general.selected', {
number: n,
msg: $t('productService.product.product'),
})
"
>
<template v-slot:header="props">
<q-tr
@ -1670,7 +1866,6 @@ async function getWorkerFromCriteria(
@click="
async () => {
if (props.row.invoiceId) {
// TODO: get invoice code
await getInvoiceCode(props.row.invoiceId);
selectedInstallmentNo =
@ -1680,6 +1875,8 @@ async function getWorkerFromCriteria(
v.invoiceId === props.row.invoiceId,
)
.map((v) => v.no) || [];
installmentAmount = props.row.amount;
view = View.Invoice;
}
}