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

1530 lines
51 KiB
Vue

<script setup>
import { ref, computed, watch, onMounted } from "vue";
import { useRoute } from "vue-router";
import { useNuxtApp } from "#app";
import { useStore } from "@/stores/index";
import FileUpload from "@/components/media/FileUpload.vue";
import PaymentSchedulePresentation from '@/components/application/PaymentSchedulePresentation.vue';
import ImageLayout from '@/components/media/ImageLayout.vue';
import NoteInfo from '@/components/common/NoteInfo.vue';
import { usePaymentCalculator } from '~/composables/usePaymentCalculator';
const props = defineProps({
row: Object,
view: Boolean,
});
const emit = defineEmits(["modalevent", "close"]);
const {
$buildFileUrl,
$dialog,
$exportpdf,
$formatFileSize,
$id,
$snackbar,
$getdata,
$insertapi,
$patchapi,
$deleteapi,
$copyToClipboard,
} = useNuxtApp();
const route = useRoute();
const store = useStore();
const { dealer } = store;
const lang = computed(() => store.lang);
const isVietnamese = computed(() => lang.value === "vi");
const calculator = usePaymentCalculator();
const tabs = ref([
{ id: 'info', name: 'Thông tin' },
{ id: 'image', name: 'Hình ảnh' },
{ id: 'document', name: 'Tài liệu' },
{ id: 'financial-policy', name: 'Chính sách bán hàng' }
]);
const activeTabId = ref("info");
const documentSubTab = ref("product");
const imageSubTab = ref("product");
const projectImages = ref([]);
const productDocuments = ref([]);
const projectDocuments = ref([]);
const record = ref(props.row ? { ...props.row } : null);
const docid = $id();
const docid1 = $id();
const product = props.row || {};
const statusOptions = ref([]);
const typeOptions = ref([]);
const directionOptions = ref([]);
const zoneTypeOptions = ref([]);
const projectOptions = ref([]);
const cartOptions = ref([]);
const selectNumberFields = [
"project",
"type",
"status",
"direction",
"zone_type",
];
const isSaving = ref(false);
const documentExtensions = [
"pdf",
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"txt",
"csv",
"zip",
"rar",
];
const policies = ref([]);
const transaction = ref(null);
const selectedPolicy = ref(null);
const selectedCustomer = ref(null);
const allPaymentPlans = ref([]);
const isLoadingPolicies = ref(false);
const hasTransaction = ref(false);
const editingDocument = ref(null);
const editingDocumentName = ref("");
const editingDocumentCaption = ref("");
const editingDocumentSource = ref("product");
const pendingDeleteDocument = ref(null);
const pendingDeleteProjectDocument = ref(null);
const originPrice = computed(() => Number(record.value?.origin_price || 0));
const calculatorOutputData = computed(() => ({
originPrice: calculator.originalPrice.value,
totalDiscount: calculator.totalDiscount.value,
salePrice: calculator.finalTotal.value,
allocatedPrice: calculator.finalTotal.value,
originalPaymentSchedule: calculator.originalPaymentSchedule.value,
finalPaymentSchedule: calculator.finalPaymentSchedule.value,
earlyDiscountDetails: calculator.earlyDiscountDetails.value,
totalRemaining: calculator.totalRemaining.value,
detailedDiscounts: calculator.detailedDiscounts.value,
baseDate: calculator.startDate.value
}));
watch(selectedPolicy, (policy) => {
if (!policy || hasTransaction.value) return;
const plans = allPaymentPlans.value.filter(p => p.policy === policy.id);
if (plans.length > 0) {
plans.sort((a, b) => a.cycle - b.cycle);
calculator.paymentPlan.value = plans.map(p => ({
cycle: p.cycle,
value: p.value,
type: p.type,
days: p.days || 0,
payment_note: p.payment_note,
due_note: p.due_note || ''
}));
}
calculator.contractAllocationPercentage.value = policy.contract_allocation_percentage || 100;
calculator.originPrice.value = originPrice.value;
calculator.discounts.value = [];
calculator.earlyPaymentCycles.value = 0;
});
watch(() => props.row, async (newRow) => {
await initializeRecord(newRow);
}, { immediate: true });
watch(() => record.value?.project, async (newProject, oldProject) => {
if (newProject && newProject !== oldProject) {
await Promise.all([loadProjectDocuments()]);
} else if (!newProject) {
projectImages.value = [];
projectDocuments.value = [];
}
}, { immediate: false });
onMounted(() => {
loadLookupOptions();
});
if (record.value) {
normalizeSelectFieldValues(record.value);
}
function findFieldName(code) {
const data = store.common;
const field = data.find((v) => v.code === code);
return field || {};
}
function normalizeOptions(data, labelKeys = ["name", "fullname", "code"]) {
if (!data) return [];
const rows = Array.isArray(data) ? data : data?.rows || [];
return rows
.filter((item) => item && item.id !== undefined)
.map((item) => {
const label =
labelKeys.reduce((acc, key) => acc || item?.[key], undefined) ||
item.name ||
item.fullname ||
item.code ||
item.id;
return {
value: item.id,
label,
};
});
}
function normalizeSelectFieldValues(target) {
if (!target) return;
selectNumberFields.forEach((field) => {
const value = target[field];
if (value !== undefined && value !== null && value !== "") {
const numValue = Number(value);
target[field] = Number.isNaN(numValue) ? value : numValue;
}
});
}
function getFileExtension(fileName) {
if (!fileName || typeof fileName !== "string") return "";
const parts = fileName.split(".");
return parts.length > 1 ? parts.pop().toLowerCase() : "";
}
function confirmAction(message, title) {
if (typeof window !== "undefined") {
return window.confirm(title ? `${title}\n${message}` : message);
}
return true;
}
function buildProductPayload() {
if (!record.value) return null;
const payload = {};
const fields = [
"id",
"code",
"land_lot_code",
"zone_code",
"trade_code",
"zone_type",
"lot_area",
"building_area",
"total_built_area",
"number_of_floors",
"land_lot_size",
"direction",
"template_name",
"type",
"project",
"status",
"note",
"cart",
];
fields.forEach((field) => {
const value = record.value[field];
if (value !== null && value !== undefined && value !== "") {
payload[field] = value;
}
});
return payload;
}
function resolveProductId(sourceRow) {
const candidate =
sourceRow?.id ||
record.value?.id ||
route?.params?.id ||
route?.query?.id ||
null;
if (candidate === null || candidate === undefined || candidate === "")
return null;
const parsed = Number(candidate);
return Number.isNaN(parsed) ? candidate : parsed;
}
async function fetchProductFiles() {
if (!record.value?.id) return [];
const links = await $getdata("productfile", { product: record.value.id });
if (!links || links.length === 0) return [];
const files = await Promise.all(
links.map(async (link) => {
const fileData = await $getdata(
"file",
{ id: link.file },
undefined,
true
).catch((error) => {
console.error(`Error loading file ${link.file}:`, error);
return null;
});
return {
link,
fileData,
};
})
);
return files.filter((entry) => entry.fileData);
}
async function loadProductDocuments() {
try {
const entries = await fetchProductFiles();
productDocuments.value = entries
.filter((entry) => documentExtensions.includes(getFileExtension(entry.fileData?.file)))
.map((entry) => ({
id: entry.link.id,
fileId: entry.link.file,
name: entry.fileData.name || entry.fileData.file || "Tài liệu",
caption: entry.fileData.caption || "",
size: entry.fileData.size ? $formatFileSize(entry.fileData.size) : "",
uploaded_by:
entry.fileData.creator__fullname || store.login?.fullname || "N/A",
uploaded_at: entry.fileData.create_time || entry.link.create_time || "",
file: entry.fileData.file,
url: $buildFileUrl(entry.fileData.file),
}));
} catch (error) {
console.error("Error loading product documents:", error);
productDocuments.value = [];
}
}
async function fetchProjectFiles() {
const projectId = record.value?.project;
if (!projectId) return [];
const links = await $getdata("projectfile", { project: projectId });
if (!links || links.length === 0) return [];
const files = await Promise.all(
links.map(async (link) => {
const fileData = await $getdata(
"file",
{ id: link.file },
undefined,
true
).catch((error) => {
console.error(`Error loading project file ${link.file}:`, error);
return null;
});
return {
link,
fileData,
};
})
);
return files.filter((entry) => entry.fileData);
}
async function loadProjectDocuments() {
try {
const entries = await fetchProjectFiles();
projectDocuments.value = entries
.filter((entry) => {
const ext = getFileExtension(entry.fileData?.file);
return documentExtensions.includes(ext);
})
.map((entry) => ({
id: entry.link.id,
fileId: entry.link.file,
name: entry.fileData.name || entry.fileData.file || "Tài liệu",
caption: entry.fileData.caption || "",
size: entry.fileData.size ? $formatFileSize(entry.fileData.size) : "",
uploaded_by:
entry.fileData.creator__fullname || store.login?.fullname || "Admin",
uploaded_at: entry.fileData.create_time || entry.link.create_time || "",
file: entry.fileData.file,
url: $buildFileUrl(entry.fileData.file),
}));
} catch (error) {
console.error("Error loading project documents:", error);
projectDocuments.value = [];
}
}
async function loadProductAssets() {
await Promise.all([
loadProductDocuments(),
loadProjectDocuments(),
loadAllPolicyData(),
]);
}
async function attachFilesToProduct(files) {
if (!record.value?.id) {
$dialog("Vui lòng lưu sản phẩm trước khi đính kèm tệp", "Thông báo");
return 0;
}
const payload = files
.filter((file) => file && file.id)
.map((file) => ({
product: record.value.id,
file: file.id,
}));
if (payload.length === 0) return 0;
const result = await $insertapi("productfile", payload, undefined, false);
if (result === "error") {
throw new Error("Không thể liên kết tệp với sản phẩm");
}
return payload.length;
}
async function attachFilesToProject(files) {
if (!record.value?.project) {
$dialog("Vui lòng chọn dự án trước khi đính kèm tài liệu", "Thông báo");
return 0;
}
const payload = files
.filter((file) => file && file.id)
.map((file) => ({
project: record.value.project,
file: file.id,
}));
if (payload.length === 0) return 0;
const result = await $insertapi("projectfile", payload, undefined, false);
if (result === "error") {
throw new Error("Không thể liên kết tệp với dự án");
}
return payload.length;
}
async function onUploadedDocs(payload) {
const uploadedFiles = Array.isArray(payload) ? payload : null;
if (uploadedFiles && uploadedFiles.length > 0) {
try {
const linked = await attachFilesToProduct(uploadedFiles);
if (linked > 0) {
await loadProductDocuments();
const message =
linked === 1
? "Đã thêm tài liệu thành công"
: `Đã thêm ${linked} tài liệu thành công`;
$snackbar(message, "Thành công", "Success");
}
} catch (error) {
console.error("Error attaching product documents:", error);
$snackbar("Không thể thêm tài liệu, vui lòng thử lại", "Lỗi", "Error");
}
return;
}
const event = payload?.target ? payload : null;
if (!event) return;
const files = event.target.files;
if (!files || files.length === 0) return;
const totalFiles = files.length;
Array.from(files).forEach((file) => {
productDocuments.value.push({
id: Date.now() + Math.random(),
name: file.name,
size: $formatFileSize(file.size),
uploaded_by: store.login?.fullname || "Admin",
});
});
const message =
totalFiles === 1
? "Đã thêm tài liệu thành công"
: `Đã thêm ${totalFiles} tài liệu thành công`;
$snackbar(message, "Thành công", "Success");
event.target.value = "";
}
async function onUploadedProjectDocs(payload) {
const uploadedFiles = Array.isArray(payload) ? payload : null;
if (uploadedFiles && uploadedFiles.length > 0) {
try {
const linked = await attachFilesToProject(uploadedFiles);
if (linked > 0) {
await loadProjectDocuments();
const message =
linked === 1
? "Đã thêm tài liệu dự án thành công"
: `Đã thêm ${linked} tài liệu dự án thành công`;
$snackbar(message, "Thành công", "Success");
}
} catch (error) {
console.error("Error attaching project documents:", error);
$snackbar(
"Không thể thêm tài liệu dự án, vui lòng thử lại",
"Lỗi",
"Error"
);
}
return;
}
const event = payload?.target ? payload : null;
if (!event) return;
const files = event.target.files;
if (!files || files.length === 0) return;
const totalFiles = files.length;
Array.from(files).forEach((file) => {
projectDocuments.value.push({
id: Date.now() + Math.random(),
name: file.name,
size: $formatFileSize(file.size),
uploaded_by: store.login?.fullname || "Admin",
});
});
const message =
totalFiles === 1
? "Đã thêm tài liệu dự án thành công"
: `Đã thêm ${totalFiles} tài liệu dự án thành công`;
$snackbar(message, "Thành công", "Success");
event.target.value = "";
}
function editDocument(doc, source = "product") {
editingDocumentSource.value = source;
editingDocument.value = doc;
editingDocumentName.value = doc.name;
editingDocumentCaption.value = doc.caption || "";
}
async function saveEditDocument() {
if (!editingDocument.value) return;
const targetList =
editingDocumentSource.value === "project"
? projectDocuments.value
: productDocuments.value;
const index = targetList.findIndex(
(doc) => doc.id === editingDocument.value.id
);
if (index !== -1) {
targetList[index] = {
...targetList[index],
name: editingDocumentName.value,
caption: editingDocumentCaption.value,
};
const fileId = editingDocument.value.fileId || editingDocument.value.file;
if (fileId) {
try {
await $patchapi(
"file",
{
id: fileId,
name: editingDocumentName.value,
caption: editingDocumentCaption.value?.trim() || null,
},
{},
false
);
$snackbar("Đã cập nhật tài liệu thành công", "Thành công", "Success");
} catch (error) {
console.error("Error updating document metadata:", error);
$snackbar("Cập nhật tài liệu thất bại", "Lỗi", "Error");
}
}
}
cancelEditDocument();
}
function cancelEditDocument() {
editingDocument.value = null;
editingDocumentName.value = "";
editingDocumentCaption.value = "";
editingDocumentSource.value = "product";
}
function deleteDocument(doc) {
pendingDeleteDocument.value = doc;
const confirmed = confirmAction(
`Bạn có chắc muốn xóa tài liệu "${doc.name}"?`,
"Xác nhận xóa"
);
if (!confirmed) {
pendingDeleteDocument.value = null;
return;
}
(async () => {
try {
if (doc.id) {
await $deleteapi("productfile", doc.id);
if (doc.fileId || doc.file) {
await $deleteapi("file", doc.fileId || doc.file);
}
await loadProductDocuments();
} else {
const index = productDocuments.value.findIndex((d) => d === doc);
if (index !== -1) productDocuments.value.splice(index, 1);
}
$snackbar("Đã xóa tài liệu thành công", "Thành công", "Success");
} catch (error) {
console.error("Error deleting product document:", error);
$snackbar("Xóa tài liệu không thành công", "Lỗi", "Error");
} finally {
pendingDeleteDocument.value = null;
}
})();
}
function deleteProjectDocument(doc) {
pendingDeleteProjectDocument.value = doc;
const confirmed = confirmAction(
`Bạn có chắc muốn xóa tài liệu dự án "${doc.name}"?`,
"Xác nhận xóa"
);
if (!confirmed) {
pendingDeleteProjectDocument.value = null;
return;
}
(async () => {
try {
if (doc.id) {
await $deleteapi("projectfile", doc.id);
if (doc.fileId || doc.file) {
await $deleteapi("file", doc.fileId || doc.file);
}
await loadProjectDocuments();
} else {
const index = projectDocuments.value.findIndex((d) => d === doc);
if (index !== -1) projectDocuments.value.splice(index, 1);
}
$snackbar("Đã xóa tài liệu dự án thành công", "Thành công", "Success");
} catch (error) {
console.error("Error deleting project document:", error);
$snackbar("Xóa tài liệu dự án không thành công", "Lỗi", "Error");
} finally {
pendingDeleteProjectDocument.value = null;
}
})();
}
async function loadLookupOptions() {
try {
const [statuses, types, directions, zoneTypes, projects, cart] =
await Promise.all([
$getdata("productstatus", {id__in:[1,2,3,4,6,8,15]},undefined,false ),
$getdata("producttype"),
$getdata("direction"),
$getdata("zonetype"),
$getdata("project", undefined, {}),
$getdata("cart"),
]);
statusOptions.value = normalizeOptions(statuses);
typeOptions.value = normalizeOptions(types);
directionOptions.value = normalizeOptions(directions);
zoneTypeOptions.value = normalizeOptions(zoneTypes);
cartOptions.value = normalizeOptions(cart);
projectOptions.value = normalizeOptions(projects, [
"name",
"fullname",
"code",
]);
} catch (error) {
console.error("Error loading lookup options:", error);
}
}
async function initializeRecord(newRow = props.row) {
try {
if (newRow) {
record.value = { ...newRow };
} else if (!record.value) {
const inferredId = resolveProductId();
if (inferredId) {
record.value = { id: inferredId };
}
}
const productId = resolveProductId(newRow);
if (!productId) {
if (!newRow) {
record.value = null;
productDocuments.value = [];
}
return;
}
const fullData = await $getdata(
"product",
{ id: productId },
undefined,
true
);
if (fullData) {
record.value = { ...fullData };
} else if (!record.value) {
record.value = { id: productId };
}
normalizeSelectFieldValues(record.value);
await loadProductAssets();
} catch (error) {
console.error("Error loading product detail:", error);
if (record.value) {
normalizeSelectFieldValues(record.value);
await loadProductAssets();
}
}
}
function changeTab(tabCode) {
if (activeTabId.value === tabCode) return;
if (!record.value)
return $snackbar("Vui lòng lưu dữ liệu trước", "Thông báo", "Info");
activeTabId.value = tabCode;
if (tabCode === "document") {
documentSubTab.value = "product";
}
if (tabCode === "image") {
imageSubTab.value = "product";
}
}
async function updateAll() {
if (!record.value) {
$snackbar("Vui lòng nhập thông tin sản phẩm", "Thông báo", "Info");
return;
}
try {
const payload = buildProductPayload();
if (!payload?.id) {
$snackbar("Không tìm thấy ID sản phẩm để cập nhật", "Thông báo", "Error");
return;
}
isSaving.value = true;
const updated = await $patchapi("product", payload, {}, false);
if (updated === "error") {
$snackbar(
isVietnamese.value
? "Có lỗi xảy ra khi cập nhật sản phẩm"
: "Error updating product",
"Lỗi",
"Error"
);
return;
}
if (updated && updated.id) {
const successMessage = isVietnamese.value
? "Cập nhật sản phẩm thành công!"
: "Product updated successfully!";
$snackbar(
successMessage,
isVietnamese.value ? "Thành công" : "Success",
"Success"
);
try {
const fullData = await $getdata(
"product",
{ id: updated.id },
undefined,
true
);
if (fullData) {
record.value = { ...record.value, ...fullData };
normalizeSelectFieldValues(record.value);
emit("modalevent", {
name: "dataevent",
data: fullData,
});
} else {
record.value = { ...record.value, ...updated };
normalizeSelectFieldValues(record.value);
emit("modalevent", {
name: "dataevent",
data: updated,
});
}
} catch (reloadError) {
console.error("Error reloading data:", reloadError);
record.value = { ...record.value, ...updated };
normalizeSelectFieldValues(record.value);
emit("modalevent", {
name: "dataevent",
data: updated,
});
}
} else {
$snackbar(
isVietnamese.value
? "Cập nhật không thành công. Vui lòng thử lại."
: "Update failed. Please try again.",
"Lỗi",
"Error"
);
}
} catch (error) {
console.error("Update error:", error);
$snackbar(
isVietnamese.value
? "Có lỗi xảy ra khi cập nhật sản phẩm: " + (error.message || error)
: "Error updating product: " + (error.message || error),
"Lỗi",
"Error"
);
} finally {
isSaving.value = false;
}
}
function extractEarlyCycles(paymentPlan) {
if (!paymentPlan || paymentPlan.length === 0) return 0;
const firstItem = paymentPlan[0];
if (firstItem.is_early_merged && firstItem.merged_cycles) {
return firstItem.merged_cycles.length;
}
return 0;
}
function calculatePaidAmount(paymentPlan) {
return paymentPlan.reduce((sum, item) => sum + (item.paid_amount || 0), 0);
}
async function loadAllPolicyData() {
if (!record.value || !record.value.id) {
policies.value = [];
selectedPolicy.value = null;
selectedCustomer.value = null;
hasTransaction.value = false;
return;
}
isLoadingPolicies.value = true;
try {
const [policiesData, plansData] = await Promise.all([
$getdata("salepolicy", { enable: "True" }, undefined, false),
$getdata("paymentplan", {}, undefined, false)
]);
policies.value = policiesData || [];
allPaymentPlans.value = plansData ? plansData.sort((a, b) => a.cycle - b.cycle) : [];
if (record.value.prdbk__transaction) {
hasTransaction.value = true;
const txn = await $getdata("transaction", { id: record.value.prdbk__transaction }, undefined, true);
transaction.value = txn;
const matchedPolicy = await $getdata("salepolicy", { id: txn.policy }, undefined, true);
selectedPolicy.value = matchedPolicy;
calculator.discounts.value = [{
id: 'txn-discount',
name: 'Tổng chiết khấu',
code: 'DISCOUNT',
type: 2,
value: txn.discount_amount || 0
}];
const policyPlans = allPaymentPlans.value.filter(p => p.policy === txn.policy);
policyPlans.sort((a, b) => a.cycle - b.cycle);
calculator.paymentPlan.value = policyPlans.map(p => ({
cycle: p.cycle,
value: p.value,
type: p.type,
days: p.days || 0,
payment_note: p.payment_note,
due_note: p.due_note || ''
}));
calculator.originPrice.value = txn.origin_price || 0;
calculator.contractAllocationPercentage.value = matchedPolicy.contract_allocation_percentage || 100;
calculator.startDate.value = new Date(txn.date || txn.create_time || Date.now());
calculator.earlyPaymentCycles.value = extractEarlyCycles(txn.payment_plan || []);
calculator.paidAmount.value = calculatePaidAmount(txn.payment_plan || []);
if (txn.customer) {
const customer = await $getdata("customer", { id: txn.customer }, undefined, true);
selectedCustomer.value = customer;
}
} else {
hasTransaction.value = false;
transaction.value = null;
selectedCustomer.value = null;
calculator.originPrice.value = originPrice.value;
calculator.discounts.value = [];
calculator.earlyPaymentCycles.value = 0;
calculator.paidAmount.value = 0;
calculator.startDate.value = new Date();
if (policies.value.length > 0) {
selectPolicy(policies.value[0]);
}
}
} catch (error) {
console.error("Error loading policy data:", error);
$snackbar("Không thể tải dữ liệu chính sách", "Lỗi", "Error");
} finally {
isLoadingPolicies.value = false;
}
}
function selectPolicy(policy) {
if (hasTransaction.value) {
$snackbar("Sản phẩm đã có giao dịch, không thể đổi chính sách", "Thông báo", "Info");
return;
}
selectedPolicy.value = policy;
const plans = allPaymentPlans.value.filter(p => p.policy === policy.id);
if (plans.length > 0) {
plans.sort((a, b) => a.cycle - b.cycle);
calculator.paymentPlan.value = plans.map(p => ({
cycle: p.cycle,
value: p.value,
type: p.type,
days: p.days || 0,
payment_note: p.payment_note,
due_note: p.due_note || ''
}));
}
calculator.contractAllocationPercentage.value = policy.contract_allocation_percentage || 100;
}
function printContent() {
if (!selectedPolicy.value) {
$snackbar('Vui lòng chọn chính sách', { type: 'is-warning' });
return;
}
const docId = 'print-area';
const fileName = `${selectedPolicy.value.name || 'Payment Schedule'} - ${record.value.code}`;
const printElement = document.getElementById(docId);
if (!printElement) return;
const scheduleContainers = printElement.querySelectorAll('.schedule-container');
const stickyHeaders = printElement.querySelectorAll('.table-container.schedule-container thead th');
const ignoreButtons = printElement.querySelectorAll('#ignore-print');
scheduleContainers.forEach(container => {
container.style.maxHeight = 'none';
container.style.overflow = 'visible';
});
stickyHeaders.forEach(header => {
header.style.position = 'static';
});
ignoreButtons.forEach(button => {
button.style.display = 'none';
});
$exportpdf(docId, fileName, 'a3');
setTimeout(() => {
scheduleContainers.forEach(container => {
container.style.maxHeight = '';
container.style.overflow = '';
});
stickyHeaders.forEach(header => {
header.style.position = '';
});
ignoreButtons.forEach(button => {
button.style.display = '';
});
}, 1200);
$snackbar('Đang xuất PDF...', { type: 'is-info' });
}
</script>
<template>
<div>
<div class="tabs is-toggle is-fullwidth mb-3">
<ul class="px-5">
<li v-for="tab in tabs" :key="tab.id" :class="{ 'is-active': activeTabId === tab.id }">
<a @click="changeTab(tab.id)">
<span class="has-text-weight-bold">{{ tab.name }}</span>
</a>
</li>
</ul>
</div>
<div v-if="record" class="px-5">
<div v-if="activeTabId === 'info'" :id="docid">
<TransactionView v-if="props.view" pagename="ProductView" :transactionId="record.prdbk__transaction"
:customerId="record.prdbk__transaction__customer" :productId="record.id" :reservationId="record.prdbk__transaction__resvtxn"
:quickAdd="true" />
<div v-else>
<div :id="docid1">
<div class="is-flex is-justify-content-space-between is-align-items-center mb-4 ml-2">
<Caption v-bind="{ title: findFieldName('product')[lang] }"></Caption>
</div>
<div class="columns is-multiline mx-0">
<div class="column is-one-quarter">
<div class="field">
<label class="label">Tên thương mại</label>
<div class="control">
<span class="hyperlink" @click="window.open(`https://info.utopia.com.vn/${record.link}`, '_blank')">
{{ record.trade_code }}
</span>
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Tên quy hoạch</label>
<div class="control">
<input class="input" type="text" v-model="record.zone_code" />
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Loại sản phẩm</label>
<div class="control">
<div class="select is-fullwidth">
<select v-model.number="record.type">
<option v-for="option in typeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Mẫu biệt thự</label>
<div class="control">
<input class="input" type="text" v-model="record.template_name" />
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Phân khu</label>
<div class="control">
<div class="select is-fullwidth">
<select v-model.number="record.zone_type">
<option v-for="option in zoneTypeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Hướng cửa</label>
<div class="control">
<div class="select is-fullwidth">
<select v-model.number="record.direction">
<option v-for="option in directionOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Diện tích sàn ()</label>
<div class="control">
<input class="input" type="number" v-model.number="record.lot_area" />
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Diện tích xây dựng ()</label>
<div class="control">
<input class="input" type="number" v-model.number="record.building_area" />
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Tổng diện tích xây dựng ()</label>
<div class="control">
<input class="input" type="number" v-model.number="record.total_built_area" />
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Số tầng</label>
<div class="control">
<input class="input" type="number" v-model.number="record.number_of_floors" />
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Diện tích đất ()</label>
<div class="control">
<input class="input" type="text" v-model="record.land_lot_size" />
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Dự án</label>
<div class="control">
<div class="select is-fullwidth">
<select v-model.number="record.project">
<option v-for="option in projectOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Trạng thái</label>
<div v-if="!record.prdbk" class="control">
<div class="select is-fullwidth">
<select v-model.number="record.status">
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
<div v-else class="">Sản phẩm đã giao dịch không thể đổi trạng thái</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Giá gốc</label>
<div class="control">
<input class="input" type="text" v-model="record.origin_price" />
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Giỏ hàng</label>
<div class="control">
<div class="select is-fullwidth">
<select v-model.number="record.cart">
<option v-for="option in cartOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="column is-one-quarter">
<div class="field">
<label class="label">Đại bán hàng</label>
<div class="control">
<input disabled class="input" type="text" v-model="record.cart__dealer__name" />
</div>
</div>
</div>
<div class="column is-full">
<div class="field">
<label class="label">Ghi chú</label>
<div class="control">
<textarea class="textarea textarea-form" v-model="record.note"></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4 has-text-right px-3">
<button class="button is is-primary has-text-white" :class="{ 'is-loading': isSaving }" @click="updateAll">
<span>Cập nhật</span>
</button>
</div>
</div>
</div>
<div v-if="activeTabId === 'image'">
<div class="columns is-gapless">
<div class="column is-2">
<div class="vertical-tabs">
<ul>
<li :class="{ 'is-active': imageSubTab === 'product' }" @click="imageSubTab = 'product'">
<a><span class="has-text-weight-bold">Hình ảnh sản phẩm</span></a>
</li>
<li :class="{ 'is-active': imageSubTab === 'project' }" @click="imageSubTab = 'project'">
<a><span class="has-text-weight-bold">Hình ảnh dự án</span></a>
</li>
</ul>
</div>
</div>
<div class="column pl-4">
<ImageLayout
:projectId="record.project"
:productId="imageSubTab === 'product' ? record.id : undefined"
/>
</div>
</div>
</div>
<div v-if="activeTabId === 'document'">
<div class="columns is-gapless">
<div class="column is-2">
<div class="vertical-tabs">
<ul>
<li :class="{ 'is-active': documentSubTab === 'product' }" @click="documentSubTab = 'product'">
<a><span class="has-text-weight-bold">Tài liệu sản phẩm</span></a>
</li>
<li :class="{ 'is-active': documentSubTab === 'project' }" @click="documentSubTab = 'project'">
<a><span class="has-text-weight-bold">Tài liệu dự án</span></a>
</li>
</ul>
</div>
</div>
<div class="column pl-4">
<div v-if="documentSubTab === 'product'">
<div v-if="!dealer && $getEditRights('edit', { code: 'product', category: 'topmenu' })" class="mb-3">
<FileUpload position="right" :type="['pdf', 'file']" @files="onUploadedDocs" />
</div>
<div class="table-container">
<table class="table is-fullwidth">
<thead>
<tr>
<th>File</th>
<th> tả</th>
<th>Người tải lên</th>
<th>Chức năng</th>
</tr>
</thead>
<tbody>
<template v-for="doc in productDocuments" :key="doc.id">
<tr v-if="editingDocument?.id === doc.id && editingDocumentSource === 'product'">
<td colspan="4">
<div class="columns is-multiline">
<div class="column is-4">
<div class="field">
<label class="label">Tên tài liệu</label>
<div class="control">
<input class="input" type="text" v-model="editingDocumentName" />
</div>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label"> tả</label>
<div class="control">
<input class="input" type="text" v-model="editingDocumentCaption" />
</div>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">&nbsp;</label>
<div class="control">
<div class="buttons">
<button class="button has-text-white is-primary" @click="saveEditDocument">Lưu</button>
<button class="button is-danger has-text-white" @click="cancelEditDocument">Hủy</button>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
<tr v-else>
<td>
<div class="is-flex is-align-items-center">
<div style="word-wrap: break-word;">
<a :href="doc.url" target="_blank" class="has-text-weight-semibold">{{ doc.name }}</a>
<p class="is-size-7 has-text-grey">{{ doc.size }}</p>
</div>
</div>
</td>
<td><span class="is-size-7">{{ doc.caption || "-" }}</span></td>
<td>
<div>
<p class="is-size-7 has-text-weight-semibold">{{ doc.uploaded_by }}</p>
<p class="is-size-7 has-text-grey">
{{ doc.uploaded_at ? new Date(doc.uploaded_at).toLocaleString("vi-VN") : "-" }}
</p>
</div>
</td>
<td>
<div class="buttons is-justify-content-center">
<a v-if="!dealer && $getEditRights('edit', { code: 'product', category: 'topmenu' })" @click="editDocument(doc, 'product')" title="Sửa">
<span class="icon"><SvgIcon v-bind="{ name: 'edit.svg', type: 'primary', size: 18 }"></SvgIcon></span>
</a>
<a :href="doc.url" target="_blank" title="Tải xuống">
<span class="icon"><SvgIcon v-bind="{ name: 'download.svg', type: 'success', size: 18 }"></SvgIcon></span>
</a>
<a v-if="!dealer && $getEditRights('edit', { code: 'product', category: 'topmenu' })" @click="deleteDocument(doc)" title="Xóa">
<span class="icon"><SvgIcon v-bind="{ name: 'bin1.svg', type: 'danger', size: 18 }"></SvgIcon></span>
</a>
</div>
</td>
</tr>
</template>
<tr v-if="productDocuments.length === 0">
<td colspan="4" class="has-text-centered py-4">
<p class="has-text-grey">Chưa tài liệu nào được đính kèm.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="documentSubTab === 'project'">
<div v-if="!dealer && $getEditRights('edit', { code: 'product', category: 'topmenu' })" class="mb-3">
<FileUpload position="right" :type="['pdf', 'file']" @files="onUploadedProjectDocs" />
</div>
<div class="table-container">
<table class="table is-fullwidth">
<thead>
<tr>
<th>File</th>
<th> tả</th>
<th>Người tải lên</th>
<th>Chức năng</th>
</tr>
</thead>
<tbody>
<template v-for="doc in projectDocuments" :key="doc.id">
<tr v-if="editingDocument?.id === doc.id && editingDocumentSource === 'project'">
<td colspan="4">
<div class="columns is-multiline">
<div class="column is-4">
<div class="field">
<label class="label">Tên tài liệu</label>
<div class="control">
<input class="input" type="text" v-model="editingDocumentName" />
</div>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label"> tả</label>
<div class="control">
<input class="input" type="text" v-model="editingDocumentCaption" />
</div>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">&nbsp;</label>
<div class="control">
<div class="buttons">
<a class="button has-text-white is-primary" @click="saveEditDocument">Lưu</a>
<a class="button is-danger has-text-white" @click="cancelEditDocument">Hủy</a>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
<tr v-else>
<td>
<div style="word-wrap: break-word;">
<a :href="doc.url" target="_blank" class="has-text-weight-semibold">{{ doc.name }}</a>
<p class="is-size-7 has-text-grey">{{ doc.size }}</p>
</div>
</td>
<td><span class="is-size-7">{{ doc.caption || "-" }}</span></td>
<td>
<div>
<p class="is-size-7 has-text-weight-semibold">{{ doc.uploaded_by }}</p>
<p class="is-size-7 has-text-grey">
{{ doc.uploaded_at ? new Date(doc.uploaded_at).toLocaleString("vi-VN") : "-" }}
</p>
</div>
</td>
<td>
<div class="buttons is-justify-content-center">
<a @click="$copyToClipboard(doc.url)" title="Sao chép link">
<span class="icon"><SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 18 }"></SvgIcon></span>
</a>
<a v-if="!dealer && $getEditRights('edit', { code: 'product', category: 'topmenu' })" @click="editDocument(doc, 'project')" title="Sửa">
<span class="icon"><SvgIcon v-bind="{ name: 'edit.svg', type: 'primary', size: 18 }"></SvgIcon></span>
</a>
<a :href="doc.url" target="_blank" title="Tải xuống">
<span class="icon"><SvgIcon v-bind="{ name: 'download.svg', type: 'success', size: 18 }"></SvgIcon></span>
</a>
<a v-if="!dealer && $getEditRights('edit', { code: 'product', category: 'topmenu' })" @click="deleteProjectDocument(doc)" title="Xóa">
<span class="icon"><SvgIcon v-bind="{ name: 'bin1.svg', type: 'primary', size: 18 }"></SvgIcon></span>
</a>
</div>
</td>
</tr>
</template>
<tr v-if="projectDocuments.length === 0">
<td colspan="4" class="has-text-centered py-4">
<p class="has-text-grey">Chưa tài liệu dự án nào được đính kèm.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div v-if="activeTabId === 'financial-policy'">
<div v-if="isLoadingPolicies" class="has-text-centered py-6">
<p class="has-text-info is-italic">Đang tải chính sách...</p>
</div>
<div v-else>
<div class="notification is-info is-light mb-4">
<p class="has-text-weight-semibold">
<span v-if="!hasTransaction">
Sản phẩm chưa giao dịch - Đang hiển thị lịch thanh toán mẫu
</span>
</p>
</div>
<div v-if="!hasTransaction && policies.length > 1" class="tabs is-boxed mb-4">
<ul>
<li v-for="pol in policies" :key="pol.id" :class="{ 'is-active': selectedPolicy?.id === pol.id }">
<a @click="selectPolicy(pol)">
<span :class="selectedPolicy?.id === pol.id ? 'has-text-weight-bold' : ''">
{{ pol.name }}
</span>
</a>
</li>
</ul>
</div>
<PaymentSchedulePresentation
v-if="selectedPolicy"
:productData="record"
:selectedPolicy="selectedPolicy"
:selectedCustomer="selectedCustomer"
:calculatorData="calculatorOutputData"
:isLoading="false"
@print="printContent"
/>
<div v-else class="has-text-centered py-6">
<p class="has-text-grey">Không chính sách nào được chọn</p>
</div>
</div>
</div>
<NoteInfo v-if="activeTabId === 'notes'" v-bind="{ row, api: 'productnote', pagename: 'pagedataProductList' }" />
</div>
</div>
</template>
<style scoped>
img {
object-fit: cover;
}
li.is-active a,
li a:hover {
color: white !important;
background-color: #204853 !important;
transition: all 0.3s ease;
}
li.is-active a .icon :deep(img),
li a:hover .icon :deep(img) {
filter: brightness(0) invert(1) !important;
}
.document-card {
border-bottom: 1px solid #e0e0e0;
padding-bottom: 0.5rem;
}
.table-container {
overflow-x: auto;
}
.table-container table {
table-layout: fixed;
}
.table th {
background-color: #f5f5f5;
}
.table td {
vertical-align: middle;
}
.table th:nth-child(1),
.table td:nth-child(1) {
width: 35%;
min-width: 200px;
}
.table th:nth-child(2),
.table td:nth-child(2) {
width: 25%;
min-width: 150px;
}
.table th:nth-child(3),
.table td:nth-child(3) {
width: 20%;
min-width: 180px;
}
.table th:nth-child(4),
.table td:nth-child(4) {
width: 20%;
min-width: 150px;
text-align: center;
}
.table td:nth-child(4) .buttons {
justify-content: center;
display: flex;
}
.vertical-tabs {
border-right: 1px solid #dbdbdb;
padding-right: 5px;
margin-right: 1rem;
min-height: 200px;
}
.vertical-tabs ul {
list-style: none;
margin: 0;
padding: 0;
}
.vertical-tabs li {
margin-bottom: 0.5rem;
cursor: pointer;
}
.vertical-tabs li a {
display: block;
padding: 0.75rem 1rem;
border-radius: 4px;
transition: all 0.3s ease;
color: #4a4a4a;
border-left: 3px solid transparent;
}
.vertical-tabs li a:hover {
background-color: #f5f5f5;
color: #204853;
}
.vertical-tabs li.is-active a {
background-color: #e8f4fd;
color: #204853;
border-left-color: #204853;
font-weight: 600;
}
.textarea-form {
padding: 0.5rem 0.5rem !important;
}
</style>