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

1103 lines
40 KiB
Vue

<template>
<div :id="docid" class="container-fluid is-flex is-flex-direction-column is-gap-2">
<div class="is-flex is-flex-direction-column is-gap-3">
<!-- PRODUCT SECTION -->
<div v-if="product">
<Caption v-bind="{ title: isVietnamese ? 'Sản phẩm' : 'Product' }"></Caption>
<div class="columns is-multiline mx-0">
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Tên thương mại' : 'Trade Code' }}</label>
<div class="control">
<a class="has-text-primary" @click="openLink()">{{ product.trade_code }}</a>
<a class="ml-4" id="ignore" @click="$copyToClipboard(product.trade_code)"><SvgIcon name="copy.svg" type="primary" :size="18" /></a>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Tên quy hoạch' : 'Zone Code' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(product.zone_code)">{{ product.zone_code }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Loại sản phẩm' : 'Type' }}</label>
<div class="control">
<span>{{ product.type__name }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Mẫu biệt thự' : 'Villa Model' }}</label>
<div class="control">
<span>{{ product.template_name }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Phân khu' : 'Zone Type' }}</label>
<div class="control">
<span>{{ product.zone_type__name }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Hướng cửa' : 'Direction' }}</label>
<div class="control">
<span>{{ product.direction__name }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Diện tích đất' : 'Lot Area' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(product.lot_area)">{{ $numtoString(product.lot_area) }}m²</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Diện tích xây dựng' : 'Building Area' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(product.building_area)">{{ $numtoString(product.building_area)
}}m²</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">
{{ isVietnamese
? `Diện tích sàn (${product.number_of_floors} tầng)`
: `Total Built Area (${product.number_of_floors} floors)`
}}
</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(product.total_built_area)">{{ $numtoString(product.total_built_area)
}}m²</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Kích thước lô' : 'Land Lot Size' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(product.land_lot_size)">{{ product.land_lot_size
}}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Dự án' : 'Project' }}</label>
<div class="control">
<span>{{ product.project__name }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Trạng thái' : 'Status' }}</label>
<div class="control">
<span>{{ productstatus?.name }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Giá gốc' : 'Original price' }}</label>
<div class="control">
<span>{{ $numtoString(product?.origin_price) }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Giỏ hàng' : 'Cart' }}</label>
<div class="control">
<span>{{ product?.cart__name || '/' }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Đại lý bán hàng' : 'Dealer' }}</label>
<div class="control">
<span>{{ product?.cart__dealer__name || '/' }}</span>
</div>
</div>
</div>
</div>
<!-- QUICK ADD SECTION -->
<div v-if="
quickAdd
&& (product.status === 2 || quickAddedTransaction)
&& (
$getEditRights('edit', { code: 'product', category: 'submenu' })
|| $getEditRights('edit', { code: 'product-setting', category: 'submenu' })
)
">
<Caption v-bind="{ title: 'Giữ chỗ nhanh' }"></Caption>
<div class="columns is-multiline mx-0 mt-0">
<div class="column is-narrow pl-0">
<span class="label is-size-7" style="color: inherit">{{ isVietnamese ? 'Khóa sản phẩm' : 'Lock Product' }}</span>
<div class="field">
<div class="is-flex is-align-items-center is-clickable"
:style="{ opacity: product.status !== 2 ? 0.5 : 1, cursor: product.status !== 2 ? 'not-allowed' : 'pointer' }"
@click="product.status === 2 && (isLockingProduct = !isLockingProduct)">
<SvgIcon v-bind="{
name: isLockingProduct ? 'checked.svg' : 'uncheck.svg',
type: isLockingProduct ? 'primary' : 'twitter',
size: 32
}" />
</div>
</div>
</div>
<div v-if="isLockingProduct" class="column is-2">
<div class="field">
<label class="label is-size-7">{{ isVietnamese ? 'Thời gian (phút)' : 'Duration (min)' }}</label>
<div class="control">
<input class="input" type="number" v-model.number="lockDurationMinutes" min="1"
:placeholder="isVietnamese ? 'Nhập số phút' : 'Enter minutes'">
</div>
</div>
</div>
<div v-if="isLockingProduct" class="column is-narrow is-flex is-align-items-flex-end">
<button class="button is-primary is-fullwidth" @click="confirmQuickAddTransaction">
{{ isVietnamese ? 'Xác nhận' : 'Confirm' }}
</button>
</div>
</div>
<!-- <div v-if="productnotes?.length > 0" class="column is-full mt-3">
<p v-for="{ detail } in productnotes" :key="detail">
<span>{{ detail }}</span>
</p>
</div> -->
</div>
</div>
<!-- CUSTOMER SECTION -->
<div v-if="record && $getEditRights('view', { code: 'customer', category: 'topmenu' })">
<Caption v-bind="{ title: data && findFieldName('customer')[lang] }"></Caption>
<div class="columns is-multiline mx-0">
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("custcode")[lang] }}</label>
<div class="control">
<span class="hyperlink has-text-primary" @click="openCustomer()">{{ record.code }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("name")[lang] }}</label>
<div class="control">
{{ record.fullname }}
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("phone_number")[lang] }}</label>
<div class="control">
<span class="hyperlink" @click="openPhone()">{{ record.phone }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">Email:</label>
<div class="control" style="word-break: break-all">
{{ record.email || "/" }}
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("personal_id")[lang] }}</label>
<div class="control">
{{ record.legal_type__name || "/" }}
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("idnum")[lang] }}</label>
<div class="control">
{{ record.legal_code || "/" }}
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("issued_date")[lang] }}</label>
<div class="control">
{{ record.issued_date ? $dayjs(record.issued_date).format("DD/MM/YYYY") : "/" }}
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("issued_place")[lang] }}</label>
<div class="control">
{{ record.issued_place__name || "/" }}
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("country")[lang] }}</label>
<div class="control">
{{ isVietnamese ? record.country__name : record.country__en || "/" }}
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("address")[lang] }}</label>
<div class="control">
{{ record.address || "/" }}
</div>
</div>
</div>
<!-- <div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("created_by")[lang] }}</label>
<div class="control">
<span class="hyperlink" @click="openUser(record.creator)">{{ record.creator__fullname || "/" }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("updated_by")[lang] }}</label>
<div class="control">
<span class="hyperlink" @click="openUser(record.updater)">{{ record.updater__fullname || "/" }}</span>
</div>
</div>
</div> -->
</div>
</div>
<!-- TRANSACTION SECTION -->
<div v-if="transaction">
<Caption v-bind="{ title: isVietnamese ? 'Giao dịch' : 'Transaction' }"></Caption>
<div class="columns is-multiline mx-0">
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Mã giao dịch' : 'Transaction code' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(transaction.code)">{{ transaction.code }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0" v-if="transactionphase">
<div class="field">
<label class="label">Giai đoạn</label>
<div class="control">
<span>{{ transactionphase.name }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ data && findFieldName("create-time")[lang] }}</label>
<div class="control">
<span>{{ $dayjs(transaction.create_time).format("DD/MM/YYYY HH:mm") }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0" v-if="salepolicy">
<div class="field">
<label class="label">{{ isVietnamese ? 'Chính sách bán hàng' : 'Sale Policy' }}</label>
<div class="control">
<span class="hyperlink has-text-primary" @click="openPoli()">{{ salepolicy.code }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Chiết khấu' : 'Discount' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(transaction.discount_amount)">{{
$numtoString(transaction.discount_amount) }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Giá hợp đồng' : 'Sale Price' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(transaction.sale_price)">{{
$numtoString(transaction.sale_price) }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Yêu cầu đặt cọc' : 'Deposit Required' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(transaction.deposit_amount)">{{
$numtoString(transaction.deposit_amount) }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Đã cọc' : 'Deposit Received' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(transaction.deposit_received)">{{
$numtoString(transaction.deposit_received) }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Tiền cọc còn lại' : 'Deposit Remaining' }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(transaction.deposit_remaining)">{{
$numtoString(transaction.deposit_remaining) }}</span>
</div>
</div>
</div>
<div class="column is-one-fifth pb-1 px-0">
<div class="field">
<label class="label">{{ isVietnamese ? 'Tổng số tiền đã nhận' : 'Total Amount Received' }}</label>
<div class="control">
<span class="hyperlink has-text-primary" @click="openMoney()">{{
$numtoString(transaction.amount_received)
}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- GIFT SECTION -->
<div v-if="transactionId && gift && gift.length > 0">
<Caption class="mb-1" v-bind="{ title: isVietnamese ? 'Danh sách quà tặng' : 'Payment/Reservation Schedule' }"></Caption>
<DataView :key="paymentScheduleKey" v-bind="{
api: 'transactiongift',
params: {
filter: { transaction: transactionId },
values: 'gift__code,gift__name,gift__detail'
},
pagename: `gift_${transactionId}_${Date.now()}`,
setting: 'transaction-gift-fields',
component: `gift_${transactionId}`
}" />
</div>
<!-- PAYMENT SCHEDULE SECTION -->
<div v-if="transactionId">
<Caption class="mb-1" v-bind="{ title: isVietnamese ? 'Diễn biến giao dịch' : 'Payment/Reservation Schedule' }"></Caption>
<DataView :key="paymentScheduleKey" v-bind="{
api: 'payment_schedule',
params: {
filter: { txn_detail__transaction: transactionId },
values: 'txn_detail,txn_detail__amount,txn_detail__creator__fullname,txn_detail__code,txn_detail__date,txn_detail__due_date,txn_detail__phase__name,txn_detail__status__name,txn_detail__approver__fullname,txn_detail__approve_time',
summary: 'annotate',
distinct_values: {
count_id: {
type: 'Count',
field: 'id',
distinct: 'true'
}
}
},
pagename: `payment_schedule_${transactionId}_${Date.now()}`,
setting: 'transaction-fields',
component: `payment_schedule_${transactionId}`
}" />
</div>
<!-- ACTION BUTTONS -->
<div class="buttons is-justify-content-flex-start mt-3" id="ignore" v-if="reservation">
<button class="button is-info" @click="$exportpdf(docid, transaction?.code || 'document', 'a4', 'landscape')">
<span>{{ data && findFieldName("print")[lang] }}</span>
</button>
<template v-if="$getEditRights()">
<button v-if="activeTabCode === 'approval' && !isContractApproved && !isContractDeclined" class="button is-success ml-2"
@click="confirmContract" :disabled="isApproving" :class="{ 'is-loading': isApproving }">
<span>{{ isVietnamese ? 'Duyệt giao dịch' : 'Approve' }}</span>
</button>
<button v-if="activeTabCode === 'approval' && !isContractApproved && !isContractDeclined" class="button is-warning ml-2"
@click="confirmSupplement" :disabled="isSupplementing" :class="{ 'is-loading': isSupplementing }">
<span>{{ isVietnamese ? 'Yêu cầu bổ sung thông tin' : 'Request Supplement' }}</span>
</button>
<button v-if="activeTabCode === 'approval' && !isContractDeclined" class="button is-danger ml-2"
@click="confirmReject" :disabled="isRejecting" :class="{ 'is-loading': isRejecting }">
<span>{{ isVietnamese ? 'Từ chối giao dịch' : 'Reject' }}</span>
</button>
</template>
<div v-if="isContractApproved && activeTabCode === 'approval'"
class="notification is-success is-light ml-2 mb-0 py-2 px-3">
<span class="icon-text">
<span class="has-text-weight-semibold">{{ isVietnamese ? 'Đã duyệt' : 'Approved' }}</span>
</span>
</div>
</div>
<!-- MODAL -->
<Modal @close="showmodal = undefined" v-bind="showmodal" @modalevent="handleModalEvent"
@confirm="handleModalEvent({ name: 'confirm' })" @noteConfirm="handleNoteConfirmEvent" v-if="showmodal"></Modal>
</div>
</template>
<script>
import { useStore } from "@/stores/index";
import { useAdvancedWorkflow } from '@/composables/useAdvancedWorkflow';
export default {
setup() {
const store = useStore();
const { $getdata, $patchapi, $insertapi, $deleteapi } = useNuxtApp();
const { approveTransactionDetail, isLoading } = useAdvancedWorkflow();
return {
store,
$getdata,
$patchapi,
$insertapi,
$deleteapi,
approveTransactionDetail,
isApproving: isLoading,
};
},
props: {
pagename: String,
transactionId: [Number, String],
customerId: [Number, String],
productId: [Number, String],
reservationId: [Number, String],
quickAdd: Boolean,
},
emits: ["close"],
data() {
return {
record: undefined,
product: undefined,
transaction: undefined,
reservation: undefined,
transactionphase: undefined,
salepolicy: undefined,
productstatus: undefined,
producttype: undefined,
zonetype: undefined,
gift: undefined,
direction: undefined,
project: undefined,
productstatuses: undefined,
productnotes: undefined,
transactionFiles: [],
errors: {},
showmodal: undefined,
docid: this.$id(),
data: this.store.common,
actionToConfirm: null,
paymentScheduleKey: 0,
// Quick Add Properties
isLockingProduct: false,
lockDurationMinutes: 30,
// New properties for supplement/reject
isSupplementing: false,
isRejecting: false,
};
},
computed: {
lang() {
return this.store.lang;
},
isVietnamese() {
return this.store.lang === "vi";
},
activeTabCode() {
if (this.store.tabinfo && this.store.tabinfo.tab) {
return this.store.tabinfo.tab.code;
}
if (this.store.tabinfo && this.store.tabinfo.vbind) {
return this.store.tabinfo.vbind.tab;
}
return null;
},
isContractDeclined() {
return this.reservation?.status === 4;
},
isContractApproved() {
return this.reservation?.status === 3;
},
paymentSchedulePagename() {
return `payment_schedule_${this.transactionId}_${this.paymentScheduleKey}`;
}
},
async created() {
// Load transaction
if (this.transactionId && !this.transaction) {
this.transaction = await this.$getdata("transaction", { id: this.transactionId }, undefined, true);
this.gift = await this.$getdata("transactiongift", { transaction: this.transactionId });
}
// Load reservation
if (this.reservationId && !this.reservation) {
this.reservation = await this.$getdata("reservation", { id: this.reservationId }, undefined, true);
if (this.reservation && !this.transactionId) {
this.transactionId = this.reservation.transaction;
}
}
// Load transaction again if reservation provided ID
if (this.transactionId && !this.transaction) {
this.transaction = await this.$getdata("transaction", { id: this.transactionId }, undefined, true);
}
// Load product
if (!this.product) {
const prodId = this.productId || this.transaction?.product;
if (prodId) {
this.product = await this.$getdata("product", { id: prodId }, undefined, true);
}
}
// Load customer
if (!this.record) {
const custId = this.customerId || this.transaction?.customer;
if (custId) {
this.record = await this.$getdata("customer", { id: custId }, undefined, true);
}
}
// Load related data
const loadPromises = [];
if (this.transaction?.phase) {
loadPromises.push(
this.$getdata('transactionphase', { id: this.transaction.phase }, undefined, true)
.then(data => this.transactionphase = data)
);
}
if (this.transaction?.policy) {
loadPromises.push(
this.$getdata('salepolicy', { id: this.transaction.policy }, undefined, true)
.then(data => this.salepolicy = data)
);
}
if (this.product?.status) {
loadPromises.push(
this.$getdata('productstatus', { id: this.product.status }, undefined, true)
.then(data => this.productstatus = data)
);
}
if (this.product?.type) {
loadPromises.push(
this.$getdata('producttype', { id: this.product.type }, undefined, true)
.then(data => this.producttype = data)
);
}
if (this.product?.zone_type) {
loadPromises.push(
this.$getdata('zonetype', { id: this.product.zone_type }, undefined, true)
.then(data => this.zonetype = data)
);
}
if (this.product?.direction) {
loadPromises.push(
this.$getdata('direction', { id: this.product.direction }, undefined, true)
.then(data => this.direction = data)
);
}
if (this.product?.project) {
loadPromises.push(
this.$getdata('project', { id: this.product.project }, undefined, true)
.then(data => this.project = data)
);
}
if (this.product) {
loadPromises.push(
this.$getdata('productnote', { ref: this.product.id }).then(data => this.productnotes = data),
);
}
await Promise.all(loadPromises);
if (this.reservationId) {
await this.fetchTransactionFiles();
}
},
methods: {
async fetchTransactionFiles() {
try {
const transactionFilesData = await this.$getdata('transactionfile', {
txn_detail: this.reservationId,
});
if (transactionFilesData && transactionFilesData.length > 0) {
this.transactionFiles = transactionFilesData.map((fileData, index) => ({
id: fileData.id,
file: fileData.file,
file__file: fileData.file__file,
name: fileData.file__file || `File ${index + 1}`,
phase: fileData.phase,
create_time: fileData.create_time
})).filter(file => file.file__file);
}
} catch (error) {
console.error("Lỗi khi fetch transaction files:", error);
this.transactionFiles = [];
}
},
confirmContract() {
this.actionToConfirm = 'contract';
this.showmodal = {
component: 'dialog/Confirm',
title: 'Duyệt hợp đồng',
width: 'auto',
height: 'auto',
vbind: {
content: 'Bạn có chắc chắn muốn duyệt hợp đồng này không?'
},
}
},
async executeConfirmContract() {
if (this.$getdata('transaction',{id:this.transactionId}).status != 3) {
if (this.isApproving || this.isContractApproved) return;
const result = await this.approveTransactionDetail(this.reservationId, 'approved');
if (result && result.success) {
const updatedDetail = this.findWorkflowStepResult(result.result, 'update_detail_status_and_approver');
this.$emit('close');
}
}else {
const { $snackbar } = useNuxtApp();
$snackbar(
this.isVietnamese ? 'Giao dịch này đã được phê duyệt rồi' : 'Error supplementing transaction',
'Error',
'Danger'
);
}
},
// New: Confirm supplement (open NoteInput)
confirmSupplement() {
this.actionToConfirm = 'supplement';
this.showmodal = {
component: 'dialog/NoteInput',
title: this.isVietnamese ? 'Yêu cầu bổ sung thông tin' : 'Request Supplement Info',
width: 'auto',
height: 'auto',
vbind: {
label: this.isVietnamese ? 'Lý do yêu cầu bổ sung' : 'Reason for Supplement Request',
placeholder: this.isVietnamese ? 'Nhập lý do...' : 'Enter reason...',
type: 'warning',
confirmText: this.isVietnamese ? 'Xác nhận' : 'Confirm',
cancelText: this.isVietnamese ? 'Hủy' : 'Cancel'
},
};
},
// New: Execute supplement
async executeSupplement(note) {
if (this.isSupplementing) return;
this.isSupplementing = true;
const { id: userId } = this.store.login;
try {
// Insert note to Product_Note
await this.$insertapi('productnote', {
ref: this.product.id,
detail: note,
user: userId
});
// Patch Transaction_Detail status to 5
await this.$patchapi('reservation', {
id: this.reservation.id,
status: 5
});
// Refresh data
this.reservation = await this.$getdata('reservation', { id: this.reservation.id }, undefined, true);
this.productnotes = await this.$getdata('productnote', { ref: this.product.id });
const { $snackbar } = useNuxtApp();
$snackbar(
this.isVietnamese ? 'Yêu cầu bổ sung thành công' : 'Supplement request successful',
'Success',
'Success'
);
this.$emit('close');
} catch (error) {
console.error('Error supplementing transaction:', error);
const { $snackbar } = useNuxtApp();
$snackbar(
this.isVietnamese ? 'Lỗi khi yêu cầu bổ sung' : 'Error supplementing transaction',
'Error',
'Danger'
);
} finally {
this.isSupplementing = false;
}
},
// New: Confirm reject (open NoteInput)
confirmReject() {
this.actionToConfirm = 'reject';
this.showmodal = {
component: 'dialog/NoteInput',
title: this.isVietnamese ? 'Từ chối giao dịch' : 'Reject Transaction',
width: 'auto',
height: 'auto',
vbind: {
label: this.isVietnamese ? 'Lý do từ chối' : 'Reason for Rejection',
placeholder: this.isVietnamese ? 'Nhập lý do...' : 'Enter reason...',
type: 'danger',
confirmText: this.isVietnamese ? 'Xác nhận' : 'Confirm',
cancelText: this.isVietnamese ? 'Hủy' : 'Cancel'
},
};
},
// New: Execute reject
async executeReject(note) {
if (this.isRejecting) return;
this.isRejecting = true;
const { id: userId } = this.store.login;
const prdbkId = this.product?.prdbk; // Giả sử prdbk là ID Product_Booked
try {
// Insert note to Product_Note
await this.$insertapi('productnote', {
ref: this.product.id,
detail: note,
user: userId
});
// Patch Transaction_Detail status to 4
await this.$patchapi('reservation', {
id: this.reservation.id,
status: 4
});
// Delete Product_Booked if exists
if (prdbkId) {
await this.$deleteapi('productbooked', prdbkId);
}
// Patch Product to null transaction/policy and status 1
await this.$patchapi('product', {
id: this.product.id,
transaction: null,
policy: null,
status: 1
});
// Refresh data
this.reservation = await this.$getdata('reservation', { id: this.reservation.id }, undefined, true);
this.product = await this.$getdata('product', { id: this.product.id }, undefined, true);
this.productstatus = await this.$getdata('productstatus', { id: this.product.status }, undefined, true);
this.productnotes = await this.$getdata('productnote', { ref: this.product.id });
const { $snackbar } = useNuxtApp();
$snackbar(
this.isVietnamese ? 'Từ chối giao dịch thành công' : 'Transaction rejected successfully',
'Success',
'Success'
);
this.$emit('close');
} catch (error) {
console.error('Error rejecting transaction:', error);
const { $snackbar } = useNuxtApp();
$snackbar(
this.isVietnamese ? 'Lỗi khi từ chối giao dịch' : 'Error rejecting transaction',
'Error',
'Danger'
);
} finally {
this.isRejecting = false;
}
},
findWorkflowStepResult(steps, stepCode) {
if (!Array.isArray(steps)) return null;
const step = steps.find(s => s.step === stepCode);
return step?.results?.[0]?.result;
},
findFieldName(code) {
return this.data.find((v) => v.code === code);
},
copy(value) {
this.$copyToClipboard(value);
this.$snackbar(this.isVietnamese ? "Đã copy vào clipboard." : "Copied to clipboard", "Copy", "Success");
},
openPhone() {
this.showmodal = {
title: "Điện thoại",
height: "180px",
width: "400px",
component: "common/Phone",
vbind: { row: this.record, pagename: this.pagename },
};
},
openCustomer() {
this.showmodal = {
title: "Khách hàng",
height: "40vh",
width: "50%",
component: "customer/CustomerView",
vbind: { row: this.record, pagename: this.pagename },
};
},
openPoli() {
if (!this.transaction || !this.product || !this.salepolicy) {
const { $snackbar } = useNuxtApp();
$snackbar(
this.isVietnamese
? 'Không đủ thông tin để hiển thị lịch thanh toán'
: 'Insufficient information to display payment schedule',
'Warning',
'Warning'
);
return;
}
let plans = [];
if (this.transaction.payment_plan) {
if (typeof this.transaction.payment_plan === 'string') {
try {
plans = JSON.parse(this.transaction.payment_plan);
} catch (e) {
console.error('Could not parse payment_plan:', e);
plans = [];
}
} else {
plans = this.transaction.payment_plan;
}
}
const isPrecalculated = Array.isArray(plans) && plans.length > 0;
this.showmodal = {
title: this.isVietnamese ? "Lịch thanh toán" : "Payment Schedule",
height: "60vh",
width: "80%",
component: "application/PaymentSchedule",
vbind: {
productData: this.product,
policies: [this.salepolicy],
policyId: this.salepolicy.id,
selectedPolicy: this.salepolicy,
isVietnamese: this.isVietnamese,
originPrice: parseFloat(this.transaction.origin_price || 0),
discountValueDisplay: parseFloat(this.transaction.discount_amount || 0),
priceAfterDiscount: parseFloat(this.transaction.sale_price || 0),
selectedCustomer: this.record,
detailedDiscounts: [],
paymentPlans: isPrecalculated ? plans : [],
isPrecalculated: isPrecalculated,
baseDate: this.transaction.date || new Date(),
},
};
},
openLink() {
window.open(`https://info.utopia.com.vn/${this.product.link}`, "_blank");
},
openMoney() {
this.showmodal = {
title: "Danh sách bút toán",
height: "50vh",
width: "70%",
component: "datatable/DataView",
pagename: 'iddnwjndujwi',
vbind: {
api: 'payment_schedule',
setting: 'internal-entry-list',
params: {
sort: "-id",
filter: {
txn_detail__transaction: this.transactionId,
entry__ref: this.reservation.code
},
exclude: {
entry__code: 'null'
},
values: "entry__ref,entry__account__currency__code,entry__balance_before,entry__type__name,entry__inputer__fullname,entry__approver__fullname,entry__balance_after,entry,entry__code,entry__date,entry__amount,entry__create_time,entry__account__code,entry__account__type__name,entry__content,entry__category__name",
},
},
};
},
openUser(userId) {
if (!userId) return;
this.showmodal = {
component: "user/UserInfo",
width: "50%",
height: "200px",
title: "User",
vbind: { userId: userId },
};
},
handleModalEvent(event) {
console.log('Modal event received:', event); // Debug log để kiểm tra event
if (event.name === 'confirm') {
if (this.actionToConfirm === 'contract') {
this.executeConfirmContract();
}
if (this.actionToConfirm === 'quickAddTransaction') {
this.quickAddTransaction();
}
} else if (event.name === 'noteConfirm') {
const note = event.data.note;
if (this.actionToConfirm === 'supplement') {
this.executeSupplement(`Yêu cầu bổ sung thông tin cho giao dịch ${this.transaction.code} của sản phẩm với do: ${note}`);
} else if (this.actionToConfirm === 'reject') {
this.executeReject(`Từ chối giao dịch ${this.transaction.code} của sản phẩm với do: ${note}`);
}
} else {
this.changeInfo(event);
}
},
// New handler for noteConfirm event
handleNoteConfirmEvent(data) {
console.log('noteConfirm event received:', data); // Debug log
this.handleModalEvent({ name: 'noteConfirm', data });
},
changeInfo(v) {
this.record = this.$copy(v);
},
confirmQuickAddTransaction() {
this.actionToConfirm = 'quickAddTransaction';
this.showmodal = {
component: 'dialog/Confirm',
title: this.isVietnamese ? 'Xác nhận giữ chỗ nhanh' : 'Confirm Quick Add',
width: 'auto',
height: 'auto',
vbind: {
content: this.isVietnamese ? 'Bạn chắc chắn muốn khóa sản phẩm này?' : 'Are you sure you want to lock this product?'
},
}
},
async quickAddTransaction() {
const { id, username } = this.store.login;
// Tính thời gian khóa từ bây giờ + số phút nhập vào
const lockedUntilValue = this.$dayjs()
.add(this.lockDurationMinutes, 'minutes')
.toISOString();
const patchProductPayload = {
id: this.product.id,
status: 15, // Cố định status thành 15
locked_until: lockedUntilValue
};
const postProductNotePayload = {
ref: this.product.id,
user: id,
detail: `${username} ${this.isVietnamese ? 'khóa sản phẩm trong' : 'locked product for'} ${this.lockDurationMinutes} ${this.isVietnamese ? 'phút đến lúc' : 'minutes until'} ${this.$dayjs(lockedUntilValue).format('HH:mm:ss - DD/MM/YYYY')}`
};
try {
await this.$patchapi('product', patchProductPayload);
await this.$insertapi('productnote', postProductNotePayload);
// Refresh data
const prodId = this.productId || this.transaction?.product;
if (prodId) {
this.product = await this.$getdata('product', { id: prodId }, undefined, true);
this.productstatus = await this.$getdata('productstatus', { id: this.product.status }, undefined, true);
this.productnotes = await this.$getdata('productnote', { ref: prodId });
}
const { $snackbar } = useNuxtApp();
$snackbar(
this.isVietnamese ? 'Khóa sản phẩm thành công' : 'Product locked successfully',
'Success',
'Success'
);
// Reset
this.isLockingProduct = false;
this.lockDurationMinutes = 30;
} catch (error) {
console.error('Error locking product:', error);
const { $snackbar } = useNuxtApp();
$snackbar(
this.isVietnamese ? 'Lỗi khi khóa sản phẩm' : 'Error locking product',
'Error',
'Danger'
);
}
}
},
watch: {
transactionId() {
this.paymentScheduleKey++;
},
isLockingProduct(newVal) {
if (newVal) {
this.lockDurationMinutes = 30; // Reset về 30 phút khi tích
}
}
},
beforeUnmount() {
if (this.paymentSchedulePagename) {
this.store.commit(this.paymentSchedulePagename, undefined);
}
}
};
</script>