1768 lines
59 KiB
Vue
1768 lines
59 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 (m²)</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 (m²)</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 (m²)</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 lô đất (m²)</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 đã có 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 lý 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>Mô 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">Mô 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"> </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 có 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>Mô 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">Mô 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"> </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 có 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 có 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 có 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>
|