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

423 lines
15 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>