1500 lines
49 KiB
Vue
1500 lines
49 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("L");
|
|
}
|
|
|
|
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>
|