1530 lines
51 KiB
Vue
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 (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> |