Initial commit
This commit is contained in:
964
app/components/application/CalculationView.vue
Normal file
964
app/components/application/CalculationView.vue
Normal file
@@ -0,0 +1,964 @@
|
||||
<template>
|
||||
<div class="fixed-grid has-12-cols pb-2">
|
||||
<div id="customer-selection" class="grid px-3 py-3 m-0 has-background-white">
|
||||
<!-- Product Selection -->
|
||||
<div v-if="!productId" class="cell is-col-span-5">
|
||||
<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', 'land_lot_size', 'zone_type__name'],
|
||||
column: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
|
||||
first: true,
|
||||
filter: { status: 2, cart__dealer: store.dealer?.id },
|
||||
viewaddon: productViewAddon,
|
||||
}" @option="handleProductSelection" />
|
||||
</div>
|
||||
|
||||
<!-- Customer Selection -->
|
||||
<div class="cell" :class="productId ? 'is-col-span-10' : 'is-col-span-5'">
|
||||
<p class="is-size-6 has-text-weight-bold mb-2">Chọn khách hàng</p>
|
||||
<SearchBox v-bind="{
|
||||
api: 'customer',
|
||||
field: 'label',
|
||||
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
|
||||
column: ['code', 'fullname', 'phone', 'legal_code'],
|
||||
first: true,
|
||||
disabled: !productData,
|
||||
viewaddon: customerViewAddon,
|
||||
addon: $getEditRights('edit', { code: 'customer', category: 'topmenu' }) ? customerViewAdd : undefined,
|
||||
}" @option="handleCustomerSelection" />
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="cell is-col-span-2 is-flex is-align-items-flex-end">
|
||||
<button v-if="contractData" class="button is-info has-text-white is-fullwidth" @click="openContractModal">
|
||||
<span>Xem hợp đồng</span>
|
||||
</button>
|
||||
<button v-else-if="$getEditRights()" class="button is-success has-text-white is-fullwidth" :disabled="!canCreateTransaction"
|
||||
:class="{ 'is-loading': isCreatingTransaction }" @click="createTransaction" style="height: 2.5em;">
|
||||
Tạo giao dịch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Policy Selection -->
|
||||
<div v-if="productData && policies.length > 0" class="px-3 py-3 m-0 has-background-white mb-3">
|
||||
<p class="is-size-6 has-text-weight-bold mb-3">Chọn chính sách thanh toán</p>
|
||||
<div class="tabs is-toggle">
|
||||
<ul>
|
||||
<li v-for="policy in policies" :key="policy.id" :class="{ 'is-active': selectedPolicy?.id === policy.id }">
|
||||
<a @click="selectPolicy(policy)">
|
||||
<span>{{ policy.name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contract Date -->
|
||||
<div v-if="productData && selectedPolicy" class="px-3 mb-3">
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Ngày ký</label>
|
||||
<Datepicker :record="dateRecord" attr="contractDate" @date="updateContractDate" position="is-bottom-left" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gift Selection -->
|
||||
<div v-if="productData && selectedPolicy" class="px-3 mb-3 has-background-white p-4">
|
||||
<p class="is-size-6 has-text-weight-bold mb-3">Chọn quà tặng</p>
|
||||
<div v-if="availableGifts.length > 0" class="buttons">
|
||||
<button
|
||||
v-for="gift in availableGifts"
|
||||
:key="gift.id"
|
||||
class="button"
|
||||
:class="isGiftSelected(gift.id) ? 'is-success' : 'is-primary is-outlined'"
|
||||
@click="toggleGift(gift)">
|
||||
<span class="icon" v-if="isGiftSelected(gift.id)">
|
||||
<SvgIcon v-bind="{ name: 'check.svg', type: 'white', size: 16 }"></SvgIcon>
|
||||
</span>
|
||||
<span>{{ gift.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="notification is-light">
|
||||
Không có quà tặng khả dụng
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discount Selection -->
|
||||
<div v-if="productData && selectedPolicy && selectedPolicy.contract_allocation_percentage == 100" class="px-3 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>
|
||||
|
||||
<!-- THÊM DROPDOWN CHỌ BASE PRICE TYPE -->
|
||||
<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 is-4" >
|
||||
<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" style="height: 100%">
|
||||
{{ row.selectedData.type === 1 ? '%' : 'đ' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<button class="button is-warning is-fullwidth" @click="removeDiscountRow(index)">
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 16 }"></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Early Payment Section -->
|
||||
<div
|
||||
v-if="productData && selectedPolicy && selectedPolicy.contract_allocation_percentage == 100 && selectedPolicy.method === 1"
|
||||
class="px-3 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 }}) đợt
|
||||
</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>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Payment Schedule Presentation Component -->
|
||||
<PaymentSchedulePresentation v-if="productData && policies.length > 0" :productData="productData"
|
||||
:selectedPolicy="selectedPolicy" :selectedCustomer="selectedCustomer" :calculatorData="calculatorOutputData"
|
||||
:isLoading="isCreatingTransaction" @print="printContent" />
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<Modal v-if="showPhaseModal" component="transaction/TransactionPhaseForm" title="Chọn loại giao dịch" width="40%"
|
||||
height="auto" @close="showPhaseModal = false" @phaseSelected="handlePhaseSelection"
|
||||
:vbind="{ filterPhases: phaseFilterOptions }" />
|
||||
<Modal v-if="showConfirmModal" @close="showConfirmModal = false" @confirm="executeTransactionCreation"
|
||||
v-bind="confirmModalConfig" />
|
||||
<Modal v-if="showContractModal" @close="showContractModal = false" @dataevent="handleContractUpdated"
|
||||
v-bind="contractModalConfig" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useStore } from "@/stores/index";
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import SearchBox from '~/components/SearchBox.vue';
|
||||
import Modal from '~/components/Modal.vue';
|
||||
import TransactionConfirmModal from '~/components/transaction/TransactionConfirmModal.vue';
|
||||
import PaymentSchedulePresentation from './PaymentSchedulePresentation.vue';
|
||||
import Datepicker from '~/components/datepicker/Datepicker.vue';
|
||||
import SvgIcon from '~/components/SvgIcon.vue';
|
||||
import { useAdvancedWorkflow } from '@/composables/useAdvancedWorkflow';
|
||||
import { usePaymentCalculator } from '@/composables/usePaymentCalculator';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SearchBox,
|
||||
Modal,
|
||||
TransactionConfirmModal,
|
||||
PaymentSchedulePresentation,
|
||||
Datepicker,
|
||||
SvgIcon,
|
||||
},
|
||||
props: {
|
||||
productId: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const { $snackbar, $getdata, $dialog, $exportpdf } = useNuxtApp();
|
||||
const { isLoading, createWorkflowTransaction } = useAdvancedWorkflow();
|
||||
|
||||
const calculator = usePaymentCalculator();
|
||||
|
||||
return {
|
||||
store,
|
||||
$snackbar,
|
||||
$getdata,
|
||||
$dialog,
|
||||
$exportpdf,
|
||||
isCreatingTransaction: isLoading,
|
||||
createFullTransaction: createWorkflowTransaction,
|
||||
calculator
|
||||
};
|
||||
},
|
||||
data() {
|
||||
const initialContractDate = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
return {
|
||||
productData: null,
|
||||
selectedCustomer: null,
|
||||
policies: [],
|
||||
selectedPolicy: null,
|
||||
allPaymentPlans: [],
|
||||
paymentPlans: [],
|
||||
|
||||
// Gift Selection
|
||||
availableGifts: [],
|
||||
selectedGifts: [],
|
||||
|
||||
discountRows: [{
|
||||
key: Date.now(),
|
||||
selectedData: null,
|
||||
customValue: 0,
|
||||
basePriceType: 'contract',
|
||||
calculatedAmount: 0
|
||||
}],
|
||||
draggedIndex: null,
|
||||
|
||||
// Early Payment
|
||||
enableEarlyPayment: false,
|
||||
earlyPaymentCycles: 2,
|
||||
|
||||
// Contract Date
|
||||
contractDate: initialContractDate,
|
||||
dateRecord: { contractDate: initialContractDate },
|
||||
|
||||
productViewAddon: {
|
||||
component: "product/ProductView",
|
||||
width: "70%",
|
||||
height: "500px",
|
||||
title: "Thông tin sản phẩm",
|
||||
},
|
||||
customerViewAddon: {
|
||||
component: "customer/CustomerView",
|
||||
width: "70%",
|
||||
height: "500px",
|
||||
title: "Thông tin khách hàng",
|
||||
},
|
||||
customerViewAdd: {
|
||||
component: "customer/CustomerInfo2",
|
||||
width: "60%",
|
||||
height: "auto",
|
||||
title: this.store.lang === "en" ? "Edit Customer" : "Chỉnh sửa khách hàng",
|
||||
},
|
||||
showPhaseModal: false,
|
||||
selectedPhaseInfo: null,
|
||||
showConfirmModal: false,
|
||||
contractData: null,
|
||||
showContractModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canCreateTransaction() {
|
||||
return this.productData && this.selectedCustomer && this.selectedPolicy;
|
||||
},
|
||||
|
||||
maxEarlyCycles() {
|
||||
return this.paymentPlans?.length || 0;
|
||||
},
|
||||
|
||||
calculatorOutputData() {
|
||||
return {
|
||||
originPrice: this.calculator.originalPrice.value,
|
||||
totalDiscount: this.calculator.totalDiscount.value,
|
||||
salePrice: this.calculator.finalTotal.value,
|
||||
allocatedPrice: this.calculator.finalTotal.value,
|
||||
originalPaymentSchedule: this.calculator.originalPaymentSchedule.value,
|
||||
finalPaymentSchedule: this.calculator.finalPaymentSchedule.value,
|
||||
earlyDiscountDetails: this.calculator.earlyDiscountDetails.value,
|
||||
totalRemaining: this.calculator.totalRemaining.value,
|
||||
detailedDiscounts: this.calculator.detailedDiscounts.value,
|
||||
baseDate: this.calculator.startDate.value
|
||||
};
|
||||
},
|
||||
|
||||
phaseFilterOptions() {
|
||||
if (!this.selectedPolicy) {
|
||||
return ['reserved', 'deposit', 'fulfillwish'];
|
||||
}
|
||||
|
||||
if (this.store.dealer) {
|
||||
return ['reserved', 'deposit'];
|
||||
}
|
||||
if (this.selectedPolicy.code === 'CS-TT-THNV') {
|
||||
return ['fulfillwish', 'reserved'];
|
||||
} else {
|
||||
return ['deposit', 'reserved'];
|
||||
}
|
||||
},
|
||||
|
||||
confirmModalConfig() {
|
||||
if (!this.productData || !this.selectedPhaseInfo || !this.selectedPolicy || !this.selectedCustomer) return {};
|
||||
return {
|
||||
component: "transaction/TransactionConfirmModal",
|
||||
title: "Xác nhận Giao dịch",
|
||||
width: "60%",
|
||||
height: "auto",
|
||||
vbind: {
|
||||
productData: this.productData,
|
||||
phaseInfo: this.selectedPhaseInfo,
|
||||
selectedPolicy: this.selectedPolicy,
|
||||
originPrice: this.calculator.originalPrice.value,
|
||||
discountValueDisplay: this.calculator.totalDiscount.value,
|
||||
selectedCustomer: this.selectedCustomer,
|
||||
initialContractDate: this.contractDate
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
contractModalConfig() {
|
||||
if (!this.contractData) return {};
|
||||
return {
|
||||
component: "application/Contract",
|
||||
title: "Hợp đồng giao dịch",
|
||||
width: "95%",
|
||||
height: "95vh",
|
||||
vbind: {
|
||||
row: {
|
||||
id: this.contractData.id
|
||||
}
|
||||
},
|
||||
event: "contractUpdated",
|
||||
eventname: "dataevent",
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
// Watch discountRows để cập nhật vào calculator
|
||||
discountRows: {
|
||||
handler(newRows) {
|
||||
const validDiscounts = newRows
|
||||
.filter(r => r.selectedData)
|
||||
.map(r => {
|
||||
if (r.selectedData.type === 1) {
|
||||
// Phần trăm - tính toán dựa trên base price type
|
||||
let basePrice = 0;
|
||||
switch (r.basePriceType) {
|
||||
case 'with_vat':
|
||||
basePrice = this.productData?.origin_price || 0;
|
||||
break;
|
||||
case 'without_vat':
|
||||
basePrice = this.productData?.price_excluding_vat ||
|
||||
(this.productData?.origin_price || 0) / 1.1;
|
||||
break;
|
||||
case 'contract':
|
||||
default:
|
||||
return {
|
||||
id: r.selectedData.id,
|
||||
name: r.selectedData.name,
|
||||
code: r.selectedData.code,
|
||||
type: r.selectedData.type,
|
||||
value: r.customValue || 0
|
||||
};
|
||||
}
|
||||
|
||||
const calculatedAmount = (basePrice * (r.customValue || 0)) / 100;
|
||||
r.calculatedAmount = calculatedAmount;
|
||||
|
||||
return {
|
||||
id: r.selectedData.id,
|
||||
name: `${r.selectedData.name} (${r.basePriceType === 'with_vat' ? 'Giá đã VAT' : 'Giá chưa VAT'})`,
|
||||
code: r.selectedData.code,
|
||||
type: 2, // Chuyển sang type 2 (tiền mặt) sau khi tính toán
|
||||
value: Math.round(calculatedAmount)
|
||||
};
|
||||
} else {
|
||||
// Tiền mặt
|
||||
r.calculatedAmount = 0;
|
||||
return {
|
||||
id: r.selectedData.id,
|
||||
name: r.selectedData.name,
|
||||
code: r.selectedData.code,
|
||||
type: r.selectedData.type,
|
||||
value: r.customValue || 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this.calculator.discounts.value = validDiscounts;
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
|
||||
earlyPaymentCycles(newVal) {
|
||||
if (this.enableEarlyPayment && newVal > 0) {
|
||||
this.calculator.earlyPaymentCycles.value = newVal;
|
||||
}
|
||||
},
|
||||
|
||||
enableEarlyPayment(newVal) {
|
||||
if (!newVal) {
|
||||
this.calculator.earlyPaymentCycles.value = 0;
|
||||
} else {
|
||||
this.calculator.earlyPaymentCycles.value = this.earlyPaymentCycles;
|
||||
}
|
||||
},
|
||||
|
||||
contractDate(newVal) {
|
||||
if (newVal) {
|
||||
this.calculator.startDate.value = new Date(newVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.loadAllData();
|
||||
if (this.productId) {
|
||||
await this.loadProductById(this.productId);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadProductById(id) {
|
||||
const product = await this.$getdata('product', { id: id }, undefined, true);
|
||||
if (product) {
|
||||
await this.handleProductSelection(product);
|
||||
} else {
|
||||
this.$snackbar(`Không tìm thấy sản phẩm với ID: ${id}`, { type: 'is-danger' });
|
||||
}
|
||||
},
|
||||
|
||||
updateContractDate(newDate) {
|
||||
this.contractDate = newDate;
|
||||
this.dateRecord.contractDate = newDate;
|
||||
this.calculator.startDate.value = new Date(newDate);
|
||||
},
|
||||
|
||||
handleEarlyPaymentToggle() {
|
||||
if (this.enableEarlyPayment) {
|
||||
this.earlyPaymentCycles = Math.min(2, this.maxEarlyCycles);
|
||||
this.calculator.earlyPaymentCycles.value = this.earlyPaymentCycles;
|
||||
} else {
|
||||
this.calculator.earlyPaymentCycles.value = 0;
|
||||
}
|
||||
},
|
||||
|
||||
validateEarlyCycles() {
|
||||
if (this.earlyPaymentCycles < 2) {
|
||||
this.earlyPaymentCycles = 2;
|
||||
}
|
||||
if (this.earlyPaymentCycles > this.maxEarlyCycles) {
|
||||
this.earlyPaymentCycles = this.maxEarlyCycles;
|
||||
}
|
||||
this.calculator.earlyPaymentCycles.value = this.earlyPaymentCycles;
|
||||
},
|
||||
|
||||
// Gift Selection Methods
|
||||
isGiftSelected(giftId) {
|
||||
return this.selectedGifts.some(g => g.id === giftId);
|
||||
},
|
||||
|
||||
toggleGift(gift) {
|
||||
const index = this.selectedGifts.findIndex(g => g.id === gift.id);
|
||||
if (index > -1) {
|
||||
// Đã chọn -> bỏ chọn
|
||||
this.selectedGifts.splice(index, 1);
|
||||
} else {
|
||||
// Chưa chọn -> thêm vào
|
||||
this.selectedGifts.push({ id: gift.id });
|
||||
}
|
||||
},
|
||||
|
||||
async loadAvailableGifts() {
|
||||
try {
|
||||
const gifts = await this.$getdata('gift', undefined, undefined, false);
|
||||
if (gifts && Array.isArray(gifts)) {
|
||||
this.availableGifts = gifts;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading gifts:', error);
|
||||
this.availableGifts = [];
|
||||
}
|
||||
},
|
||||
|
||||
addNewDiscountRow() {
|
||||
this.discountRows.push({
|
||||
key: Date.now(),
|
||||
selectedData: null,
|
||||
customValue: 0,
|
||||
basePriceType: 'contract',
|
||||
calculatedAmount: 0
|
||||
});
|
||||
},
|
||||
|
||||
removeDiscountRow(index) {
|
||||
this.discountRows.splice(index, 1);
|
||||
if (!this.discountRows.length) this.addNewDiscountRow();
|
||||
},
|
||||
|
||||
handleRowSelect(index, data) {
|
||||
const row = this.discountRows[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';
|
||||
this.recalculateDiscount(index);
|
||||
} else {
|
||||
row.calculatedAmount = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
recalculateDiscount(index) {
|
||||
const row = this.discountRows[index];
|
||||
if (!row.selectedData || row.selectedData.type !== 1) return;
|
||||
|
||||
let basePrice = 0;
|
||||
switch (row.basePriceType) {
|
||||
case 'with_vat':
|
||||
basePrice = this.productData?.origin_price || 0;
|
||||
break;
|
||||
case 'without_vat':
|
||||
basePrice = this.productData?.price_excluding_vat ||
|
||||
(this.productData?.origin_price || 0) / 1.1;
|
||||
break;
|
||||
case 'contract':
|
||||
default:
|
||||
row.calculatedAmount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
row.calculatedAmount = (basePrice * (row.customValue || 0)) / 100;
|
||||
},
|
||||
|
||||
validateRowValue(index) {
|
||||
const row = this.discountRows[index];
|
||||
if (!row.selectedData) return;
|
||||
|
||||
if (row.selectedData.type === 1) {
|
||||
if (row.customValue > 100) row.customValue = 100;
|
||||
if (row.customValue < 0) row.customValue = 0;
|
||||
this.recalculateDiscount(index);
|
||||
} else {
|
||||
if (row.customValue < 0) row.customValue = 0;
|
||||
}
|
||||
},
|
||||
|
||||
dragStart(e) {
|
||||
this.draggedIndex = parseInt(e.currentTarget.getAttribute('data-index'));
|
||||
e.currentTarget.style.opacity = '0.5';
|
||||
},
|
||||
|
||||
dragOver(e) {
|
||||
e.preventDefault();
|
||||
const target = e.currentTarget;
|
||||
if (target.getAttribute('data-index') !== null) {
|
||||
target.style.borderTop = '2px solid #204853';
|
||||
}
|
||||
},
|
||||
|
||||
drop(e) {
|
||||
e.preventDefault();
|
||||
const dropIndex = parseInt(e.currentTarget.getAttribute('data-index'));
|
||||
|
||||
if (this.draggedIndex !== null && this.draggedIndex !== dropIndex) {
|
||||
const draggedRow = this.discountRows[this.draggedIndex];
|
||||
this.discountRows.splice(this.draggedIndex, 1);
|
||||
this.discountRows.splice(dropIndex, 0, draggedRow);
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-index]').forEach(row => {
|
||||
row.style.borderTop = '';
|
||||
row.style.opacity = '1';
|
||||
});
|
||||
},
|
||||
|
||||
dragEnd(e) {
|
||||
document.querySelectorAll('[data-index]').forEach(row => {
|
||||
row.style.borderTop = '';
|
||||
row.style.opacity = '1';
|
||||
});
|
||||
this.draggedIndex = null;
|
||||
},
|
||||
|
||||
handleCustomerSelection(customer) {
|
||||
this.selectedCustomer = customer;
|
||||
},
|
||||
|
||||
async handleProductSelection(product) {
|
||||
if (!product) {
|
||||
this.productData = null;
|
||||
this.selectedPolicy = null;
|
||||
this.paymentPlans = [];
|
||||
this.discountRows = [{
|
||||
key: Date.now(),
|
||||
selectedData: null,
|
||||
customValue: 0,
|
||||
basePriceType: 'contract',
|
||||
calculatedAmount: 0
|
||||
}];
|
||||
this.selectedCustomer = null;
|
||||
this.contractData = null;
|
||||
this.enableEarlyPayment = false;
|
||||
this.earlyPaymentCycles = 2;
|
||||
this.selectedGifts = [];
|
||||
|
||||
this.calculator.originPrice.value = 0;
|
||||
this.calculator.discounts.value = [];
|
||||
this.calculator.paymentPlan.value = [];
|
||||
this.calculator.paidAmount.value = 0;
|
||||
this.calculator.earlyPaymentCycles.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!product.trade_code && !product.code) {
|
||||
this.$snackbar('Không tìm thấy mã sản phẩm hợp lệ', { type: 'is-danger' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.productData = product;
|
||||
this.selectedPolicy = null;
|
||||
this.paymentPlans = [];
|
||||
this.discountRows = [{
|
||||
key: Date.now(),
|
||||
selectedData: null,
|
||||
customValue: 0,
|
||||
basePriceType: 'contract',
|
||||
calculatedAmount: 0
|
||||
}];
|
||||
this.contractData = null;
|
||||
this.enableEarlyPayment = false;
|
||||
this.earlyPaymentCycles = 2;
|
||||
this.selectedGifts = [];
|
||||
|
||||
this.calculator.originPrice.value = product.origin_price || 0;
|
||||
this.calculator.discounts.value = [];
|
||||
this.calculator.startDate.value = new Date(this.contractDate);
|
||||
this.calculator.earlyPaymentCycles.value = 0;
|
||||
|
||||
if (this.policies.length > 0) {
|
||||
this.selectPolicy(this.policies[0]);
|
||||
} else {
|
||||
this.$snackbar('Không có chính sách thanh toán cho sản phẩm này', { type: 'is-warning' });
|
||||
}
|
||||
},
|
||||
|
||||
async loadAllData() {
|
||||
try {
|
||||
this.policies = await this.$getdata("salepolicy", { enable: "True" }, undefined, false);
|
||||
this.allPaymentPlans = await this.$getdata("paymentplan", { policy__enable: "True" }, undefined, false);
|
||||
if (this.allPaymentPlans) {
|
||||
this.allPaymentPlans.sort((a, b) => a.cycle - b.cycle);
|
||||
}
|
||||
|
||||
// Load gifts
|
||||
await this.loadAvailableGifts();
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
}
|
||||
},
|
||||
|
||||
selectPolicy(policy) {
|
||||
this.selectedPolicy = policy;
|
||||
this.loadPlansForPolicy(policy.id);
|
||||
|
||||
this.calculator.contractAllocationPercentage.value = policy.contract_allocation_percentage || 100;
|
||||
|
||||
this.enableEarlyPayment = false;
|
||||
this.earlyPaymentCycles = 2;
|
||||
this.calculator.earlyPaymentCycles.value = 0;
|
||||
|
||||
if (policy.contract_allocation_percentage < 100) {
|
||||
this.discountRows = [{
|
||||
key: Date.now(),
|
||||
selectedData: null,
|
||||
customValue: 0,
|
||||
basePriceType: 'contract',
|
||||
calculatedAmount: 0
|
||||
}];
|
||||
}
|
||||
},
|
||||
|
||||
loadPlansForPolicy(id) {
|
||||
const policy = this.policies.find(pol => pol.id === id);
|
||||
if (!policy) return;
|
||||
|
||||
this.selectedPolicy = policy;
|
||||
this.paymentPlans = this.allPaymentPlans.filter(plan => plan.policy === id);
|
||||
|
||||
if (this.paymentPlans.length > 0) {
|
||||
this.calculator.paymentPlan.value = this.paymentPlans.map(p => ({
|
||||
cycle: p.cycle,
|
||||
value: p.value,
|
||||
type: p.type,
|
||||
days: p.days,
|
||||
payment_note: p.payment_note,
|
||||
due_note: p.due_note
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
createTransaction() {
|
||||
const invalidRow = this.discountRows.find(r => r.selectedData && r.selectedData.type !== 1 && r.customValue < 1000);
|
||||
if (invalidRow) {
|
||||
this.$snackbar('Giá trị tiền mặt phải >= 1.000', { type: 'is-warning' });
|
||||
return;
|
||||
}
|
||||
this.showPhaseModal = true;
|
||||
},
|
||||
|
||||
handlePhaseSelection(phaseInfo) {
|
||||
this.showPhaseModal = false;
|
||||
this.selectedPhaseInfo = phaseInfo;
|
||||
this.showConfirmModal = true;
|
||||
},
|
||||
|
||||
async executeTransactionCreation({ currentDate, dueDate, paymentAmount, depositReceived, people }) {
|
||||
this.showConfirmModal = false;
|
||||
|
||||
this.calculator.startDate.value = new Date(currentDate);
|
||||
this.calculator.paidAmount.value = 0;
|
||||
|
||||
const finalSchedule = this.calculator.finalPaymentSchedule.value;
|
||||
|
||||
const paymentPlansForBackend = finalSchedule.map((plan, index) => ({
|
||||
amount: plan.amount,
|
||||
due_days: plan.days,
|
||||
cycle: plan.cycle,
|
||||
note: plan.payment_note,
|
||||
is_early_merged: plan.is_merged || false,
|
||||
merged_cycles: plan.is_merged ? (plan.original_cycles || []) : [],
|
||||
raw_amount: plan.amount,
|
||||
paid_amount: plan.paid_amount,
|
||||
remain_amount: plan.remain_amount,
|
||||
from_date: dayjs(plan.from_date).format('YYYY-MM-DD'),
|
||||
to_date: dayjs(plan.to_date).format('YYYY-MM-DD'),
|
||||
due_note: plan.due_note || ''
|
||||
}));
|
||||
|
||||
if (paymentPlansForBackend.length > 0) {
|
||||
paymentPlansForBackend[0].remain_amount = Math.max(0, paymentPlansForBackend[0].remain_amount - paymentAmount);
|
||||
paymentPlansForBackend[0].amount = Math.max(0, paymentPlansForBackend[0].amount - paymentAmount);
|
||||
}
|
||||
|
||||
const totalEarlyDiscount = this.enableEarlyPayment
|
||||
? this.calculator.totalEarlyDiscount.value
|
||||
: 0;
|
||||
|
||||
const params = {
|
||||
product: this.productData,
|
||||
customer: this.selectedCustomer,
|
||||
policy: this.selectedPolicy,
|
||||
phaseInfo: this.selectedPhaseInfo,
|
||||
priceAfterDiscount: this.calculator.finalTotal.value,
|
||||
discountValue: this.calculator.totalDiscount.value,
|
||||
detailedDiscounts: this.calculator.detailedDiscounts.value,
|
||||
paymentPlans: paymentPlansForBackend,
|
||||
currentDate,
|
||||
reservationDueDate: dueDate,
|
||||
reservationAmount: paymentAmount,
|
||||
depositReceived: depositReceived,
|
||||
people: people,
|
||||
earlyDiscountAmount: totalEarlyDiscount,
|
||||
gifts: this.selectedGifts // Thêm danh sách quà tặng
|
||||
};
|
||||
|
||||
const result = await this.createFullTransaction(params);
|
||||
|
||||
if (result && result.transaction) {
|
||||
this.contractData = result.transaction;
|
||||
this.showContractModal = true;
|
||||
} else {
|
||||
this.$snackbar('Tạo giao dịch thất bại. Vui lòng thử lại.', { type: 'is-danger' });
|
||||
}
|
||||
|
||||
this.selectedPhaseInfo = null;
|
||||
},
|
||||
|
||||
openContractModal() {
|
||||
if (!this.contractData) {
|
||||
this.$snackbar('Chưa có dữ liệu hợp đồng để xem.', { type: 'is-warning' });
|
||||
return;
|
||||
}
|
||||
this.showContractModal = true;
|
||||
},
|
||||
|
||||
handleContractUpdated(eventData) {
|
||||
if (eventData?.data) {
|
||||
this.contractData = { ...this.contractData, ...eventData.data };
|
||||
this.$snackbar("Hợp đồng đã được cập nhật");
|
||||
}
|
||||
},
|
||||
|
||||
printContent() {
|
||||
if (!this.productData || !this.selectedPolicy) {
|
||||
this.$snackbar('Vui lòng tìm sản phẩm và chọn chính sách', { type: 'is-warning' });
|
||||
return;
|
||||
}
|
||||
const docId = 'print-area';
|
||||
const fileName = `${this.selectedPolicy?.name || 'Payment Schedule'} - ${this.productData?.code}`;
|
||||
const printElement = document.getElementById(docId);
|
||||
if (!printElement) return;
|
||||
|
||||
const scheduleContainers = printElement.querySelectorAll('.schedule-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';
|
||||
});
|
||||
this.$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);
|
||||
this.$snackbar('Đang xuất PDF...', { type: 'is-info' });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#customer-selection {
|
||||
position: sticky;
|
||||
z-index: 5;
|
||||
top: 52px;
|
||||
}
|
||||
|
||||
.modal-card-body #customer-selection {
|
||||
top: -16px;
|
||||
}
|
||||
|
||||
.button.is-dashed-border {
|
||||
border: 1px dashed #b5b5b5;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.button.is-dashed-border:hover {
|
||||
border-color: #204853;
|
||||
color: #204853 !important;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.schedule-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table-container.schedule-container thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: white;
|
||||
z-index: 2;
|
||||
border-bottom: 1px solid #dbdbdb !important;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
li.is-active a,
|
||||
li a:hover {
|
||||
color: white !important;
|
||||
background-color: #204853 !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.schedule-container {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.table-container.schedule-container thead th {
|
||||
position: static;
|
||||
}
|
||||
|
||||
#ignore-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user