Initial commit
This commit is contained in:
415
app/composables/useAdvancedWorkflow.ts
Normal file
415
app/composables/useAdvancedWorkflow.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
import { ref } from 'vue';
|
||||
import { useNuxtApp } from '#app';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Interface cho các tham số từ UI
|
||||
interface TransactionParams {
|
||||
product: any;
|
||||
customer: any;
|
||||
policy: any;
|
||||
phaseInfo: any;
|
||||
priceAfterDiscount: number;
|
||||
discountValue: number;
|
||||
detailedDiscounts: any[];
|
||||
paymentPlans: any[];
|
||||
currentDate: string;
|
||||
reservationDueDate: string;
|
||||
reservationAmount: number;
|
||||
depositReceived: number;
|
||||
people?: number | null;
|
||||
earlyDiscountAmount?: number;
|
||||
gifts?: Array<{ id: number }>; // Thêm trường gifts
|
||||
}
|
||||
|
||||
export function useAdvancedWorkflow() {
|
||||
const { $insertapi, $snackbar, $store } = useNuxtApp();
|
||||
const isLoading = ref(false);
|
||||
const error = ref<any>(null);
|
||||
|
||||
/**
|
||||
* Helper: Tìm kết quả của một step cụ thể trong mảng 'result' của backend trả về
|
||||
*/
|
||||
const findStepResult = (
|
||||
steps: any[],
|
||||
stepCode: string,
|
||||
filter?: string | ((res: any) => boolean)
|
||||
) => {
|
||||
if (!Array.isArray(steps)) return null;
|
||||
|
||||
// 1. Tìm đúng step đang thực thi
|
||||
const step = steps.find(s => (s.step === stepCode || s.step?.startsWith(stepCode)) && s.executed);
|
||||
if (!step || !Array.isArray(step.results) || step.results.length === 0) return null;
|
||||
|
||||
// 2. Nếu filter là một hàm (Callback)
|
||||
if (typeof filter === 'function') {
|
||||
const matched = step.results.find(filter);
|
||||
return matched ? matched.result : null;
|
||||
}
|
||||
|
||||
// 3. Nếu filter là một chuỗi (Action Name)
|
||||
if (typeof filter === 'string') {
|
||||
const matched = step.results.find(r => r.action === filter);
|
||||
return matched ? matched.result : null;
|
||||
}
|
||||
|
||||
// 4. Mặc định: Trả về result của phần tử đầu tiên
|
||||
return step.results[0]?.result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Chuyển đổi danh sách chiết khấu sang format API
|
||||
*/
|
||||
const mapDiscountsList = (detailedDiscounts: any[]) => {
|
||||
if (!detailedDiscounts) return [];
|
||||
return detailedDiscounts
|
||||
.filter(d => d.id)
|
||||
.map(d => ({
|
||||
discount: d.id,
|
||||
value: d.customValue,
|
||||
type: d.customType,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Tạo lịch thanh toán cọc (Installments)
|
||||
*/
|
||||
const createInstallments = (depositReceived: number, reservationAmount: number) => {
|
||||
const installments = [];
|
||||
if (depositReceived > 0) {
|
||||
installments.push({
|
||||
amount: depositReceived,
|
||||
due_days: 0,
|
||||
description: "Thanh toán tiền đặt cọc đã nhận",
|
||||
detail: { note: "Thanh toán tiền đặt cọc" }
|
||||
});
|
||||
}
|
||||
|
||||
const remaining = reservationAmount - depositReceived;
|
||||
if (remaining > 0) {
|
||||
installments.push({
|
||||
amount: remaining,
|
||||
due_days: 2,
|
||||
description: "Phần cọc còn lại",
|
||||
detail: { note: "Thanh toán phần cọc còn lại" }
|
||||
});
|
||||
}
|
||||
return installments;
|
||||
};
|
||||
|
||||
const createWorkflowTransaction = async (params: TransactionParams) => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const userId = $store?.login?.id;
|
||||
if (!userId) {
|
||||
throw new Error('Không tìm thấy thông tin người dùng. Vui lòng đăng nhập lại.');
|
||||
}
|
||||
|
||||
// 1. Cấu hình Body theo đúng workflow
|
||||
const workflowPayload = {
|
||||
workflow_code: "FULL_CONTRACT_CREATION",
|
||||
trigger: "create",
|
||||
data: {
|
||||
phase_code: params.phaseInfo.code,
|
||||
current_date: params.currentDate,
|
||||
due_date: params.reservationDueDate,
|
||||
customer_id: params.customer.id,
|
||||
product_id: params.product.id,
|
||||
policy_id: params.policy.id,
|
||||
user_id: userId,
|
||||
sale_price: params.product.origin_price,
|
||||
origin_price: params.product.origin_price,
|
||||
deposit_amount: params.reservationAmount,
|
||||
discount_amount: 0,
|
||||
amount_received: 0,
|
||||
payment_plan: params.paymentPlans,
|
||||
installments: createInstallments(params.depositReceived, params.reservationAmount),
|
||||
people: params.people || null,
|
||||
early_discount_amount: Math.trunc(params.earlyDiscountAmount || 0),
|
||||
gifts: params.gifts || [] // Thêm danh sách quà tặng
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Gọi API Workflow
|
||||
const response = await $insertapi('workflow', workflowPayload, undefined, false);
|
||||
|
||||
// Kiểm tra thành công
|
||||
if (response === 'error' || !response?.success) {
|
||||
throw new Error(response?.message || 'Thực thi Workflow thất bại.');
|
||||
}
|
||||
|
||||
// 3. Bóc tách dữ liệu từ mảng result trả về
|
||||
const steps = response.result;
|
||||
|
||||
// Lấy Transaction từ step "create_transaction" với action "API_CALL"
|
||||
const txnData = findStepResult(steps, 'create_transaction', 'API_CALL');
|
||||
|
||||
console.log('Transaction Data:', txnData);
|
||||
|
||||
$snackbar('Giao dịch đã được khởi tạo thành công!', 'Thành công', 'Success');
|
||||
|
||||
// 4. Return transaction data
|
||||
return {
|
||||
transaction: txnData ? { id: txnData.id, code: txnData.code } : null,
|
||||
};
|
||||
|
||||
} catch (e: any) {
|
||||
error.value = e;
|
||||
console.error('Workflow Error:', e);
|
||||
$snackbar(e.message || 'Có lỗi xảy ra khi thực thi workflow.', 'Lỗi', 'Error');
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DUYỆT CÔNG NỢ (KẾ TOÁN) - SỬ DỤNG WORKFLOW
|
||||
*/
|
||||
const confirmPaymentSchedule = async (paymentId: number, payload?: object): Promise<boolean> => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const userId = $store?.login?.id;
|
||||
if (!userId) {
|
||||
throw new Error('Không tìm thấy thông tin người dùng. Vui lòng đăng nhập lại.');
|
||||
}
|
||||
|
||||
const workflowPayload = {
|
||||
workflow_id: 9,
|
||||
workflow_code: "APPROVE_PAYMENT",
|
||||
trigger: "create",
|
||||
data: {
|
||||
payment_id: paymentId,
|
||||
user_id: userId,
|
||||
...payload
|
||||
}
|
||||
};
|
||||
|
||||
const response = await $insertapi('workflow', workflowPayload, undefined, false);
|
||||
|
||||
if (response === 'error' || !response?.success) {
|
||||
throw new Error(response?.message || 'Không thể xác nhận công nợ qua workflow.');
|
||||
}
|
||||
|
||||
$snackbar('Công nợ đã được xác nhận thành công!', 'Thành công', 'Success');
|
||||
return true;
|
||||
|
||||
} catch (e: any) {
|
||||
error.value = e;
|
||||
console.error('Workflow Error (confirmPaymentSchedule):', e);
|
||||
$snackbar(e.message || 'Có lỗi xảy ra khi xác nhận công nợ.', 'Lỗi', 'Error');
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const rollbackPayment = async (paymentId: number, entryCode: string, entryAmount: number) => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const userId = $store?.login?.id;
|
||||
if (!userId) {
|
||||
throw new Error('Không tìm thấy thông tin người dùng. Vui lòng đăng nhập lại.');
|
||||
}
|
||||
|
||||
const workflowPayload = {
|
||||
workflow_code: "ROLLBACK_PAYMENT",
|
||||
trigger: "create",
|
||||
data: {
|
||||
payment_id: paymentId,
|
||||
entry_code: entryCode,
|
||||
amount: entryAmount,
|
||||
}
|
||||
};
|
||||
|
||||
const response = await $insertapi('workflow', workflowPayload, undefined, false);
|
||||
|
||||
if (response === 'error' || !response?.success) {
|
||||
throw new Error(response?.message || 'Không thể hủy bút toán qua workflow.');
|
||||
}
|
||||
|
||||
$snackbar('Công nợ đã được xác nhận thành công!', 'Thành công', 'Success');
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
error.value = e;
|
||||
console.error('Workflow Error (confirmPaymentSchedule):', e);
|
||||
$snackbar(e.message || 'Có lỗi xảy ra khi xác nhận công nợ.', 'Lỗi', 'Error');
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DUYỆT CHI TIẾT GIAO DỊCH (QUẢN LÝ) - SỬ DỤNG WORKFLOW
|
||||
*/
|
||||
const approveTransactionDetail = async (detailId: number, statusCode: string): Promise<any> => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const userId = $store?.login?.id;
|
||||
if (!userId) {
|
||||
throw new Error('Không tìm thấy thông tin người dùng. Vui lòng đăng nhập lại.');
|
||||
}
|
||||
|
||||
const workflowPayload = {
|
||||
workflow_code: "APPROVE_TRANSACTION_DETAIL",
|
||||
trigger: "create",
|
||||
data: {
|
||||
detail_id: detailId,
|
||||
status_code: statusCode,
|
||||
user_id: userId,
|
||||
}
|
||||
};
|
||||
|
||||
const response = await $insertapi('workflow', workflowPayload, undefined, false);
|
||||
|
||||
if (response === 'error' || !response?.success) {
|
||||
throw new Error(response?.message || 'Không thể duyệt chi tiết giao dịch qua workflow.');
|
||||
}
|
||||
|
||||
$snackbar('Chi tiết giao dịch đã được duyệt thành công!', 'Thành công', 'Success');
|
||||
return response;
|
||||
|
||||
} catch (e: any) {
|
||||
error.value = e;
|
||||
console.error('Workflow Error (approveTransactionDetail):', e);
|
||||
$snackbar(e.message || 'Có lỗi xảy ra khi duyệt chi tiết giao dịch.', 'Lỗi', 'Error');
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CHUYỂN GIAI ĐOẠN GIAO DỊCH - SỬ DỤNG WORKFLOW
|
||||
*/
|
||||
const advanceTransactionPhase = async (detailId: number): Promise<any> => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const userId = $store?.login?.id;
|
||||
if (!userId) {
|
||||
throw new Error('Không tìm thấy thông tin người dùng. Vui lòng đăng nhập lại.');
|
||||
}
|
||||
|
||||
const workflowPayload = {
|
||||
workflow_code: "ADVANCE_TRANSACTION_PHASE",
|
||||
trigger: "create",
|
||||
data: {
|
||||
detail_id: detailId,
|
||||
user_id: userId,
|
||||
}
|
||||
};
|
||||
|
||||
const response = await $insertapi('workflow', workflowPayload, undefined, false);
|
||||
|
||||
if (response === 'error' || !response?.success) {
|
||||
throw new Error(response?.message || 'Không thể chuyển giai đoạn giao dịch qua workflow.');
|
||||
}
|
||||
|
||||
const steps = response.result;
|
||||
|
||||
// 1. Tìm contract thông qua step bắt đầu bằng 'adv_from_phase_' với action là 'GENERATE_DOCUMENT'
|
||||
const contract = findStepResult(
|
||||
steps,
|
||||
'adv_lookup_transaction',
|
||||
'LOOKUP_DATA'
|
||||
);
|
||||
|
||||
|
||||
console.log('contract', contract)
|
||||
|
||||
$snackbar('Chuyển giai đoạn giao dịch thành công!', contract, 'Success');
|
||||
|
||||
return {
|
||||
...response,
|
||||
contract: contract
|
||||
};
|
||||
|
||||
} catch (e: any) {
|
||||
error.value = e;
|
||||
console.error('Workflow Error (advanceTransactionPhase):', e);
|
||||
$snackbar(e.message || 'Có lỗi xảy ra khi chuyển giai đoạn.', 'Lỗi', 'Error');
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CHUYỂN ĐỔI KHÁCH HÀNG - SỬ DỤNG WORKFLOW
|
||||
*/
|
||||
const updateTransactionCustomer = async (
|
||||
transactionId: number,
|
||||
newCustomerId: number,
|
||||
contractDate?: string
|
||||
): Promise<any> => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const userId = $store?.login?.id;
|
||||
if (!userId) {
|
||||
throw new Error('Không tìm thấy thông tin người dùng. Vui lòng đăng nhập lại.');
|
||||
}
|
||||
|
||||
const workflowPayload = {
|
||||
workflow_code: "UPDATE_TRANS_AND_CO_OP",
|
||||
trigger: "create",
|
||||
data: {
|
||||
transaction: transactionId,
|
||||
new_cus: newCustomerId,
|
||||
user_id: userId,
|
||||
date: contractDate || dayjs().format('YYYY-MM-DD'),
|
||||
}
|
||||
};
|
||||
|
||||
const response = await $insertapi('workflow', workflowPayload, undefined, false);
|
||||
|
||||
if (response === 'error' || !response?.success) {
|
||||
throw new Error(response?.message || 'Không thể chuyển đổi khách hàng qua workflow.');
|
||||
}
|
||||
|
||||
const steps = response.result;
|
||||
|
||||
// Find the contract from any executed step starting with 'GEN_'
|
||||
const contract = findStepResult(
|
||||
steps,
|
||||
'GEN_',
|
||||
'API_CALL'
|
||||
);
|
||||
|
||||
$snackbar('Chuyển đổi khách hàng thành công!', 'Thành công', 'Success');
|
||||
|
||||
return {
|
||||
...response,
|
||||
contract: contract
|
||||
};
|
||||
} catch (e: any) {
|
||||
error.value = e;
|
||||
console.error('Workflow Error (updateTransactionCustomer):', e);
|
||||
$snackbar(e.message || 'Có lỗi xảy ra khi chuyển đổi khách hàng.', 'Lỗi', 'Error');
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
createWorkflowTransaction,
|
||||
confirmPaymentSchedule,
|
||||
approveTransactionDetail,
|
||||
advanceTransactionPhase,
|
||||
updateTransactionCustomer,
|
||||
rollbackPayment
|
||||
};
|
||||
}
|
||||
264
app/composables/usePaymentCalculator.js
Normal file
264
app/composables/usePaymentCalculator.js
Normal file
@@ -0,0 +1,264 @@
|
||||
// composables/usePaymentCalculator.js
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export function usePaymentCalculator() {
|
||||
// Inputs cơ bản
|
||||
const originPrice = ref(0);
|
||||
const discounts = ref([]); // [{ id, name, code, type: 1 (percent) | 2 (fixed), value: number }]
|
||||
const paymentPlan = ref([]); // [{cycle, value, type: 1 (percent) | other (fixed), days, ...}]
|
||||
const contractAllocationPercentage = ref(100);
|
||||
const startDate = ref(new Date());
|
||||
|
||||
// Thanh toán sớm
|
||||
const earlyPaymentCycles = ref(0);
|
||||
const earlyDiscountRate = ref(0.0191780821918);
|
||||
|
||||
// Số tiền đã thanh toán thực tế
|
||||
const paidAmount = ref(0);
|
||||
|
||||
// Tiền gốc
|
||||
const originalPrice = computed(() => originPrice.value);
|
||||
|
||||
// Chi tiết chiết khấu
|
||||
const detailedDiscounts = computed(() => {
|
||||
const details = [];
|
||||
let currentBalance = originPrice.value || 0;
|
||||
|
||||
discounts.value.forEach(discountData => {
|
||||
const d = discountData;
|
||||
let amount = 0;
|
||||
if (d.type === 1) { // percent
|
||||
amount = (currentBalance * Number(d.value)) / 100;
|
||||
} else { // fixed
|
||||
amount = Number(d.value);
|
||||
}
|
||||
if (currentBalance - amount < 0) {
|
||||
amount = currentBalance;
|
||||
}
|
||||
currentBalance -= amount;
|
||||
|
||||
details.push({
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
code: d.code,
|
||||
customValue: d.value,
|
||||
customType: d.type,
|
||||
amount: amount,
|
||||
remaining: currentBalance
|
||||
});
|
||||
});
|
||||
return details;
|
||||
});
|
||||
|
||||
// Tổng tiền giảm giá từ discounts
|
||||
const totalDiscount = computed(() => {
|
||||
return detailedDiscounts.value.reduce((sum, d) => sum + d.amount, 0);
|
||||
});
|
||||
|
||||
// Giá sau chiết khấu
|
||||
const salePrice = computed(() => {
|
||||
const lastDiscount = detailedDiscounts.value[detailedDiscounts.value.length - 1];
|
||||
return lastDiscount ? lastDiscount.remaining : originPrice.value;
|
||||
});
|
||||
|
||||
const allocatedAmount = computed(() => {
|
||||
const amount = salePrice.value * (contractAllocationPercentage.value / 100);
|
||||
return Math.round(amount / 1000) * 1000;
|
||||
});
|
||||
|
||||
const finalTotal = computed(() => allocatedAmount.value);
|
||||
|
||||
// Lịch thanh toán gốc (hỗ trợ cả percent và fixed cho paymentPlan.type)
|
||||
const originalPaymentSchedule = computed(() => {
|
||||
const schedule = [];
|
||||
let remaining = allocatedAmount.value;
|
||||
|
||||
const sortedPlan = [...paymentPlan.value].sort((a, b) => a.cycle - b.cycle);
|
||||
|
||||
let lastToDateForPeriodCalc = new Date(startDate.value); // Used to calculate the duration of each installment period.
|
||||
|
||||
sortedPlan.forEach(plan => {
|
||||
let amount = 0;
|
||||
|
||||
// Xử lý linh hoạt theo type của Payment_Plan
|
||||
if (plan.type === 1) {
|
||||
// percent
|
||||
amount = allocatedAmount.value * (plan.value / 100);
|
||||
} else {
|
||||
// fixed (type 2 hoặc bất kỳ giá trị khác)
|
||||
amount = plan.value || 0;
|
||||
}
|
||||
|
||||
const adjustedAmount = Math.min(amount, remaining);
|
||||
remaining -= adjustedAmount;
|
||||
|
||||
const fromDate = new Date(startDate.value); // Per user request, all installments start from the same date.
|
||||
const toDate = new Date(startDate.value);
|
||||
toDate.setDate(toDate.getDate() + (plan.days || 0));
|
||||
|
||||
// The duration of the installment period is the difference between this installment's due date and the previous one.
|
||||
const daysInPeriod = Math.round((toDate.getTime() - lastToDateForPeriodCalc.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
schedule.push({
|
||||
cycle: plan.cycle,
|
||||
from_date: fromDate,
|
||||
to_date: toDate,
|
||||
amount: adjustedAmount,
|
||||
paid_amount: 0,
|
||||
remain_amount: adjustedAmount,
|
||||
days: daysInPeriod > 0 ? daysInPeriod : 0, // Use calculated period duration; it cannot be negative.
|
||||
status: 1,
|
||||
type: plan.type,
|
||||
value: plan.value,
|
||||
payment_note: `Thanh toán theo kế hoạch đợt số ${plan.cycle}`
|
||||
});
|
||||
|
||||
lastToDateForPeriodCalc = new Date(toDate); // Update the last due date for the next period's duration calculation.
|
||||
});
|
||||
|
||||
// Cộng phần dư (nếu có) vào đợt cuối
|
||||
if (remaining > 0 && schedule.length > 0) {
|
||||
const last = schedule[schedule.length - 1];
|
||||
last.amount += remaining;
|
||||
last.remain_amount += remaining;
|
||||
}
|
||||
|
||||
return schedule;
|
||||
});
|
||||
|
||||
// Chiết khấu thanh toán sớm
|
||||
const earlyDiscounts = computed(() => {
|
||||
if (earlyPaymentCycles.value <= 0) return [];
|
||||
|
||||
const schedule = originalPaymentSchedule.value;
|
||||
const numEarly = Math.min(earlyPaymentCycles.value, schedule.length);
|
||||
const actualPaymentDate = new Date(startDate.value);
|
||||
const actualPaymentDateOnly = new Date(actualPaymentDate.getFullYear(), actualPaymentDate.getMonth(), actualPaymentDate.getDate());
|
||||
|
||||
return schedule.slice(0, numEarly).map(item => {
|
||||
const originalDate = new Date(item.to_date);
|
||||
const originalDateOnly = new Date(originalDate.getFullYear(), originalDate.getMonth(), originalDate.getDate());
|
||||
const earlyDays = Math.round((originalDateOnly.getTime() - actualPaymentDateOnly.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (earlyDays <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return item.amount * earlyDays * (earlyDiscountRate.value / 100);
|
||||
});
|
||||
});
|
||||
|
||||
const totalEarlyDiscount = computed(() => {
|
||||
const rawTotal = earlyDiscounts.value.reduce((sum, disc) => sum + disc, 0);
|
||||
return parseInt(rawTotal);
|
||||
});
|
||||
|
||||
// Chi tiết chiết khấu thanh toán sớm
|
||||
const earlyDiscountDetails = computed(() => {
|
||||
if (earlyPaymentCycles.value <= 0) return [];
|
||||
|
||||
const schedule = originalPaymentSchedule.value;
|
||||
const numEarly = Math.min(earlyPaymentCycles.value, schedule.length);
|
||||
const actualPaymentDate = new Date(startDate.value);
|
||||
const actualPaymentDateOnly = new Date(actualPaymentDate.getFullYear(), actualPaymentDate.getMonth(), actualPaymentDate.getDate());
|
||||
|
||||
return schedule.slice(0, numEarly).map((item, index) => {
|
||||
const originalDate = new Date(item.to_date);
|
||||
const originalDateOnly = new Date(originalDate.getFullYear(), originalDate.getMonth(), originalDate.getDate());
|
||||
const earlyDays = Math.round((originalDateOnly.getTime() - actualPaymentDateOnly.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
return {
|
||||
cycle: item.cycle,
|
||||
original_payment_date: originalDate,
|
||||
actual_payment_date: actualPaymentDate,
|
||||
early_days: earlyDays > 0 ? earlyDays : 0,
|
||||
original_amount: item.amount,
|
||||
discount_rate: earlyDiscountRate.value,
|
||||
discount_amount: earlyDiscounts.value[index] || 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Lịch sau thanh toán sớm (gộp đợt)
|
||||
const scheduleAfterEarly = computed(() => {
|
||||
const base = originalPaymentSchedule.value;
|
||||
const numEarly = Math.min(earlyPaymentCycles.value, base.length);
|
||||
|
||||
if (numEarly <= 0) return base;
|
||||
|
||||
const earlyItems = base.slice(0, numEarly);
|
||||
const totalEarlyAmount = earlyItems.reduce((sum, i) => sum + i.amount, 0);
|
||||
const mergedAmount = totalEarlyAmount - totalEarlyDiscount.value;
|
||||
|
||||
const originalCycles = earlyItems.map(i => i.cycle);
|
||||
|
||||
const mergedItem = {
|
||||
cycle: 1,
|
||||
from_date: earlyItems[0].from_date,
|
||||
to_date: earlyItems[0].from_date, // Due date is the start date for the merged installment
|
||||
amount: Math.max(0, mergedAmount),
|
||||
paid_amount: 0,
|
||||
remain_amount: Math.max(0, mergedAmount),
|
||||
payment_note: `Thanh toán gộp đợt ${originalCycles.join(', ')} (đã trừ số tiền chiết khấu thanh toán sớm là ${totalEarlyDiscount.value})`,
|
||||
days: 0,
|
||||
status: 1,
|
||||
is_merged: true,
|
||||
original_cycles: originalCycles
|
||||
};
|
||||
|
||||
const remainingItems = base.slice(numEarly).map((item, idx) => ({
|
||||
...item,
|
||||
cycle: idx + 2,
|
||||
}));
|
||||
|
||||
return [mergedItem, ...remainingItems];
|
||||
});
|
||||
|
||||
// Lịch thanh toán cuối cùng
|
||||
const finalPaymentSchedule = computed(() => {
|
||||
const schedule = scheduleAfterEarly.value.map(item => ({ ...item }));
|
||||
let remainingPaid = paidAmount.value || 0;
|
||||
|
||||
for (let i = 0; i < schedule.length && remainingPaid > 0; i++) {
|
||||
const item = schedule[i];
|
||||
const canPay = Math.min(remainingPaid, item.remain_amount);
|
||||
|
||||
item.paid_amount += canPay;
|
||||
item.remain_amount -= canPay;
|
||||
remainingPaid -= canPay;
|
||||
|
||||
if (item.remain_amount <= 0) {
|
||||
item.status = 2;
|
||||
item.remain_amount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return schedule;
|
||||
});
|
||||
|
||||
const totalRemaining = computed(() =>
|
||||
finalPaymentSchedule.value.reduce((sum, item) => sum + item.remain_amount, 0)
|
||||
);
|
||||
|
||||
return {
|
||||
originPrice,
|
||||
discounts,
|
||||
paymentPlan,
|
||||
contractAllocationPercentage,
|
||||
startDate,
|
||||
earlyPaymentCycles,
|
||||
earlyDiscountRate,
|
||||
paidAmount,
|
||||
|
||||
originalPrice,
|
||||
detailedDiscounts,
|
||||
totalDiscount,
|
||||
salePrice,
|
||||
allocatedAmount,
|
||||
finalTotal,
|
||||
finalPaymentSchedule,
|
||||
originalPaymentSchedule,
|
||||
earlyDiscountDetails,
|
||||
totalEarlyDiscount,
|
||||
totalRemaining,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user