1103 lines
40 KiB
Vue
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 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 - 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> |