557 lines
17 KiB
Vue
557 lines
17 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Loading Overlay -->
|
|
<div
|
|
v-if="isLoading"
|
|
class="loading-overlay"
|
|
>
|
|
<div class="loader"></div>
|
|
</div>
|
|
|
|
<!-- View Mode Toggle -->
|
|
<div
|
|
class="mb-5 pb-3"
|
|
style="border-bottom: 2px solid #e8e8e8"
|
|
>
|
|
<div class="buttons has-addons">
|
|
<button
|
|
@click="viewMode = 'list'"
|
|
:class="['button', viewMode === 'list' ? 'is-primary' : 'is-light']"
|
|
>
|
|
Danh sách
|
|
</button>
|
|
<button
|
|
@click="viewMode = 'gallery'"
|
|
:class="['button', viewMode === 'gallery' ? 'is-primary' : 'is-light']"
|
|
>
|
|
Thư viện
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Phase Document Types List -->
|
|
<div v-if="phasedoctypes && phasedoctypes.length > 0">
|
|
<div
|
|
v-for="doctype in phasedoctypes"
|
|
:key="doctype.id"
|
|
class="mb-6"
|
|
>
|
|
<!-- Document Type Header with Upload Button -->
|
|
<div class="level is-mobile mb-4">
|
|
<div class="level-left">
|
|
<div class="level-item">
|
|
<p class="is-size-6 has-text-weight-semibold has-text-primary">
|
|
{{ doctype.doctype__name }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="level-right">
|
|
<div class="level-item">
|
|
<FileUpload
|
|
v-if="$getEditRights()"
|
|
:type="['file', 'image', 'pdf']"
|
|
@files="(files) => handleUpload(files, doctype.doctype)"
|
|
position="right"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- List View -->
|
|
<div v-if="viewMode === 'list'">
|
|
<div v-if="getFilesByDocType(doctype.doctype).length > 0">
|
|
<div
|
|
v-for="file in getFilesByDocType(doctype.doctype)"
|
|
:key="file.id"
|
|
class="is-flex is-justify-content-space-between is-align-items-center py-3 px-4 has-background-warning has-text-white"
|
|
style="border-bottom: #e8e8e8 solid 1px; transition: all 0.2s ease; opacity: 0.95; cursor: pointer"
|
|
@mouseenter="$event.currentTarget.style.opacity = '1'"
|
|
@mouseleave="$event.currentTarget.style.opacity = '0.95'"
|
|
>
|
|
<div style="flex: 1; min-width: 0">
|
|
<p
|
|
class="is-size-7 has-text-weight-semibold has-text-white mb-1"
|
|
style="word-break: break-word"
|
|
>
|
|
{{ file.name || file.file__name }}
|
|
</p>
|
|
<p class="is-size-7 has-text-white-bis">
|
|
{{ $formatFileSize(file.file__size) }} •
|
|
{{ $dayjs(file.create_time).format("DD/MM/YYYY HH:mm") }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="buttons are-small ml-3">
|
|
<button
|
|
@click="viewFile(file)"
|
|
class="button has-background-white has-text-primary"
|
|
>
|
|
<span class="icon">
|
|
<SvgIcon
|
|
v-bind="{
|
|
name: 'view.svg',
|
|
type: 'success',
|
|
size: 18,
|
|
}"
|
|
></SvgIcon>
|
|
</span>
|
|
</button>
|
|
<button
|
|
@click="downloadFile(file)"
|
|
class="button has-background-white has-text-primary"
|
|
>
|
|
<span class="icon">
|
|
<SvgIcon
|
|
v-bind="{
|
|
name: 'download.svg',
|
|
type: 'success',
|
|
size: 18,
|
|
}"
|
|
></SvgIcon>
|
|
</span>
|
|
</button>
|
|
<button
|
|
@click="deleteFile(file.id)"
|
|
class="button has-background-white has-text-danger"
|
|
>
|
|
<span class="icon">
|
|
<SvgIcon
|
|
v-bind="{
|
|
name: 'bin.svg',
|
|
type: 'danger',
|
|
size: 18,
|
|
}"
|
|
></SvgIcon>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="has-text-grey-light is-size-7 has-text-centered py-5"
|
|
>
|
|
Chưa có file nào
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gallery View -->
|
|
<div v-if="viewMode === 'gallery'">
|
|
<div
|
|
v-if="getFilesByDocType(doctype.doctype).length > 0"
|
|
class="columns is-multiline is-variable is-2"
|
|
>
|
|
<div
|
|
v-for="file in getFilesByDocType(doctype.doctype)"
|
|
:key="file.id"
|
|
class="column is-half-tablet is-one-third-desktop"
|
|
>
|
|
<div
|
|
class="has-background-warning has-text-white"
|
|
style="
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 2px 8px rgba(50, 115, 220, 0.2);
|
|
"
|
|
@mouseenter="
|
|
$event.currentTarget.style.transform = 'translateY(-4px)';
|
|
$event.currentTarget.style.boxShadow = '0 6px 16px rgba(50, 115, 220, 0.3)';
|
|
"
|
|
@mouseleave="
|
|
$event.currentTarget.style.transform = 'translateY(0)';
|
|
$event.currentTarget.style.boxShadow = '0 2px 8px rgba(50, 115, 220, 0.2)';
|
|
"
|
|
>
|
|
<div
|
|
style="
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 16px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
min-height: 140px;
|
|
"
|
|
>
|
|
<div
|
|
v-if="isImage(file.file__name)"
|
|
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center"
|
|
>
|
|
<img
|
|
:src="`${$getpath()}static/files/${file.file__file}`"
|
|
:alt="file.file__name"
|
|
style="max-width: 100%; max-height: 100%; object-fit: contain"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="has-text-white-ter"
|
|
style="font-size: 48px; line-height: 1"
|
|
>
|
|
FILE
|
|
</div>
|
|
</div>
|
|
|
|
<div style="padding: 12px 16px">
|
|
<p
|
|
class="is-size-7 has-text-weight-semibold has-text-white mb-1"
|
|
:title="file.file__name"
|
|
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
|
|
>
|
|
{{ file.file__name }}
|
|
</p>
|
|
<p class="is-size-7 has-text-white-bis mb-3">
|
|
{{ $formatFileSize(file.file__size) }}
|
|
</p>
|
|
|
|
<div class="buttons are-small is-centered">
|
|
<button
|
|
@click="viewFile(file)"
|
|
class="button has-background-white has-text-primary"
|
|
>
|
|
<span class="icon">
|
|
<SvgIcon
|
|
v-bind="{
|
|
name: 'view.svg',
|
|
type: 'success',
|
|
size: 18,
|
|
}"
|
|
></SvgIcon>
|
|
</span>
|
|
</button>
|
|
<button
|
|
@click="downloadFile(file)"
|
|
class="button has-background-white has-text-primary"
|
|
>
|
|
<span class="icon">
|
|
<SvgIcon
|
|
v-bind="{
|
|
name: 'download.svg',
|
|
type: 'success',
|
|
size: 18,
|
|
}"
|
|
></SvgIcon>
|
|
</span>
|
|
</button>
|
|
<button
|
|
@click="deleteFile(file.id)"
|
|
class="button has-background-white has-text-danger"
|
|
>
|
|
<span class="icon">
|
|
<SvgIcon
|
|
v-bind="{
|
|
name: 'bin.svg',
|
|
type: 'danger',
|
|
size: 18,
|
|
}"
|
|
></SvgIcon>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="has-text-grey-light is-size-7 has-text-centered py-5"
|
|
>
|
|
Chưa có file nào
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- If no phase doctypes -->
|
|
<div
|
|
v-else-if="!isLoading"
|
|
class="has-text-centered py-6"
|
|
>
|
|
<p class="has-text-grey-light is-size-7">Chưa có loại tài liệu được định nghĩa cho giai đoạn này.</p>
|
|
</div>
|
|
|
|
<Modal
|
|
@close="showmodal = undefined"
|
|
@modalevent="handleModalEvent"
|
|
v-bind="showmodal"
|
|
v-if="showmodal"
|
|
></Modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: "ContractPaymentUpload",
|
|
setup() {
|
|
const { $formatFileSize, $dayjs, $getpath } = useNuxtApp();
|
|
return { $formatFileSize, $dayjs, $getpath };
|
|
},
|
|
props: {
|
|
row: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
transaction: null,
|
|
files: [],
|
|
isLoading: false,
|
|
showmodal: undefined,
|
|
phasedoctypes: [],
|
|
viewMode: "list",
|
|
};
|
|
},
|
|
async created() {
|
|
const { $getdata } = useNuxtApp();
|
|
this.isLoading = true;
|
|
try {
|
|
const transactionId = this.row.id;
|
|
this.transaction = await $getdata("transaction", { id: transactionId }, undefined, true);
|
|
|
|
if (this.transaction?.phase) {
|
|
await this.fetchPhaseDoctypes();
|
|
}
|
|
|
|
await this.fetchFiles();
|
|
} catch (error) {
|
|
console.error("Error during component creation:", error);
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
},
|
|
methods: {
|
|
async fetchPhaseDoctypes() {
|
|
const { $getdata } = useNuxtApp();
|
|
if (!this.transaction?.phase) return;
|
|
|
|
try {
|
|
const phasedoctypesData = await $getdata(
|
|
"phasedoctype",
|
|
{
|
|
phase: this.transaction.phase,
|
|
},
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
if (phasedoctypesData) {
|
|
this.phasedoctypes = Array.isArray(phasedoctypesData) ? phasedoctypesData : [phasedoctypesData];
|
|
} else {
|
|
this.phasedoctypes = [];
|
|
}
|
|
} catch (error) {
|
|
console.error("Lỗi khi tải phase doctypes:", error);
|
|
this.phasedoctypes = [];
|
|
}
|
|
},
|
|
async fetchFiles() {
|
|
const { $getdata } = useNuxtApp();
|
|
if (!this.row.id) return;
|
|
this.isLoading = true;
|
|
try {
|
|
const detail = await $getdata(
|
|
"reservation",
|
|
{
|
|
id: this.transaction.txncurrent__detail,
|
|
},
|
|
undefined,
|
|
true,
|
|
);
|
|
const filesArray = await $getdata(
|
|
"transactionfile",
|
|
{
|
|
txn_detail: detail.id,
|
|
},
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
if (filesArray) {
|
|
this.files = (Array.isArray(filesArray) ? filesArray : [filesArray]).sort(
|
|
(a, b) => new Date(b.create_time) - new Date(a.create_time),
|
|
);
|
|
} else {
|
|
this.files = [];
|
|
}
|
|
} catch (error) {
|
|
console.error("Lỗi khi tải danh sách file:", error);
|
|
this.files = [];
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
},
|
|
getFilesByDocType(docTypeId) {
|
|
return this.files.filter(
|
|
(file) => file.file__doc_type === docTypeId || (file.file__doc_type == null && docTypeId == null),
|
|
);
|
|
},
|
|
getFileExtension(fileName) {
|
|
return fileName ? fileName.split(".").pop().toLowerCase() : "file";
|
|
},
|
|
isImage(fileName) {
|
|
const ext = this.getFileExtension(fileName);
|
|
return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext);
|
|
},
|
|
isViewableDocument(fileName) {
|
|
const ext = this.getFileExtension(fileName);
|
|
return ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(ext);
|
|
},
|
|
async handleUpload(uploadedFiles, docTypeId) {
|
|
if (!uploadedFiles || uploadedFiles.length === 0) return;
|
|
|
|
this.isLoading = true;
|
|
const { $patchapi, $getdata, $insertapi } = useNuxtApp();
|
|
|
|
try {
|
|
for (const fileRecord of uploadedFiles) {
|
|
if (docTypeId) {
|
|
await $patchapi("file", {
|
|
id: fileRecord.id,
|
|
doc_type: docTypeId,
|
|
});
|
|
}
|
|
|
|
const detail = await $getdata(
|
|
"reservation",
|
|
{
|
|
id: this.transaction.txncurrent__detail,
|
|
},
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
const payload = {
|
|
txn_detail: detail.id,
|
|
file: fileRecord.id,
|
|
phase: this.transaction?.phase,
|
|
};
|
|
|
|
const result = await $insertapi("transactionfile", payload);
|
|
if (result && result.error) {
|
|
throw new Error(result.error || "Lưu file không thành công.");
|
|
}
|
|
}
|
|
|
|
await this.fetchFiles();
|
|
this.$emit("upload-completed");
|
|
} catch (error) {
|
|
console.error("Lỗi khi lưu file:", error);
|
|
alert("Đã xảy ra lỗi khi tải file lên. Vui lòng thử lại.");
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
},
|
|
downloadFile(file) {
|
|
const { $getpath } = useNuxtApp();
|
|
const filePath = file.file__file || file.file;
|
|
if (!filePath) return;
|
|
|
|
const link = document.createElement("a");
|
|
link.href = `${$getpath()}static/files/${filePath}`;
|
|
link.download = file.file__name || "download";
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
},
|
|
deleteFile(fileId) {
|
|
this.showmodal = {
|
|
component: "dialog/Confirm",
|
|
title: "Xác nhận xóa",
|
|
height: "10vh",
|
|
width: "40%",
|
|
vbind: {
|
|
content: "Bạn có chắc chắn muốn xóa file này không?",
|
|
},
|
|
onConfirm: async () => {
|
|
this.isLoading = true;
|
|
const { $deleteapi } = useNuxtApp();
|
|
try {
|
|
const result = await $deleteapi("transactionfile", fileId);
|
|
if (result && !result.error) {
|
|
await this.fetchFiles();
|
|
} else {
|
|
throw new Error(result.error || "Xóa file không thành công.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Lỗi khi xóa file:", error);
|
|
alert("Đã xảy ra lỗi khi xóa file. Vui lòng thử lại.");
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
},
|
|
};
|
|
},
|
|
handleModalEvent(event) {
|
|
if (event.name === "confirm" && typeof this.showmodal?.onConfirm === "function") {
|
|
this.showmodal.onConfirm();
|
|
}
|
|
},
|
|
viewFile(file) {
|
|
const { $getpath } = useNuxtApp();
|
|
const fileName = file.file__name || "";
|
|
const filePath = file.file__file || file.file;
|
|
if (!filePath) return;
|
|
|
|
const isImageFile = this.isImage(fileName);
|
|
const isViewable = this.isViewableDocument(fileName);
|
|
const fileUrl = `${$getpath()}static/files/${filePath}`;
|
|
|
|
if (isImageFile) {
|
|
this.showmodal = {
|
|
title: fileName,
|
|
component: "media/ChipImage",
|
|
vbind: {
|
|
extend: false,
|
|
file: file,
|
|
image: fileUrl,
|
|
show: ["download", "delete"],
|
|
},
|
|
};
|
|
} else if (isViewable) {
|
|
// Mở Google Viewer trực tiếp trong tab mới
|
|
const viewerUrl = `https://docs.google.com/gview?url=${fileUrl}&embedded=false`;
|
|
window.open(viewerUrl, "_blank");
|
|
} else {
|
|
this.downloadFile(file);
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.loader {
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid #3273dc;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
.loading-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background-color: rgba(255, 255, 255, 0.7);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 100;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|