Files
web/app/components/application/PhaseAdvance.vue
2026-03-02 09:45:33 +07:00

1240 lines
46 KiB
Vue

<template>
<div class="fixed-grid has-12-cols px-3 pb-2">
<div class="grid m-0">
<div class="cell is-col-span-8">
<p class="is-size-6 has-text-weight-bold mb-2">Chọn sản phẩm</p>
<SearchBox v-bind="{
api: 'product',
field: 'label',
searchfield: ['code', 'trade_code', 'type__name', 'zone_type__name'],
column: ['code', 'trade_code', 'type__name', 'land_lot_size', 'status__name'],
first: true,
filter: {
prdbk__transaction__txncurrent__detail__phase__id: filterStatus,
prdbk__transaction__txncurrent__detail__status__id: 3
},
viewaddon: productViewAddon,
}" @option="handleProductSelection" />
</div>
<div class="cell is-col-span-4 is-flex is-align-items-flex-end">
<div style="width: 100%; display: flex; gap: 0.5rem;">
<button v-if="success" class="button is-info has-text-white" style="flex: 1;" @click="openContractModal">
<span>Xem hợp đồng</span>
</button>
<button v-if="!success && $getEditRights()" class="button is-primary has-text-white" style="flex: 1;"
:class="{ 'is-loading': isAdvancing }" @click="confirmAdvancePhase" :disabled="!transactionData">
Chuyển tiếp
</button>
</div>
</div>
</div>
<div v-if="transactionData" class="mt-4">
<div class="columns is-multiline ">
<div class="column is-6">
<div class="box p-0 has-background-white">
<div class="p-4 has-background-primary">
<p class="title is-5 has-text-white">Thông tin Sản phẩm</p>
</div>
<div class="content p-4">
<div class="columns is-multiline is-mobile">
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">Mã sản phẩm</p>
<p class="has-text-weight-bold has-text-primary is-clickable is-size-6 icon-text"
@click="$copyToClipboard(selectedProduct.trade_code)">
<span>{{ selectedProduct.trade_code }}</span>
<span class="icon is-small ml-1">
<SvgIcon name="copy.svg" type="primary" :size="14" />
</span>
</p>
</div>
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">Loại</p>
<p class="has-text-weight-semibold is-size-6">{{ selectedProduct.type__name }}</p>
</div>
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">Diện tích đất</p>
<p class="has-text-weight-semibold is-size-6">{{ $numtoString(selectedProduct.lot_area) }} m²</p>
</div>
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">Phân khu</p>
<p class="has-text-weight-semibold is-size-6">{{ selectedProduct.zone_type__name }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="column is-6">
<div class="box p-0 has-background-white">
<div class="p-4 has-background-primary is-flex is-justify-content-space-between is-align-items-center">
<p class="title is-5 has-text-white m-0">Thông tin Khách hàng</p>
<button
v-if="!isEditingCustomer && currentPhaseType!='dowill' && $getEditRights()"
class="button is-light py-1 px-2"
@click="isEditingCustomer = true"
:disabled="!transactionData || isUpdatingCustomer"
>
<span class="fs-16">Đổi khách hàng</span>
</button>
</div>
<div class="content p-4">
<div v-if="!isEditingCustomer">
<div class="columns is-multiline is-mobile">
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">Mã khách hàng</p>
<p class="has-text-weight-bold has-text-primary is-clickable is-size-6" @click="showCustomerDetails">{{ transactionData.customer__code }}</p>
</div>
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">Số điện thoại</p>
<p class="has-text-weight-bold has-text-primary is-clickable is-size-6" @click="$copyToClipboard(transactionData.customer__phone)">{{ transactionData.customer__phone }}</p>
</div>
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">Họ và tên</p>
<p class="has-text-weight-bold">{{ transactionData.customer__fullname }}</p>
</div>
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">{{ transactionData.customer__legal_type__name }}</p>
<p class="has-text-weight-bold has-text-primary is-clickable is-size-6" @click="$copyToClipboard(transactionData.customer__legal_code)">{{ transactionData.customer__legal_code }}</p>
</div>
</div>
<div v-if="coOwner" class="mt-4 pt-4" style="border-top: 1px solid #dbdbdb;">
<p class="has-text-weight-bold">Người đồng sở hữu</p>
<div class="columns is-multiline is-mobile">
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">Họ và tên</p>
<p class="has-text-weight-bold">{{ coOwner.people__fullname }}</p>
</div>
<div class="column is-6">
<p class="has-text-primary is-size-7 mb-1">Số điện thoại</p>
<p class="has-text-weight-bold has-text-primary">{{ coOwner.people__phone }}</p>
</div>
</div>
</div>
</div>
<div v-else>
<p class="has-text-weight-bold mb-2">Chọn khách hàng mới để thay thế</p>
<SearchBox v-bind="{
api: 'customer',
field: 'label',
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
column: ['code', 'fullname', 'phone', 'legal_code'],
first: true,
clearable: true,
viewaddon: customerViewAddon,
addon: customerViewAdd
}" @option="handleNewCustomerSelected" />
<hr>
<div v-if="transactionData">
<p class="has-text-weight-bold mb-2">Quản lý đồng sở hữu</p>
<div v-if="(pendingCoOwner?.action === 'add') || (coOwner && pendingCoOwner?.action !== 'remove')" class="mt-2 pt-2">
<div class="level is-mobile mb-2">
<div class="level-left">
<p class="has-text-weight-semibold">
{{ pendingCoOwner?.action === 'add' ? pendingCoOwner.data.people__fullname : coOwner.people__fullname }}
</p>
</div>
<div class="level-right">
<button class="delete is-small" @click="confirmRemoveCoOwner" title="Xóa người đồng sở hữu"></button>
</div>
</div>
</div>
<div v-if="(!coOwner && !pendingCoOwner) || (pendingCoOwner?.action === 'remove')" class="mt-2">
<div v-if="newCustomer ? newCustomer.type === 1 : transactionData.customer__type === 1">
<SearchBox
:key="coOwnerSearchBoxKey"
:vdata="relatedPeople"
field="people__fullname"
:column="['people__code', 'people__fullname']"
first="true"
clearable="true"
:placeholder="'Thêm đồng sở hữu...'"
@option="handleCoOwnerPersonSelected"
/>
</div>
<p v-else class="is-size-7 has-text-grey">Chỉ khách hàng cá nhân mới có thể thêm đồng sở hữu.</p>
</div>
</div>
<div class="buttons is-right mt-5">
<button class="button is-light" @click="cancelCustomerEdit">Hủy</button>
<button class="button is-primary" @click="executeCustomerUpdate" :disabled="!newCustomer || isUpdatingCustomer" :class="{'is-loading': isUpdatingCustomer}">Xác nhận</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="has-background-white p-5">
<div class="level is-mobile mb-4">
<div class="level-left">
<p class="title is-5 has-text-primary">
<span class="icon-text"><span>Thông tin Giao dịch & Giá</span></span>
</p>
</div>
<div class="level-right">
<button v-if="canEditPolicy && !isEditingPolicy && !success && $getEditRights()" class="button is-info"
@click="enablePolicyEdit" :disabled="isUpdatingPolicy">
<span class="icon"><SvgIcon name="pen1.svg" type="white" :size="16" /></span>
<span>Sửa chính sách</span>
</button>
<div v-if="isEditingPolicy" class="buttons">
<button class="button is-light" @click="cancelPolicyEdit">Hủy</button>
</div>
</div>
</div>
<div v-if="isEditingPolicy" class="has-background-white p-5 mb-5">
<div v-if="['purchase', 'dowill', 'deposit'].includes(currentPhaseType)" class="mb-5">
<p class="is-size-6 has-text-weight-bold mb-2">Chọn chính sách bán hàng</p>
<div class="tabs is-toggle">
<ul>
<li v-for="policy in filteredPolicies" :key="policy.id"
:class="{ 'is-primary': selectedNewPolicy?.id === policy.id }">
<a @click="handlePolicySelect(policy)">
<span>{{ policy.name }}</span>
</a>
</li>
</ul>
</div>
</div>
<div v-if="currentPhaseType !== 'deposit'" class="mb-5">
<p class="is-size-6 has-text-weight-bold mb-2">Chọn chiết khấu</p>
<div class="columns is-multiline">
<div class="column is-12" v-for="(row, index) in discountRows" :key="row.key"
draggable="true"
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@dragend="dragEnd"
:data-index="index"
style="cursor: move; border-bottom: 1px solid #204853;">
<div class="columns is-mobile is-vcentered m-0 is-variable is-1">
<div class="column is-narrow" style="display: flex; align-items: center; justify-content: center;">
<SvgIcon v-bind="{ name: 'dot.svg', type: 'primary', size: 16 }"></SvgIcon>
</div>
<div class="column" :class="row.selectedData?.type === 1 ? 'is-4' : 'is-6'">
<SearchBox v-bind="{
api: 'discounttype',
field: 'label',
searchfield: ['code', 'name', 'value'],
column: ['code', 'name', 'value'],
first: true,
clearable: true,
placeholder: 'Chọn loại chiết khấu...'
}" @option="(val) => handleRowSelect(index, val)" />
</div>
<div v-if="row.selectedData?.type === 1" class="column is-3">
<div class="select is-fullwidth">
<select v-model="row.basePriceType" @change="recalculateDiscount(index)">
<option value="contract">Giá trị hợp đồng</option>
<option value="with_vat">Giá đã VAT</option>
<option value="without_vat">Giá chưa VAT</option>
</select>
</div>
</div>
<div class="column" :class="row.selectedData?.type === 1 ? 'is-4' : 'is-5'">
<div class="control has-icons-right">
<input
class="input has-text-centered has-text-weight-bold"
type="number"
v-model.number="row.customValue"
@input="validateRowValue(index)"
placeholder="0"
:disabled="!row.selectedData">
<span v-if="row.selectedData" class="icon is-right has-text-grey is-size-7">
{{ row.selectedData.type === 1 ? '%' : 'đ' }}
</span>
</div>
</div>
<button class="column is-auto button is-warning" @click="removeDiscountRow(index)">
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 16 }"></SvgIcon>
</button>
</div>
</div>
<div class="column is-12">
<button class="button is-text is-fullwidth" @click="addNewDiscountRow">
<SvgIcon v-bind="{ name: 'add4.svg', type: 'primary', size: 18 }" />
<span class="ml-2">Thêm chiết khấu</span>
</button>
</div>
</div>
</div>
<div class="px-3 mb-3">
<div class="columns">
<div class="column is-3">
<div class="field">
<label class="label">Ngày ký hợp đồng mới</label>
<Datepicker :record="dateRecord" attr="contractDate" @date="updateContractDate" position="is-top-right" />
</div>
</div>
</div>
</div>
<div v-if="selectedNewPolicy && selectedNewPolicy.contract_allocation_percentage == 100 && selectedNewPolicy.method === 1" class="px-3 mb-5 has-background-white p-4">
<div class="level is-mobile mb-3">
<div class="level-left">
<label class="checkbox">
<input type="checkbox" v-model="enableEarlyPayment" @change="handleEarlyPaymentToggle">
<span class="is-size-6 has-text-weight-semibold has-text-primary ml-2">
Thanh toán sớm (2 - {{ maxEarlyCycles }}) kỳ
</span>
</label>
</div>
</div>
<transition name="fade">
<div v-if="enableEarlyPayment && maxEarlyCycles >= 2" class="field">
<label class="label">Số đợt thanh toán sớm</label>
<div class="control" style="max-width: 200px;">
<input class="input" type="number" v-model.number="earlyPaymentCycles" :min="2" :max="maxEarlyCycles"
@input="validateEarlyCycles">
</div>
<p class="help">Gộp {{ earlyPaymentCycles }} đợt đầu thành 1 với chiết khấu 0.019%/ngày</p>
</div>
</transition>
</div>
<PaymentSchedulePresentation
ref="schedulePresentationRef"
v-if="selectedNewPolicy"
:productData="productDataForSchedule"
:selectedPolicy="selectedNewPolicy"
:selectedCustomer="selectedCustomer"
:calculatorData="calculationResult"
:baseDate="transactionData.create_time"
:isLoading="isUpdatingPolicy"
@print="printContent"
/>
</div>
<div class="columns is-multiline">
<div class="column is-3">
<p class="has-text-primary is-size-7 mb-1">Mã giao dịch</p>
<p class="has-text-weight-bold has-text-primary is-clickable is-size-6 icon-text"
@click="$copyToClipboard(transactionData.code)">
<span>{{ transactionData.code }}</span>
<span class="icon is-small ml-1"><SvgIcon name="copy.svg" type="primary" :size="14" /></span>
</p>
</div>
<div class="column is-3">
<p class="has-text-primary is-size-7 mb-1">Giai đoạn hiện tại</p>
<p><span class="tag is-primary is-medium is-rounded">{{ transactionData.phase__name }}</span></p>
</div>
<div class="column is-3">
<p class="has-text-primary is-size-7 mb-1">Chính sách bán hàng</p>
<p class="has-text-weight-semibold is-size-6">{{ currentPolicyName }}</p>
</div>
<div class="column is-3">
<p class="has-text-primary is-size-7 mb-1">Ngày ký văn bản</p>
<p class="has-text-weight-semibold is-size-6">{{ formatDate(transactionData.date) }}</p>
</div>
</div>
<hr class="has-background-primary">
<div class="columns is-multiline">
<div class="column is-3">
<p class="has-text-primary mb-1">Giá gốc</p>
<p class="title is-5 has-text-primary-dark">{{ $numtoString(transactionData.origin_price) }} đ</p>
</div>
<div class="column is-3">
<p class="has-text-primary mb-1">Chiết khấu</p>
<p class="title is-5 has-text-danger" v-if="calculationResult? calculationResult.totalDiscount>0 : false">
-{{ $numtoString(isEditingPolicy ? calculationResult.totalDiscount : transactionData.discount_amount) }} đ
</p>
</div>
<div class="column is-3">
<p class="has-text-primary mb-1">Giá hợp đồng</p>
<p class="title is-3 has-text-primary has-text-weight-bold">{{ $numtoString(isEditingPolicy ? calculationResult.salePrice : transactionData.sale_price) }} đ</p>
</div>
<div class="column is-3">
<p class="has-text-primary mb-1">Tổng đã nhận</p>
<p class="title is-5 has-text-success">{{ $numtoString(transactionData.amount_received) }} đ</p>
</div>
</div>
</div>
</div>
<div v-if="!selectedProduct && !isLoading" class="has-text-centered py-6">
<p class="mb-4"><span class="icon has-text-primary"><SvgIcon name="info.svg" type="primary" :size="64" /></span></p>
<p class="title is-4 has-text-primary">Vui lòng chọn sản phẩm đang giữ chỗ</p>
<p class="subtitle is-6 has-text-primary">Để xem thông tin chi tiết và chuyển giai đoạn</p>
</div>
<div v-if="isLoading" class="has-text-centered py-6">
<button class="button is-primary is-loading is-large"></button>
<p class="mt-4 has-text-primary has-text-weight-semibold">Đang tải thông tin...</p>
</div>
<div v-if="error" class="notification is-danger is-light mt-4">
<button class="delete" @click="error = null"></button>
<strong>Lỗi:</strong> {{ error }}
</div>
<Modal @close="showmodal = undefined" v-bind="showmodal" @confirm="showmodal.onConfirm" v-if="showmodal" />
<Modal v-if="showContractModal" @close="showContractModal = false" @dataevent="handleContractUpdated" v-bind="contractModalConfig" />
<Modal v-if="customerModal" @close="customerModal = null" v-bind="customerModal" />
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue';
import { useStore } from "@/stores/index";
import { useAdvancedWorkflow } from '@/composables/useAdvancedWorkflow';
import { usePaymentCalculator } from '~/composables/usePaymentCalculator';
import SearchBox from '~/components/SearchBox.vue';
import Modal from '~/components/Modal.vue';
import SvgIcon from '~/components/SvgIcon.vue';
import PaymentSchedulePresentation from './PaymentSchedulePresentation.vue';
import Datepicker from '~/components/datepicker/Datepicker.vue';
import dayjs from 'dayjs';
const store = useStore();
const { $getdata, $snackbar, $copyToClipboard, $patchapi, $insertapi, $deleteapi, $exportpdf } = useNuxtApp();
const { advanceTransactionPhase } = useAdvancedWorkflow();
const calculator = usePaymentCalculator();
// State
const selectedProduct = ref(null);
const transactionData = ref(null);
const isLoading = ref(false);
const isAdvancing = ref(false);
const success = ref(null);
const error = ref(null);
const productViewAddon = {
component: "product/ProductView",
width: "70%",
height: "500px",
title: "Thông tin sản phẩm",
};
const showmodal = ref(null);
const showContractModal = ref(false);
const createdContractId = ref(null);
const customerModal = ref(null);
const isEditingCustomer = ref(false);
const isUpdatingCustomer = ref(false);
const newCustomer = ref(null);
const policies = ref([]);
const allPaymentPlans = ref([]);
const selectedNewPolicy = ref(null);
const selectedNewPolicyId = ref(null);
const isEditingPolicy = ref(false);
const isUpdatingPolicy = ref(false);
const coOwner = ref(null);
const relatedPeople = ref([]);
const coOwnerSearchBoxKey = ref(0);
// --- NEW STATE: Stores pending changes { action: 'add'|'remove', data: personObj }
const pendingCoOwner = ref(null);
const discountRows = ref([{
key: Date.now(),
selectedData: null,
customValue: 0,
basePriceType: 'contract',
calculatedAmount: 0
}]);
const draggedIndex = ref(null);
const schedulePresentationRef = ref(null);
const contractDate = ref(dayjs().format('YYYY-MM-DD'));
const dateRecord = ref({ contractDate: contractDate.value });
const enableEarlyPayment = ref(false);
const earlyPaymentCycles = ref(2);
// Addons
const customerViewAddon = {
component: "customer/CustomerView",
width: "70%",
height: "500px",
title: "Thông tin khách hàng",
};
const customerViewAdd = {
component: "customer/CustomerInfo2",
width: "60%",
height: "600px",
title: "Tạo / Chỉnh sửa khách hàng",
};
// Computed
const activeTabInfo = computed(() => {
const tabinfo = store.tabinfo;
let tab = null, subtab = null;
if (tabinfo) {
tab = tabinfo.tab?.code || tabinfo.vbind?.tab;
subtab = tabinfo.subtab?.code || tabinfo.vbind?.subtab;
}
return { tab, subtab };
});
const maxEarlyCycles = computed(() => {
return calculator.paymentPlan.value?.length || 0;
});
const currentPhaseType = computed(() => {
const { tab, subtab } = activeTabInfo.value;
if (tab === 'calculation' && subtab === 'deposit') return 'deposit';
if (tab === 'calculation' && subtab === 'dowill') return 'dowill';
if (tab === 'calculation' && subtab === 'purchase') return 'purchase';
return null;
});
const filterStatus = computed(() => {
const { subtab } = activeTabInfo.value;
if (subtab === 'deposit') return 1;
if (subtab === 'dowill') return 4;
if (subtab === 'purchase') return 2;
});
const canEditPolicy = computed(() => ['deposit', 'dowill', 'purchase'].includes(currentPhaseType.value));
const contractModalConfig = computed(() => ({
component: "application/Contract",
title: "Hợp đồng",
width: "90%",
height: "90vh",
vbind: { row: { id: createdContractId.value } },
event: "contractUpdated",
eventname: "dataevent",
}));
const currentPolicyName = computed(() => {
if (isEditingPolicy.value && selectedNewPolicy.value) {
return selectedNewPolicy.value.name;
}
return transactionData.value?.policy__name || '-';
});
const productDataForSchedule = computed(() => {
if (!selectedProduct.value) return null;
return {
...selectedProduct.value,
origin_price: transactionData.value?.origin_price || selectedProduct.value.origin_price
};
});
const selectedCustomer = computed(() => {
if (!transactionData.value) return null;
return {
code: transactionData.value.customer__code,
fullname: transactionData.value.customer__fullname
};
});
const calculationResult = computed(() => ({
originPrice: calculator.originPrice?.value || 0,
totalDiscount: calculator.totalDiscount?.value || 0,
salePrice: calculator.finalTotal.value,
allocatedPrice: calculator.allocatedAmount?.value ?? (calculator.salePrice?.value || 0),
originalPaymentSchedule: calculator.originalPaymentSchedule?.value || [],
finalPaymentSchedule: calculator.finalPaymentSchedule?.value || [],
earlyDiscountDetails: calculator.earlyDiscountDetails?.value || [],
totalRemaining: calculator.totalRemaining?.value || 0,
detailedDiscounts: calculator.detailedDiscounts.value,
baseDate: calculator.startDate?.value
}));
const filteredPolicies = computed(() => {
if (!policies.value?.length) return [];
if (currentPhaseType.value === 'deposit') {
return policies.value.filter(p => p.id === 15);
}
const currentPolicyId = transactionData.value?.policy;
switch (currentPhaseType.value) {
case 'dowill':
return policies.value.filter(p => p.id !== (currentPolicyId || 15));
case 'purchase':
return policies.value.filter(p => p.id !== 15);
default:
return [];
}
});
// Watchers
watch(discountRows, () => {
calculator.discounts.value = discountRows.value
.filter(row => row.selectedData)
.map(row => {
if (row.selectedData.type === 1) {
let basePrice = 0;
switch (row.basePriceType) {
case 'with_vat':
basePrice = transactionData.value?.origin_price || selectedProduct.value?.origin_price || 0;
break;
case 'without_vat':
basePrice = selectedProduct.value?.price_excluding_vat ||
(transactionData.value?.origin_price || selectedProduct.value?.origin_price || 0) / 1.1;
break;
case 'contract':
default:
return {
id: row.selectedData.id,
name: row.selectedData.name,
code: row.selectedData.code,
type: row.selectedData.type,
value: row.customValue || 0
};
}
const calculatedAmount = (basePrice * (row.customValue || 0)) / 100;
row.calculatedAmount = calculatedAmount;
return {
id: row.selectedData.id,
name: `${row.selectedData.name} (${row.basePriceType === 'with_vat' ? 'Giá đã VAT' : 'Giá chưa VAT'})`,
code: row.selectedData.code,
type: 2,
value: Math.round(calculatedAmount)
};
} else {
row.calculatedAmount = 0;
return {
id: row.selectedData.id,
name: row.selectedData.name,
code: row.selectedData.code,
type: row.selectedData.type,
value: row.customValue || 0
};
}
});
}, { deep: true });
watch(selectedNewPolicy, (policy) => {
if (!policy) return;
const plans = allPaymentPlans.value.filter(p => p.policy === policy.id);
if (plans.length > 0) {
plans.sort((a, b) => a.cycle - b.cycle);
calculator.paymentPlan.value = plans.map(p => ({
cycle: p.cycle,
value: p.value,
type: p.type,
days: p.days || 0,
payment_note: p.payment_note,
due_note: p.due_note || ''
}));
}
calculator.contractAllocationPercentage.value = policy.contract_allocation_percentage || 100;
});
watch(contractDate, (newVal) => {
if (newVal) {
calculator.startDate.value = new Date(newVal);
}
});
watch(enableEarlyPayment, (newVal) => {
if (!newVal) {
calculator.earlyPaymentCycles.value = 0;
} else {
calculator.earlyPaymentCycles.value = earlyPaymentCycles.value;
}
});
watch(earlyPaymentCycles, (newVal) => {
if (enableEarlyPayment.value && newVal > 0) {
calculator.earlyPaymentCycles.value = newVal;
}
});
// Methods
async function handleProductSelection(product) {
selectedProduct.value = null;
transactionData.value = null;
error.value = null;
success.value = null;
createdContractId.value = null;
showmodal.value = null;
isEditingPolicy.value = false;
selectedNewPolicy.value = null;
discountRows.value = [{
key: Date.now(),
selectedData: null,
customValue: 0,
basePriceType: 'contract',
calculatedAmount: 0
}];
enableEarlyPayment.value = false;
earlyPaymentCycles.value = 2;
contractDate.value = dayjs().format('YYYY-MM-DD');
dateRecord.value.contractDate = contractDate.value;
isEditingCustomer.value = false;
coOwner.value = null;
relatedPeople.value = [];
pendingCoOwner.value = null; // Reset pending
coOwnerSearchBoxKey.value++;
if (!product?.id) return;
isLoading.value = true;
try {
const fullProduct = await $getdata('product', { id: product.id }, undefined, true);
if (!fullProduct) throw new Error('Không tìm thấy thông tin chi tiết sản phẩm.');
selectedProduct.value = fullProduct;
const transactionId = fullProduct.prdbk__transaction;
if (!transactionId) throw new Error('Sản phẩm này không có giao dịch liên kết.');
const transaction = await $getdata('transaction', { id: transactionId }, undefined, true);
if (!transaction) throw new Error('Không tìm thấy thông tin giao dịch.');
transactionData.value = transaction;
const coOwnerData = await $getdata('co_op', { transaction: transaction.id }, undefined, true);
if (coOwnerData) {
coOwner.value = coOwnerData;
}
if (transaction.customer__type === 1) { // Only for individuals
const people = await $getdata('customerpeople', { customer: transaction.customer });
relatedPeople.value = Array.isArray(people) ? people : (people ? [people] : []);
}
await loadPoliciesAndPlans();
const initialDate = new Date(transaction.create_time || Date.now());
calculator.originPrice.value = transaction.origin_price || 0;
calculator.discounts.value = [];
calculator.paymentPlan.value = [];
calculator.contractAllocationPercentage.value = 100;
calculator.startDate.value = initialDate;
calculator.paidAmount.value = transaction.amount_received || 0;
calculator.earlyPaymentCycles.value = 0;
contractDate.value = dayjs(initialDate).format('YYYY-MM-DD');
dateRecord.value.contractDate = contractDate.value;
} catch (e) {
error.value = e.message || 'Lỗi khi tải thông tin giao dịch.';
$snackbar(error.value, { type: 'is-danger' });
} finally {
isLoading.value = false;
}
}
async function loadPoliciesAndPlans() {
try {
policies.value = await $getdata("salepolicy", { enable: "True" }, undefined, false);
const plans = await $getdata("paymentplan", { policy__enable: "True" }, undefined, false);
if (plans) {
plans.sort((a, b) => a.cycle - b.cycle);
allPaymentPlans.value = plans;
}
} catch (err) {
console.error('Error loading policies and plans:', err);
}
}
function handlePolicySelect(policy) {
selectedNewPolicy.value = policy;
selectedNewPolicyId.value = policy ? policy.id : null;
const plans = allPaymentPlans.value.filter(p => p.policy === policy.id);
if (plans.length > 0) {
plans.sort((a, b) => a.cycle - b.cycle);
calculator.paymentPlan.value = plans.map(p => ({
cycle: p.cycle,
value: p.value,
type: p.type,
days: p.days || 0,
payment_note: p.payment_note,
due_note: p.due_note || ''
}));
}
calculator.contractAllocationPercentage.value = policy.contract_allocation_percentage || 100;
enableEarlyPayment.value = false;
earlyPaymentCycles.value = 2;
}
function enablePolicyEdit() {
isEditingPolicy.value = true;
discountRows.value = [{
key: Date.now(),
selectedData: null,
customValue: 0,
basePriceType: 'contract',
calculatedAmount: 0
}];
selectedNewPolicy.value = null;
selectedNewPolicyId.value = null;
calculator.originPrice.value = transactionData.value?.origin_price || 0;
const initialDate = new Date(transactionData.value.create_time || Date.now());
calculator.startDate.value = initialDate;
contractDate.value = dayjs(initialDate).format('YYYY-MM-DD');
dateRecord.value.contractDate = contractDate.value;
if (currentPhaseType.value === 'deposit') {
const policy15 = filteredPolicies.value.find(p => p.id === 15);
if (policy15) {
handlePolicySelect(policy15);
}
} else {
const currentPolicy = policies.value.find(p => p.id === transactionData.value.policy);
if (currentPolicy) {
handlePolicySelect(currentPolicy);
}
}
}
function cancelPolicyEdit() {
isEditingPolicy.value = false;
selectedNewPolicy.value = null;
discountRows.value = [{
key: Date.now(),
selectedData: null,
customValue: 0,
basePriceType: 'contract',
calculatedAmount: 0
}];
enableEarlyPayment.value = false;
earlyPaymentCycles.value = 2;
}
async function executePolicyUpdate() {
showmodal.value = null;
isUpdatingPolicy.value = true;
error.value = null;
try {
const calcResult = calculationResult.value;
const isEarlyPaymentActive = isEditingPolicy.value && enableEarlyPayment.value;
let plansForApi = calcResult.finalPaymentSchedule;
const paymentPlanData = plansForApi.map(plan => ({
amount: plan.amount || 0,
due_days: plan.days || plan.cycle_days || 0,
cycle: String(plan.cycle || 1),
note: plan.payment_note,
is_early_merged: plan.is_merged || false,
merged_cycles: plan.is_merged ? (plan.original_cycles || []) : [],
raw_amount: plan.amount || 0,
status: plan.status || 1,
paid_amount: plan.paid_amount || 0,
remain_amount: plan.remain_amount || 0,
from_date: plan.from_date instanceof Date ? plan.from_date.toISOString().split('T')[0] : (plan.from_date || ''),
to_date: plan.to_date instanceof Date ? plan.to_date.toISOString().split('T')[0] : (plan.to_date || ''),
due_note: plan.due_note || ''
}));
const totalEarlyDiscount = isEarlyPaymentActive
? calculator.totalEarlyDiscount.value
: 0;
const patchData = {
id: transactionData.value.id,
policy: selectedNewPolicy.value.id,
payment_plan: paymentPlanData,
date: contractDate.value,
discounts: calcResult.detailedDiscounts.map(d => ({
discount: d.id,
type: d.customType,
value: d.customValue
})),
sale_price: calcResult.salePrice,
discount_amount: calcResult.totalDiscount,
early_discount_amount: parseInt(totalEarlyDiscount),
contract_sign_date: contractDate.value
};
const result = await $patchapi('transaction', patchData);
if (!result) throw new Error('Cập nhật chính sách thất bại.');
transactionData.value = await $getdata('transaction', { id: transactionData.value.id }, undefined, true);
$snackbar('Đã cập nhật chính sách thành công.', { type: 'is-success' });
isEditingPolicy.value = false;
selectedNewPolicy.value = null;
} catch (e) {
error.value = e.message || 'Lỗi khi cập nhật chính sách.';
$snackbar(error.value, { type: 'is-danger' });
} finally {
isUpdatingPolicy.value = false;
}
}
function confirmAdvancePhase() {
const tx = transactionData.value;
if (tx?.txncurrent__detail__amount_remaining > 0) {
return $snackbar('Khách hàng chưa thanh toán số tiền yêu cầu', { type: 'is-warning' });
}
if (currentPhaseType.value === 'deposit' && tx.policy !== 15 && !isEditingPolicy.value) {
return $snackbar('Phải đổi sang chính sách THNV trước khi chuyển tiếp giai đoạn', { type: 'is-warning' });
}
if (currentPhaseType.value === 'dowill' && tx.policy === 15 && !isEditingPolicy.value) {
return $snackbar('Phải chọn chính sách thanh toán cho giai đoạn tiếp theo', { type: 'is-warning' });
}
if (isEditingPolicy.value && !selectedNewPolicy.value) {
return $snackbar('Vui lòng chọn chính sách thanh toán mới', { type: 'is-warning' });
}
showmodal.value = {
component: 'dialog/Confirm',
title: 'Xác nhận chuyển giai đoạn',
width: '600px',
height: '150px',
vbind: { content: `Xác nhận chuyển giai đoạn cho giao dịch: ${tx?.code}?` },
onConfirm: handleConfirmAdvance
};
}
async function handleConfirmAdvance() {
if (isEditingPolicy.value) {
await executePolicyUpdate();
}
await executeAdvancePhase();
}
async function executeAdvancePhase() {
isAdvancing.value = true;
error.value = null;
try {
if (!transactionData.value?.id) throw new Error('Thông tin giao dịch không đầy đủ.');
const apiDetail = await $getdata('reservation', {
transaction: transactionData.value.id,
phase: transactionData.value.phase,
status: 3
}, undefined, true);
if (!apiDetail?.id) throw new Error('Không tìm thấy chi tiết giao dịch hợp lệ để chuyển giai đoạn.');
const result = await advanceTransactionPhase(apiDetail.id);
if (!result) throw new Error('Thực hiện chuyển giai đoạn thất bại.');
success.value = true;
$snackbar('Chuyển giai đoạn thành công!', { type: 'is-success' });
createdContractId.value = result.contract;
showContractModal.value = true;
} catch (e) {
error.value = e.message;
$snackbar(error.value, { type: 'is-danger' });
} finally {
isAdvancing.value = false;
}
}
function openContractModal() {
showContractModal.value = true;
}
function handleContractUpdated(eventData) {
if (eventData?.data) {
$snackbar("Hợp đồng đã được cập nhật");
}
}
function formatDate(dateString) {
if (!dateString) return '-';
return dayjs(dateString).format('DD/MM/YYYY');
}
function showCustomerDetails() {
if (!transactionData.value?.customer) return;
customerModal.value = {
...customerViewAddon,
vbind: {row: { id: transactionData.value.customer }}
};
}
// --- MODIFIED: Load related people immediately for NEW customer ---
async function handleNewCustomerSelected(customer) {
newCustomer.value = customer;
pendingCoOwner.value = null;
relatedPeople.value = [];
if (customer && customer.type === 1) {
try {
const people = await $getdata('customerpeople', { customer: customer.id });
relatedPeople.value = Array.isArray(people) ? people : (people ? [people] : []);
} catch (e) {
console.error("Error loading related people", e);
$snackbar('Không thể tải danh sách người thân.', { type: 'is-danger' });
}
}
coOwnerSearchBoxKey.value++;
}
// --- MODIFIED: Store local pending state only ---
function handleCoOwnerPersonSelected(person) {
if (!person || !person.people) return;
pendingCoOwner.value = {
action: 'add',
data: {
people__fullname: person.people__fullname,
people__phone: person.people__phone,
people: person.people,
}
};
}
// --- MODIFIED: Mark for local removal ---
function confirmRemoveCoOwner() {
if (pendingCoOwner.value?.action === 'add') {
pendingCoOwner.value = null;
return;
}
pendingCoOwner.value = { action: 'remove' };
}
// --- MODIFIED: Batch Update (Clean old -> Update Tx -> Add new) ---
async function executeCustomerUpdate() {
if (!transactionData.value || !newCustomer.value) return;
isUpdatingCustomer.value = true;
error.value = null;
try {
// 1. Remove old co-owner from DB if exists
if (coOwner.value) {
await $deleteapi('co_op', coOwner.value.id);
coOwner.value = null;
}
// 2. Update Transaction with new customer
const result = await $patchapi('transaction', {
id: transactionData.value.id,
customer: newCustomer.value.id
});
if (result && result !== 'error') {
// 3. Add new co-owner if pending
if (pendingCoOwner.value?.action === 'add') {
await $insertapi('co_op', {
transaction: transactionData.value.id,
people: pendingCoOwner.value.data.people
});
}
$snackbar('Đã cập nhật thông tin thành công.', { type: 'is-success' });
isEditingCustomer.value = false;
newCustomer.value = null;
pendingCoOwner.value = null;
// Refresh all data
if (selectedProduct.value) {
await handleProductSelection(selectedProduct.value);
}
} else {
throw new Error('Cập nhật khách hàng thất bại.');
}
} catch (e) {
error.value = e.message || 'Lỗi khi cập nhật khách hàng.';
$snackbar(error.value, { type: 'is-danger' });
} finally {
isUpdatingCustomer.value = false;
}
}
// --- ADDED: Reset State on Cancel ---
function cancelCustomerEdit() {
isEditingCustomer.value = false;
newCustomer.value = null;
pendingCoOwner.value = null;
}
// ... (Rest of existing methods: handleRowSelect, recalculateDiscount, etc. keep same) ...
function handleRowSelect(index, data) {
const row = discountRows.value[index];
if (!data) {
row.selectedData = null;
row.customValue = 0;
row.basePriceType = 'contract';
row.calculatedAmount = 0;
} else {
row.selectedData = data;
row.customValue = data.value;
if (data.type === 1) {
row.basePriceType = 'contract';
recalculateDiscount(index);
} else {
row.calculatedAmount = 0;
}
}
}
function recalculateDiscount(index) {
const row = discountRows.value[index];
if (!row.selectedData || row.selectedData.type !== 1) return;
let basePrice = 0;
switch (row.basePriceType) {
case 'with_vat':
basePrice = transactionData.value?.origin_price || selectedProduct.value?.origin_price || 0;
break;
case 'without_vat':
basePrice = selectedProduct.value?.price_excluding_vat ||
(transactionData.value?.origin_price || selectedProduct.value?.origin_price || 0) / 1.1;
break;
case 'contract':
default:
row.calculatedAmount = 0;
return;
}
row.calculatedAmount = (basePrice * (row.customValue || 0)) / 100;
}
function validateRowValue(index) {
const row = discountRows.value[index];
if (!row.selectedData) return;
if (row.selectedData.type === 1) {
if (row.customValue > 100) row.customValue = 100;
if (row.customValue < 0) row.customValue = 0;
recalculateDiscount(index);
} else {
if (row.customValue < 0) row.customValue = 0;
}
}
function addNewDiscountRow() {
discountRows.value.push({
key: Date.now(),
selectedData: null,
customValue: 0,
basePriceType: 'contract',
calculatedAmount: 0
});
}
function removeDiscountRow(index) {
discountRows.value.splice(index, 1);
if (!discountRows.value.length) addNewDiscountRow();
}
function dragStart(e) {
draggedIndex.value = parseInt(e.currentTarget.getAttribute('data-index'));
e.currentTarget.style.opacity = '0.5';
}
function dragOver(e) {
e.preventDefault();
const target = e.currentTarget;
if (target.getAttribute('data-index') !== null) {
target.style.borderTop = '2px solid #204853';
}
}
function drop(e) {
e.preventDefault();
const dropIdx = parseInt(e.currentTarget.getAttribute('data-index'));
if (draggedIndex.value !== null && draggedIndex.value !== dropIdx) {
const draggedRow = discountRows.value[draggedIndex.value];
discountRows.value.splice(draggedIndex.value, 1);
discountRows.value.splice(dropIdx, 0, draggedRow);
}
document.querySelectorAll('[data-index]').forEach(row => {
row.style.borderTop = '';
row.style.opacity = '1';
});
}
function dragEnd(e) {
document.querySelectorAll('[data-index]').forEach(row => {
row.style.borderTop = '';
row.style.opacity = '1';
});
draggedIndex.value = null;
}
function updateContractDate(newDate) {
contractDate.value = newDate;
dateRecord.value.contractDate = newDate;
}
function handleEarlyPaymentToggle() {
if (enableEarlyPayment.value) {
earlyPaymentCycles.value = Math.min(2, maxEarlyCycles.value);
calculator.earlyPaymentCycles.value = earlyPaymentCycles.value;
} else {
calculator.earlyPaymentCycles.value = 0;
}
}
function validateEarlyCycles() {
if (earlyPaymentCycles.value < 2) {
earlyPaymentCycles.value = 2;
}
if (earlyPaymentCycles.value > maxEarlyCycles.value) {
earlyPaymentCycles.value = maxEarlyCycles.value;
}
calculator.earlyPaymentCycles.value = earlyPaymentCycles.value;
}
async function printContent() {
if (!selectedProduct.value || !selectedNewPolicy.value) {
$snackbar('Vui lòng chọn sản phẩm và chính sách mới', { type: 'is-warning' });
return;
}
await nextTick();
const docId = 'print-area';
const fileName = `${selectedNewPolicy.value?.name || 'Payment Schedule'} - ${selectedProduct.value?.code}`;
const printElement = document.getElementById(docId);
if (!printElement) {
$snackbar('Không tìm thấy vùng nội dung để in.', { type: 'is-danger' });
return;
}
const scheduleContainers = printElement.querySelectorAll('.schedule-container, .table-container');
const stickyHeaders = printElement.querySelectorAll('.table-container.schedule-container thead th');
const ignoreButtons = printElement.querySelectorAll('#ignore-print');
scheduleContainers.forEach(container => {
container.style.maxHeight = 'none';
container.style.overflow = 'visible';
});
stickyHeaders.forEach(header => {
header.style.position = 'static';
});
ignoreButtons.forEach(button => {
button.style.display = 'none';
});
$exportpdf(docId, fileName, 'a3');
setTimeout(() => {
scheduleContainers.forEach(container => {
container.style.maxHeight = '';
container.style.overflow = '';
});
stickyHeaders.forEach(header => {
header.style.position = '';
});
ignoreButtons.forEach(button => {
button.style.display = '';
});
}, 1200);
$snackbar('Đang xuất PDF...', { type: 'is-info' });
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>