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

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>