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

1055 lines
30 KiB
Vue

<script setup>
import { ref, computed, onMounted, watch } from "vue";
import { useNuxtApp } from "#app";
import { useStore } from "@/stores/index";
const props = defineProps({
row: Object,
});
const {
$buildFileUrl,
$id,
$formatFileSize,
$getdata,
$getpath,
$getapi,
$findapi,
$snackbar,
$exportpdf,
} = useNuxtApp();
const store = useStore();
const product = props.row || {};
const tab = ref("info");
const documentSubTab = ref("product");
const imageSubTab = ref("product");
const record = ref(null);
const docid = $id();
const docid1 = $id();
const showmodal = ref();
const lang = computed(() => store.lang);
const isVietnamese = computed(() => lang.value === "vi");
const imageFiles = ref([]);
const projectImages = ref([]);
const documents = ref([]);
const projectDocuments = ref([]);
const viewingImage = ref(null);
const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"];
const documentExtensions = [
"pdf",
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"txt",
"csv",
"zip",
"rar",
];
const policies = ref([]);
const selectedPolicy = ref(null);
const allPaymentPlans = ref([]);
const paymentPlans = ref([]);
const activeTab = ref(null);
const isLoadingPolicies = ref(false);
const associatedTransaction = ref(null); // New ref for associated transaction
const originPrice = computed(() => Number(record.value?.origin_price || 0));
const remainingPrice = computed(() => {
if (!selectedPolicy.value) return originPrice.value;
return originPrice.value - Number(selectedPolicy.value.deposit || 0);
});
async function loadAllPolicyData() {
if (!record.value || !record.value.id) {
policies.value = [];
selectedPolicy.value = null;
paymentPlans.value = [];
return;
}
isLoadingPolicies.value = true;
try {
const policiesData = await $getdata("salepolicy", {}, undefined, false);
const plansData = await $getdata("paymentplan", {}, undefined, false);
policies.value = policiesData || [];
allPaymentPlans.value = plansData
? plansData.sort((a, b) => a.cycle - b.cycle)
: [];
if (policies.value.length > 0) {
loadPlansForPolicy(policies.value[0].id);
} else {
selectedPolicy.value = null;
activeTab.value = null;
paymentPlans.value = [];
}
} catch (error) {
console.error("Error loading policy data:", error);
} finally {
isLoadingPolicies.value = false;
}
}
function loadPlansForPolicy(id) {
const policy = policies.value.find((pol) => pol.id === id);
if (!policy) return;
selectedPolicy.value = policy;
paymentPlans.value = allPaymentPlans.value.filter(
(plan) => plan.policy === id
);
activeTab.value = id;
}
function printContent() {
if (!record.value || !selectedPolicy.value) {
$snackbar(
isVietnamese.value
? "Vui lòng chọn chính sách"
: "Please select a policy",
{ type: "is-warning" }
);
return;
}
const docId = 'print-area';
const fileName = `${selectedPolicy.value?.name || "Payment Schedule"} - ${
record.value?.code
}`;
if (typeof $exportpdf === "function") {
$exportpdf(docId, fileName);
$snackbar(isVietnamese.value ? "Đang xuất PDF..." : "Exporting PDF...", {
type: "is-info",
});
} else {
console.warn("$exportpdf function is not available in nuxtApp.");
}
}
function getFileExtension(fileName) {
if (!fileName || typeof fileName !== "string") return "";
const parts = fileName.split(".");
return parts.length > 1 ? parts.pop().toLowerCase() : "";
}
function changeTab(tabCode) {
if (tab.value === tabCode) return;
tab.value = tabCode;
if (tabCode === "document") {
documentSubTab.value = "product";
}
if (tabCode === "image") {
imageSubTab.value = "product";
}
}
function openEditModal() {
if (!record.value?.id) return;
showmodal.value = {
component: "product/ProductEdit",
title: "Chỉnh sửa sản phẩm",
width: "60%",
height: "50%",
vbind: {
row: { ...record.value },
},
};
}
function viewImage(image) {
viewingImage.value = image;
}
function closeViewImage() {
viewingImage.value = null;
}
async function loadProductFiles() {
if (!record.value?.id) return;
try {
const links = await $getdata("productfile", { product: record.value.id });
if (!links || links.length === 0) {
imageFiles.value = [];
documents.value = [];
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 {
id: link.id,
name: fileData?.name || fileData?.file || `Tệp ${link.file}`,
file: fileData?.file,
size: fileData?.size ? $formatFileSize(Number(fileData.size)) : "",
uploaded_by: fileData?.creator__fullname || "Admin",
doc_type: fileData?.doc_type || fileData?.doc_type__code,
caption: fileData?.caption || "",
create_time: link.create_time,
};
})
);
const imageList = [];
const docList = [];
files.forEach((file) => {
const extension = getFileExtension(file.file);
const fileEntry = {
id: file.id,
name: file.name,
caption: file.caption || "",
url: $buildFileUrl(file.file),
file: file.file,
size: file.size,
uploaded_by: file.uploaded_by,
uploaded_at: file.create_time || "",
};
if (imageExtensions.includes(extension)) {
imageList.push(fileEntry);
} else if (documentExtensions.includes(extension) || extension) {
docList.push(fileEntry);
} else {
docList.push(fileEntry);
}
});
imageFiles.value = imageList;
documents.value = docList;
} catch (err) {
console.error("Error loading product files:", err);
}
}
async function loadProjectFiles() {
if (!record.value?.project) {
projectImages.value = [];
projectDocuments.value = [];
return;
}
try {
const links = await $getdata("projectfile", {
project: record.value.project,
});
if (!links || links.length === 0) {
projectImages.value = [];
projectDocuments.value = [];
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,
};
})
);
const validFiles = files.filter((entry) => entry.fileData);
const allowedDocExtensions = ["pdf", "doc", "docx"];
const imageList = [];
const docList = [];
validFiles.forEach((entry) => {
const ext = getFileExtension(entry.fileData?.file);
const fileEntry = {
id: entry.link.id,
name: entry.fileData.name || entry.fileData.file || "File",
caption: entry.fileData.caption || "",
url: $buildFileUrl(entry.fileData.file),
file: entry.fileData.file,
size: entry.fileData.size
? $formatFileSize(Number(entry.fileData.size))
: "",
uploaded_by: entry.fileData.creator__fullname || "Admin",
uploaded_at: entry.fileData.create_time || entry.link.create_time || "",
};
if (imageExtensions.includes(ext)) {
imageList.push(fileEntry);
} else if (allowedDocExtensions.includes(ext)) {
docList.push(fileEntry);
}
});
projectImages.value = imageList;
projectDocuments.value = docList;
} catch (err) {
console.error("Error loading project files:", err);
projectImages.value = [];
projectDocuments.value = [];
}
}
async function handleModalEvent(event) {
if (event.name === "dataevent" && event.data) {
const updatedData = event.data;
if (updatedData.id && updatedData.id === record.value?.id) {
record.value = { ...record.value, ...updatedData };
await loadFullData();
}
}
}
function findFieldName(code) {
const data = store.common;
const field = data.find((v) => v.code === code);
return field || {};
}
async function loadLookupNames() {
if (!record.value) return;
try {
const basePath = $getpath();
const promises = [];
if (record.value.status && !record.value.status__name) {
promises.push(
$getdata("productstatus", { id: record.value.status }, undefined, true)
.then((data) => {
if (data && data.name) {
record.value.status__name = data.name;
}
})
);
}
if (record.value.type && !record.value.type__name) {
promises.push(
$getdata("producttype", { id: record.value.type }, undefined, true)
.then((data) => {
if (data && data.name) {
record.value.type__name = data.name;
record.value.type__en = data.name_en || data.en || data.name;
}
})
);
}
if (record.value.direction && !record.value.direction__name) {
promises.push(
$getdata("direction", { id: record.value.direction }, undefined, true)
.then((data) => {
if (data && data.name) {
record.value.direction__name = data.name;
record.value.direction__en = data.name_en || data.en || data.name;
}
})
);
}
if (record.value.zone_type && !record.value.zone_type__name) {
promises.push(
$getdata("zonetype", { id: record.value.zone_type }, undefined, true)
.then((data) => {
if (data && data.name) {
record.value.zone_type__name = data.name;
record.value.zone_type__en = data.name_en || data.en || data.name;
}
})
);
}
if (record.value.project && !record.value.project__name) {
promises.push(
(async () => {
try {
const found = $findapi("project");
const projectApi = {
name: "project",
url: `${found.url_detail}${record.value.project}/`,
params: found.params || {},
};
const rs = await $getapi([projectApi]);
const data = rs[0]?.data;
if (data) {
const projectName =
data.name || data.fullname || data.code || data.title;
if (projectName) {
record.value.project__name = projectName;
record.value.project__fullname =
data.fullname || data.name || projectName;
}
}
} catch (err) {
console.error(
`Failed to load project for id ${record.value.project}:`,
err
);
}
})()
);
}
await Promise.all(promises);
} catch (error) {
console.error("Error loading lookup names:", error);
}
}
async function loadFullData() {
if (!props.row?.id) return;
try {
const fullData = await $getdata(
"product",
{ id: props.row.id },
undefined,
true
);
record.value = fullData;
await loadLookupNames();
await loadProductFiles();
await loadProjectFiles();
await loadAllPolicyData();
} catch (error) {
console.error("Error loading full product data:", error);
record.value = props.row ? { ...props.row } : null;
if (record.value) {
await loadLookupNames();
await loadAllPolicyData();
associatedTransaction.value = null;
}
}
}
watch(
() => props.row?.id,
async (newId) => {
if (newId) {
await loadFullData();
}
},
{ immediate: false }
);
watch(
() => record.value?.project,
async (newProjectId) => {
if (newProjectId) {
await loadProjectFiles();
} else {
projectImages.value = [];
projectDocuments.value = [];
}
},
{ immediate: false }
);
onMounted(async () => {
await loadFullData();
});
</script>
<template>
<div v-if="record" class="px-5">
<div class="tabs is-toggle is-fullwidth mb-4">
<ul>
<li :class="{ 'is-active': tab === 'info' }">
<a @click="changeTab('info')">
<span class="has-text-weight-bold">Thông tin</span>
</a>
</li>
<li :class="{ 'is-active': tab === 'image' }">
<a @click="changeTab('image')">
<span class="has-text-weight-bold">Hình ảnh</span>
</a>
</li>
<li :class="{ 'is-active': tab === 'document' }">
<a @click="changeTab('document')">
<span class="has-text-weight-bold">Tài liệu</span>
</a>
</li>
<li :class="{ 'is-active': tab === 'financial-policy' }">
<a @click="changeTab('financial-policy')">
<span class="has-text-weight-bold">Chính sách tài chính</span>
</a>
</li>
</ul>
</div>
<div class="">
<div v-if="tab === 'info' && record" :id="docid">
<TransactionView
pagename="ProductView"
:transactionId="product.txnprd"
:productId="product.id"
/>
</div>
<!-- Tab Hình ảnh -->
<div v-if="tab === 'image' && record">
<div class="columns is-gapless">
<!-- Tab con bên trái -->
<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>
<!-- Nội dung bên phải -->
<div class="column pl-4">
<!-- Hình ảnh sản phẩm -->
<div v-if="imageSubTab === 'product'">
<div
v-if="imageFiles.length > 0"
class="columns is-multiline px-2 mt-0"
>
<div
class="column is-one-quarter p-2"
v-for="image in imageFiles"
:key="image.id"
>
<div class="card image-card">
<div
class="card-image"
style="cursor: pointer"
@click="viewImage(image)"
>
<figure class="image is-4by3">
<img
:src="image.url"
:alt="image.caption || image.name"
/>
</figure>
</div>
<div class="card-content p-2">
<div class="image-meta">
<p
class="has-text-weight-semibold is-size-7 is-clipped"
>
{{ image.name }}
</p>
<p class="is-size-7 is-clipped">
{{ image.caption }}
</p>
</div>
</div>
</div>
</div>
</div>
<div v-else class="has-text-centered py-4">
<p class="">Chưa hình ảnh nào được tải lên.</p>
</div>
</div>
<!-- Hình ảnh dự án -->
<div v-if="imageSubTab === 'project'">
<div
v-if="projectImages.length > 0"
class="columns is-multiline px-2 mt-0"
>
<div
class="column is-one-quarter p-2"
v-for="image in projectImages"
:key="image.id"
>
<div class="card image-card">
<div
class="card-image"
style="cursor: pointer"
@click="viewImage(image)"
>
<figure class="image is-4by3">
<img
:src="image.url"
:alt="image.caption || image.name"
/>
</figure>
</div>
<div class="card-content p-2">
<div class="image-meta">
<p
class="has-text-weight-semibold is-size-7 is-clipped"
>
{{ image.name }}
</p>
<p class="is-size-7 is-clipped">
{{ image.caption }}
</p>
</div>
</div>
</div>
</div>
</div>
<div v-else class="has-text-centered py-4">
<p class="">Chưa hình ảnh dự án nào được tải lên.</p>
</div>
</div>
</div>
</div>
<!-- Modal xem ảnh lớn -->
<div
v-if="viewingImage"
class="modal is-active"
@click.self="closeViewImage"
>
<div class="modal-background"></div>
<div class="modal-content" style="width: 90vw; height: 90vh">
<figure
class="image"
style="
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
"
>
<img
:src="viewingImage.url"
:alt="viewingImage.caption || viewingImage.name || 'Hình ảnh'"
style="max-width: 100%; max-height: 90vh; object-fit: contain"
/>
</figure>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="closeViewImage"
></button>
</div>
</div>
<!-- Tab Tài liệu -->
<div v-if="tab === 'document' && record">
<div class="columns is-gapless">
<!-- Tab con bên trái -->
<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>
<!-- Nội dung bên phải -->
<div class="column pl-4">
<!-- Tài liệu sản phẩm -->
<div v-if="documentSubTab === 'product'">
<div class="table-container">
<table class="table is-fullwidth is-striped">
<thead>
<tr>
<th class="">File</th>
<th class=""> tả</th>
<th class="">Người tải lên</th>
<th class="">Tải xuống</th>
</tr>
</thead>
<tbody>
<tr v-for="doc in documents" :key="doc.id">
<td>
<div class="is-flex is-align-items-center">
<span class="icon is-medium has-text-info mr-2">
<SvgIcon
v-bind="{
name: 'attach-file.svg',
type: 'primary',
size: 20,
}"
></SvgIcon>
</span>
<div>
<p class="has-text-weight-semibold">
{{ doc.name }}
</p>
<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
class="button is-small is-info is-outlined"
:href="doc.url"
target="_blank"
title="Tải xuống"
>
<span class="icon is-small"
><SvgIcon
v-bind="{
name: 'download.svg',
type: 'primary',
size: 14,
}"
></SvgIcon
></span>
</a>
</div>
</td>
</tr>
<tr v-if="documents.length === 0">
<td colspan="4" class="has-text-centered py-4">
<p class="has-text-grey">
Chưa có tài liệu nào được đính kèm.
</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Tài liệu dự án -->
<div v-if="documentSubTab === 'project'">
<div class="table-container">
<table class="table is-fullwidth is-striped">
<thead>
<tr>
<th class="">File</th>
<th class="">Mô tả</th>
<th class="">Người tải lên</th>
<th class="">Chức năng</th>
</tr>
</thead>
<tbody>
<tr v-for="doc in projectDocuments" :key="doc.id">
<td>
<div class="is-flex is-align-items-center">
<span class="icon is-medium has-text-info mr-2">
<SvgIcon
v-bind="{
name: 'attach-file.svg',
type: 'primary',
size: 20,
}"
></SvgIcon>
</span>
<div>
<p class="has-text-weight-semibold">
{{ doc.name }}
</p>
<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
class="button is-small is-info is-outlined"
:href="doc.url"
target="_blank"
title="Tải xuống"
>
<span class="icon is-small"
><SvgIcon
v-bind="{
name: 'download.svg',
type: 'primary',
size: 14,
}"
></SvgIcon
></span>
</a>
</div>
</td>
</tr>
<tr v-if="projectDocuments.length === 0">
<td colspan="4" class="has-text-centered py-4">
<p class="has-text-grey">
Chưa có tài liệu dự án nào được đính kèm.
</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Tab Chính sách tài chính -->
<div v-if="tab === 'financial-policy' && record" class="py-3">
<div v-if="isLoadingPolicies" class="has-text-centered">
<p class="has-text-info is-italic">Đang tải chính sách...</p>
</div>
<PaymentSchedule v-else
:productData="record"
:policies="policies"
:activeTab="activeTab"
:selectedPolicy="selectedPolicy"
:isVietnamese="isVietnamese"
:originPrice="originPrice"
:discountValueDisplay="0"
:priceAfterDiscount="remainingPrice"
:selectedCustomer="null"
:detailedDiscounts="[]"
:paymentPlans="paymentPlans"
@policy-selected="loadPlansForPolicy"
@print="printContent"
/>
</div>
</div>
</div>
<Modal
@close="showmodal = undefined"
@modalevent="handleModalEvent"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</template>
<style scoped>
.table-container {
overflow-x: auto;
}
.table-container table {
table-layout: fixed;
}
.table th {
background-color: #f5f5f5;
font-weight: 600;
}
.table td {
vertical-align: middle;
}
.table th:nth-child(1),
.table td:nth-child(1) {
width: 30%;
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: 25%;
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;
}
.image-card {
height: 100%;
display: flex;
flex-direction: column;
}
.image-card .card-image {
flex-shrink: 0;
}
.image-card .card-content {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.image-card .image-meta {
flex-grow: 1;
}
.document-card {
border-bottom: 1px solid #e0e0e0;
padding-bottom: 0.5rem;
}
.tabs .icon:first-child {
margin-right: 0.2rem;
margin-bottom: 0.1rem;
}
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;
}
.image-card {
height: 100%;
display: flex;
flex-direction: column;
}
.image-card .card-image {
flex-shrink: 0;
}
.image-card .card-content {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.image-card .image-meta {
flex-grow: 1;
}
.document-card {
border-bottom: 1px solid #e0e0e0;
padding-bottom: 1rem;
}
</style>