Files
web/app/components/transaction/TransactionView.vue
2026-05-07 16:15:33 +07:00

1254 lines
42 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) }}</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) }}</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) }}</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("L") : "/" }}
</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("L 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 lý 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 lý 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 có 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 - L")}`,
};
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>