458 lines
12 KiB
Vue
458 lines
12 KiB
Vue
<template>
|
|
<div>
|
|
<div class="columns is-multiline mx-1">
|
|
<div :class="`column px-0 is-full ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
|
<div class="field">
|
|
<label class="label">Sản phẩm</label>
|
|
<div class="control">
|
|
<SearchBox
|
|
v-bind="{
|
|
api: 'product',
|
|
field: 'trade_code',
|
|
column: ['trade_code'],
|
|
optionid: props.row.trade_code,
|
|
first: true,
|
|
disabled: true,
|
|
viewaddon: productViewAddon,
|
|
}"
|
|
@option="selected('product', $event)"
|
|
/>
|
|
</div>
|
|
<p class="help is-danger" v-if="false">error</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="`column px-0 is-full ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
|
<div class="field">
|
|
<label class="label">
|
|
Giao dịch<b class="ml-1 has-text-danger">*</b>
|
|
</label>
|
|
<div class="control">
|
|
<SearchBox
|
|
v-bind="{
|
|
api: 'transactionphase',
|
|
field: 'name',
|
|
column: ['name'],
|
|
optionid: transaction?.phase,
|
|
first: true,
|
|
viewaddon: transactionTypeViewAddon,
|
|
}"
|
|
@option="selected('phase', $event)"
|
|
/>
|
|
</div>
|
|
<p class="help is-danger" v-if="errors.phase">
|
|
{{ errors.phase }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="`column px-0 is-full ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
|
<div class="field">
|
|
<label class="label">
|
|
Khách hàng<b class="ml-1 has-text-danger">*</b>
|
|
</label>
|
|
<div class="control">
|
|
<SearchBox
|
|
v-bind="{
|
|
api: 'customer',
|
|
field: 'label',
|
|
column: ['fullname', 'phone'],
|
|
first: true,
|
|
optionid: transaction?.customer,
|
|
addon: customerAddon,
|
|
viewaddon: customerViewAddon,
|
|
}"
|
|
@option="selected('customer', $event)"
|
|
/>
|
|
</div>
|
|
<p class="help is-danger" v-if="errors.customer">
|
|
{{ errors.customer }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="`column px-0 is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
|
<div class="field">
|
|
<label class="label">
|
|
Số tiền đặt cọc<b class="ml-1 has-text-danger">*</b>
|
|
</label>
|
|
<div class="control">
|
|
<InputNumber
|
|
v-bind="{
|
|
record: formData,
|
|
attr: 'amount',
|
|
defaultValue: true,
|
|
}"
|
|
@number="selected('amount', $event)"
|
|
></InputNumber>
|
|
</div>
|
|
<p class="help is-danger" v-if="errors.amount">
|
|
{{ errors.amount }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="`column is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
|
<div class="field">
|
|
<label class="label"
|
|
>Hạn thanh toán<b class="ml-1 has-text-danger">*</b></label
|
|
>
|
|
<div class="control">
|
|
<Datepicker
|
|
v-bind="{ record: formData, attr: 'due_date' }"
|
|
@date="selected('due_date', $event)"
|
|
/>
|
|
</div>
|
|
<p class="help is-danger" v-if="errors.due_date">
|
|
{{ errors.due_date }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="`column px-0 is-full ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
|
<div class="field">
|
|
<label class="label">{{
|
|
dataLang && findFieldName("note")[lang]
|
|
}}</label>
|
|
<div class="control">
|
|
<textarea
|
|
v-model="formData.note"
|
|
class="textarea"
|
|
name="note"
|
|
placeholder=""
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
<p class="help is-danger" v-if="errors.note">{{ errors.note }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="`column is-full px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
|
<button
|
|
:class="`button is-primary has-text-white ${
|
|
isSubmitting ? 'is-loading' : ''
|
|
}`"
|
|
@click="handleSubmitData"
|
|
:disabled="isSubmitting"
|
|
>
|
|
{{ isVietnamese ? "Tạo giao dịch" : "Create Transaction" }}
|
|
</button>
|
|
|
|
<!-- Nút xem hợp đồng - chỉ hiện khi có contract -->
|
|
<button
|
|
v-if="contractData"
|
|
class="button is-info ml-3"
|
|
@click="openContractModal"
|
|
>
|
|
<span>{{ isVietnamese ? "Xem hợp đồng" : "View Contract" }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal hiển thị hợp đồng -->
|
|
<Modal
|
|
v-if="showContractModal"
|
|
@close="showContractModal = false"
|
|
@dataevent="handleContractUpdated"
|
|
v-bind="contractModalConfig"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from "vue";
|
|
import { isEqual, pick } from "es-toolkit";
|
|
import dayjs from "dayjs";
|
|
import { useStore } from "~/stores/index";
|
|
|
|
const props = defineProps({
|
|
row: Object,
|
|
api: String,
|
|
});
|
|
|
|
const {
|
|
$updatepage,
|
|
$getdata,
|
|
$insertapi,
|
|
$updateapi,
|
|
$empty,
|
|
$snackbar,
|
|
$generateDocument,
|
|
} = useNuxtApp();
|
|
const transaction = await $getdata(
|
|
"transaction",
|
|
{ product: props.row.id },
|
|
undefined,
|
|
true
|
|
);
|
|
const reservation = transaction
|
|
? await $getdata(
|
|
"reservation",
|
|
{ transaction: transaction.id },
|
|
undefined,
|
|
true
|
|
)
|
|
: undefined;
|
|
|
|
const viewport = 5;
|
|
const store = useStore();
|
|
const lang = computed(() => store.lang);
|
|
const isVietnamese = computed(() => lang.value === "vi");
|
|
const dataLang = ref(store.common);
|
|
const isSubmitting = ref(false);
|
|
const contractData = ref(null);
|
|
const showContractModal = ref(false);
|
|
|
|
// Load contract nếu đã có
|
|
if (transaction) {
|
|
contractData.value = await $getdata(
|
|
"contract",
|
|
{ transaction: transaction.id },
|
|
undefined,
|
|
true
|
|
);
|
|
}
|
|
|
|
const initFormData = {
|
|
product: props.row.id,
|
|
phase: transaction?.phase,
|
|
customer: transaction?.customer,
|
|
amount: reservation?.amount,
|
|
due_date: reservation?.due_date,
|
|
note: props.row.note,
|
|
};
|
|
|
|
const formData = ref({ ...initFormData });
|
|
watch(formData, (val) => console.log(toRaw(val)), { deep: true });
|
|
|
|
const errors = ref({});
|
|
|
|
const productViewAddon = {
|
|
component: "product/ProductView",
|
|
width: "55%",
|
|
height: "600px",
|
|
title: lang === "en" ? "Product" : "Sản phẩm",
|
|
};
|
|
|
|
const transactionTypeViewAddon = {
|
|
component: "transaction/TransactionTypeView",
|
|
width: "40%",
|
|
height: "100px",
|
|
title: lang === "en" ? "Transaction Type" : "Loại giao dịch",
|
|
};
|
|
|
|
const customerViewAddon = {
|
|
component: "customer/CustomerView",
|
|
width: "75%",
|
|
height: "600px",
|
|
title: lang === "en" ? "Customer" : "Khách hàng",
|
|
};
|
|
|
|
const customerAddon = {
|
|
component: "customer/Customer",
|
|
width: "75%",
|
|
height: "400px",
|
|
title: isVietnamese.value ? "Tạo khách hàng" : "Add customer",
|
|
};
|
|
|
|
// Config cho modal hợp đồng
|
|
const contractModalConfig = computed(() => ({
|
|
component: "application/Contract",
|
|
title: isVietnamese.value ? "Hợp đồng" : "Contract",
|
|
width: "90%",
|
|
height: "90vh",
|
|
vbind: {
|
|
row: {
|
|
id: contractData.value?.transaction,
|
|
...contractData.value,
|
|
},
|
|
api: "transaction",
|
|
},
|
|
event: "contractUpdated",
|
|
eventname: "dataevent",
|
|
}));
|
|
|
|
const findFieldName = (code) => {
|
|
let field = dataLang.value.find((v) => v.code === code);
|
|
return field;
|
|
};
|
|
|
|
const selected = (fieldName, value) => {
|
|
const finalValue =
|
|
value !== null && typeof value === "object"
|
|
? value.id || value.index || value.code || value.label
|
|
: value;
|
|
formData.value[fieldName] = finalValue;
|
|
};
|
|
|
|
const checkErrors = (fields) => {
|
|
const { phase, customer, amount, due_date } = fields;
|
|
errors.value = {};
|
|
|
|
if ($empty(phase)) {
|
|
errors.value.phase = isVietnamese.value
|
|
? "Giai đoạn giao dịch không được để trống"
|
|
: "Transaction phase is required";
|
|
}
|
|
|
|
if ($empty(customer)) {
|
|
errors.value.customer = isVietnamese.value
|
|
? "Khách hàng không được để trống"
|
|
: "Customer is required";
|
|
}
|
|
|
|
if ($empty(amount)) {
|
|
errors.value.amount = isVietnamese.value
|
|
? "Số tiền đặt cọc không được để trống"
|
|
: "Deposit amount is required";
|
|
}
|
|
|
|
if ($empty(due_date)) {
|
|
errors.value.due_date = isVietnamese.value
|
|
? "Hạn thanh toán không được để trống"
|
|
: "Due date is required";
|
|
}
|
|
|
|
return Object.keys(errors.value).length > 0;
|
|
};
|
|
|
|
async function handleSubmitData() {
|
|
try {
|
|
if (isSubmitting.value) return;
|
|
|
|
if (isEqual(formData.value, initFormData)) {
|
|
$snackbar(
|
|
isVietnamese.value ? "Form không thay đổi" : "Form is unchanged"
|
|
);
|
|
return;
|
|
}
|
|
|
|
const hasValidationErrors = checkErrors(formData.value);
|
|
if (hasValidationErrors) {
|
|
$snackbar(
|
|
isVietnamese.value
|
|
? "Vui lòng kiểm tra lại dữ liệu."
|
|
: "Please check the data again."
|
|
);
|
|
return;
|
|
}
|
|
|
|
isSubmitting.value = true;
|
|
|
|
// 1. Tạo transaction
|
|
const transactionPayload = {
|
|
...pick(formData.value, ["product", "phase", "customer"]),
|
|
date: dayjs().format("YYYY-MM-DD"),
|
|
};
|
|
|
|
const transactionRs = await $insertapi(
|
|
"transaction",
|
|
transactionPayload,
|
|
undefined,
|
|
false
|
|
);
|
|
const { date, id: transactionId, code: transactionCode } = transactionRs;
|
|
|
|
// 2. Tạo reservation
|
|
const reservationPayload = {
|
|
amount: Number(formData.value.amount),
|
|
due_date: formData.value.due_date,
|
|
creator: 1,
|
|
date:dayjs().format("YYYY-MM-DD"),
|
|
transaction: transactionId,
|
|
};
|
|
|
|
const reservationnRs = await $insertapi(
|
|
"reservation",
|
|
reservationPayload,
|
|
undefined,
|
|
false
|
|
);
|
|
const reservationId = reservationnRs.id;
|
|
|
|
// 3. Generate document
|
|
const documents = [];
|
|
|
|
try {
|
|
const docResult = await $generateDocument({
|
|
doc_code: "PXLTT1",
|
|
customer_id: formData.value.customer,
|
|
investor_id: 1,
|
|
product_id: formData.value.product,
|
|
reservation: reservationId,
|
|
output_filename: `hop_dong_${transactionCode}`,
|
|
});
|
|
|
|
if (docResult.success && docResult.data) {
|
|
documents.push({
|
|
code: docResult.data.code,
|
|
name: docResult.data.name,
|
|
en: docResult.data.name,
|
|
file: docResult.data.file,
|
|
pdf: docResult.data.pdf,
|
|
});
|
|
} else {
|
|
console.error("Generate document failed:", docResult.error);
|
|
}
|
|
} catch (docError) {
|
|
console.error("Lỗi khi tạo tài liệu:", docError);
|
|
}
|
|
|
|
// 4. Tạo contract record
|
|
if (documents.length > 0) {
|
|
const contractPayload = {
|
|
transaction: transactionId,
|
|
document: documents,
|
|
link: crypto.randomUUID(),
|
|
signature: null,
|
|
status: 1,
|
|
user: null,
|
|
};
|
|
|
|
const contractResult = await $insertapi(
|
|
"contract",
|
|
contractPayload,
|
|
undefined,
|
|
false
|
|
);
|
|
contractData.value = contractResult;
|
|
}
|
|
|
|
$snackbar(
|
|
isVietnamese.value
|
|
? "Tạo giao dịch và hợp đồng thành công!"
|
|
: "Transaction and contract created successfully!"
|
|
);
|
|
|
|
// Tự động mở modal xem hợp đồng
|
|
if (contractData.value) {
|
|
showContractModal.value = true;
|
|
}
|
|
} catch (error) {
|
|
console.error("Create transaction failed:", error);
|
|
$snackbar(
|
|
isVietnamese.value
|
|
? "Tạo giao dịch thất bại"
|
|
: "Create transaction failed"
|
|
);
|
|
} finally {
|
|
isSubmitting.value = false;
|
|
}
|
|
}
|
|
|
|
function openContractModal() {
|
|
showContractModal.value = true;
|
|
}
|
|
|
|
function handleContractUpdated(eventData) {
|
|
if (eventData?.data) {
|
|
contractData.value = { ...contractData.value, ...eventData.data };
|
|
$snackbar(
|
|
isVietnamese.value
|
|
? "Hợp đồng đã được cập nhật"
|
|
: "Contract has been updated"
|
|
);
|
|
}
|
|
}
|
|
</script>
|