upload tài liệu
This commit is contained in:
330
public/js/app.js
330
public/js/app.js
@@ -65,6 +65,7 @@ class AccountManager {
|
||||
this.users = [];
|
||||
this.assets = [];
|
||||
this.consumables = [];
|
||||
this.documents = [];
|
||||
this.roles = [];
|
||||
this.accountPage = 1;
|
||||
this.accountPageSize = 9;
|
||||
@@ -98,6 +99,8 @@ class AccountManager {
|
||||
this.consumableExportRecipientFilter = '';
|
||||
this.consumableExportProjectFilter = '';
|
||||
this.consumableExportDateFilter = '';
|
||||
this.documentSearchTerm = '';
|
||||
this.documentMaxFileSizeMb = 25;
|
||||
this.assetBorrows = [];
|
||||
this.assetBorrowSearchTerm = '';
|
||||
this.assetBorrowTypeFilter = '';
|
||||
@@ -259,6 +262,7 @@ class AccountManager {
|
||||
await this.fetchAssetBorrows();
|
||||
await this.fetchAssetDepartments();
|
||||
await this.fetchAssetProjects();
|
||||
await this.fetchDocuments();
|
||||
|
||||
if (this.canCurrentUserManageAssets()) {
|
||||
await this.fetchUsers();
|
||||
@@ -338,6 +342,9 @@ class AccountManager {
|
||||
this.setupAddButtonListeners();
|
||||
this.setupFilters();
|
||||
this.setupAccountPagerListeners();
|
||||
} else if (page === 'documents') {
|
||||
mainContent.innerHTML = this.getDocumentsContent();
|
||||
this.setupDocumentListeners();
|
||||
} else if (page === 'users') {
|
||||
// Check if user is admin
|
||||
if (!this.isCurrentUserAdmin()) {
|
||||
@@ -481,6 +488,25 @@ class AccountManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchDocuments(search = '') {
|
||||
try {
|
||||
const query = String(search || '').trim();
|
||||
const url = query
|
||||
? `${this.apiBase}/documents?search=${encodeURIComponent(query)}`
|
||||
: `${this.apiBase}/documents`;
|
||||
const response = await fetch(url, { cache: 'no-store' });
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || 'Không thể tải danh sách tài liệu');
|
||||
}
|
||||
this.documents = Array.isArray(data.data) ? data.data : [];
|
||||
this.documentMaxFileSizeMb = Number(data.maxFileSizeMb) || 25;
|
||||
} catch (err) {
|
||||
console.error('Fetch documents error:', err);
|
||||
this.documents = [];
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAccountSecret(accountId) {
|
||||
const response = await fetch(`${this.apiBase}/accounts/${accountId}/secret`, { cache: 'no-store' });
|
||||
const data = await response.json();
|
||||
@@ -2737,6 +2763,310 @@ class AccountManager {
|
||||
`;
|
||||
}
|
||||
|
||||
formatFileSize(value) {
|
||||
const bytes = Number(value) || 0;
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
getFilteredDocuments() {
|
||||
const keyword = String(this.documentSearchTerm || '').trim().toLocaleLowerCase('vi');
|
||||
if (!keyword) return this.documents;
|
||||
return this.documents.filter(document => [
|
||||
document?.Title,
|
||||
document?.OriginalFileName,
|
||||
document?.UploadedByName
|
||||
].some(value => String(value || '').toLocaleLowerCase('vi').includes(keyword)));
|
||||
}
|
||||
|
||||
getDocumentsContent() {
|
||||
const documents = this.getFilteredDocuments();
|
||||
const totalSize = this.documents.reduce((sum, document) => sum + (Number(document?.FileSize) || 0), 0);
|
||||
return `
|
||||
<div class="documents-page p-4 md:p-6 w-full h-full overflow-y-auto">
|
||||
<div class="page-header flex items-end justify-between gap-4 mb-5">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 text-red-600 mb-1">
|
||||
<span class="material-symbols-outlined">picture_as_pdf</span>
|
||||
<span class="text-xs font-extrabold uppercase tracking-widest">Kho tài liệu</span>
|
||||
</div>
|
||||
<h1 class="text-2xl font-extrabold text-slate-900 dark:text-slate-50">Tài liệu PDF</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Lưu trữ, xem và tải xuống tài liệu dùng chung.</p>
|
||||
</div>
|
||||
<div class="document-header-actions flex flex-wrap items-center justify-end gap-3">
|
||||
<div class="inline-flex h-10 items-center gap-2 whitespace-nowrap px-1 text-xs text-slate-500" aria-label="${this.documents.length} tài liệu, tổng dung lượng ${this.formatFileSize(totalSize)}">
|
||||
<span class="material-symbols-outlined text-lg text-slate-400">folder</span>
|
||||
<span><strong class="font-extrabold text-slate-700">${this.documents.length}</strong> tài liệu</span>
|
||||
<span class="text-slate-300" aria-hidden="true">•</span>
|
||||
<span class="font-semibold text-slate-600">${this.formatFileSize(totalSize)}</span>
|
||||
</div>
|
||||
<button id="openDocumentUploadDialog" type="button" class="inline-flex items-center justify-center gap-2 rounded-xl bg-red-600 px-4 py-2 text-sm font-bold text-white shadow-sm transition hover:bg-red-700 active:scale-95">
|
||||
<span class="material-symbols-outlined text-base">upload_file</span>
|
||||
<span>Thêm tài liệu</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
||||
<div class="page-filters flex items-center gap-3 border-b border-slate-200 p-4">
|
||||
<div class="relative flex-1">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">search</span>
|
||||
<input id="documentSearch" type="search" value="${this.escapeHtml(this.documentSearchTerm)}" placeholder="Tìm theo tên tài liệu, tên file hoặc người tải lên..."
|
||||
class="w-full rounded-lg border-slate-200 py-2 pl-10 pr-3 text-sm focus:border-primary focus:ring-primary" />
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-slate-500 whitespace-nowrap">${documents.length} kết quả</span>
|
||||
</div>
|
||||
|
||||
${documents.length ? `
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4 p-4">
|
||||
${documents.map(document => {
|
||||
const documentId = Number(document?.DocumentId);
|
||||
const canDelete = Boolean(document?.CanDelete);
|
||||
return `
|
||||
<article class="flex min-w-0 gap-4 rounded-xl border border-slate-200 p-4 transition hover:border-red-200 hover:shadow-sm">
|
||||
<div class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-red-100 text-red-600">
|
||||
<span class="material-symbols-outlined text-3xl">picture_as_pdf</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="truncate font-extrabold text-slate-800" title="${this.escapeHtml(document?.Title || '')}">${this.escapeHtml(document?.Title || 'Tài liệu PDF')}</h2>
|
||||
<p class="mt-0.5 truncate text-xs text-slate-500" title="${this.escapeHtml(document?.OriginalFileName || '')}">${this.escapeHtml(document?.OriginalFileName || '-')}</p>
|
||||
<div class="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-slate-500">
|
||||
<span class="inline-flex items-center gap-1"><span class="material-symbols-outlined text-sm">hard_drive</span>${this.formatFileSize(document?.FileSize)}</span>
|
||||
<span class="inline-flex items-center gap-1"><span class="material-symbols-outlined text-sm">person</span>${this.escapeHtml(document?.UploadedByName || '-')}</span>
|
||||
<span class="inline-flex items-center gap-1"><span class="material-symbols-outlined text-sm">schedule</span>${this.formatDateTime(document?.UploadedDate)}</span>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<a href="${this.apiBase}/documents/${documentId}/file" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-xs font-bold text-white hover:bg-primary-dim">
|
||||
<span class="material-symbols-outlined text-base">visibility</span>Xem PDF
|
||||
</a>
|
||||
<a href="${this.apiBase}/documents/${documentId}/file?download=1" class="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-xs font-bold text-slate-700 hover:bg-slate-50">
|
||||
<span class="material-symbols-outlined text-base">download</span>Tải xuống
|
||||
</a>
|
||||
${canDelete ? `
|
||||
<button type="button" class="delete-document ml-auto inline-flex items-center gap-1 rounded-lg px-2 py-1.5 text-xs font-bold text-red-600 hover:bg-red-50" data-document-id="${documentId}" data-document-title="${this.escapeHtml(document?.Title || document?.OriginalFileName || 'Tài liệu')}">
|
||||
<span class="material-symbols-outlined text-base">delete</span>Xóa
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
` : `
|
||||
<div class="flex flex-col items-center justify-center px-6 py-16 text-center">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-slate-100 text-slate-400">
|
||||
<span class="material-symbols-outlined text-4xl">folder_open</span>
|
||||
</div>
|
||||
<h2 class="mt-4 font-extrabold text-slate-700">${this.documentSearchTerm ? 'Không tìm thấy tài liệu' : 'Chưa có tài liệu PDF'}</h2>
|
||||
<p class="mt-1 max-w-md text-sm text-slate-500">${this.documentSearchTerm ? 'Hãy thử một từ khóa khác.' : 'Bấm “Thêm tài liệu” để lưu tài liệu đầu tiên.'}</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="documentUploadDialog" aria-labelledby="documentUploadDialogTitle" class="m-auto w-[min(92vw,34rem)] max-w-none overflow-hidden rounded-2xl border-0 bg-white p-0 text-slate-800 shadow-2xl backdrop:bg-slate-950/50 backdrop:backdrop-blur-sm">
|
||||
<form id="documentUploadForm">
|
||||
<div class="flex items-start justify-between gap-4 border-b border-slate-200 bg-red-50 px-5 py-4">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-red-100 text-red-600">
|
||||
<span class="material-symbols-outlined">upload_file</span>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h2 id="documentUploadDialogTitle" class="text-lg font-extrabold text-slate-900">Thêm tài liệu PDF</h2>
|
||||
<p class="mt-0.5 text-xs text-slate-500">Tải tài liệu lên kho dùng chung.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" data-close-document-dialog class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-slate-500 transition hover:bg-white hover:text-slate-800 disabled:cursor-not-allowed disabled:opacity-50" aria-label="Đóng hộp thoại">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 px-5 py-5">
|
||||
<div>
|
||||
<label for="documentFile" class="mb-1.5 block text-sm font-bold text-slate-700">Chọn file PDF <span class="text-red-600">*</span></label>
|
||||
<input id="documentFile" name="file" type="file" accept="application/pdf,.pdf" required
|
||||
class="block h-11 w-full rounded-lg border border-slate-200 bg-white p-0 pr-3 text-sm file:mr-4 file:h-full file:border-0 file:bg-slate-100 file:px-4 file:font-bold file:text-slate-700 hover:file:bg-slate-200" />
|
||||
<p class="mt-1.5 text-xs text-slate-500">Chỉ nhận định dạng PDF, tối đa ${this.documentMaxFileSizeMb} MB.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="documentTitle" class="mb-1.5 block text-sm font-bold text-slate-700">Tên hiển thị <span class="font-normal text-slate-400">(không bắt buộc)</span></label>
|
||||
<input id="documentTitle" name="title" maxlength="255" placeholder="Mặc định lấy theo tên file"
|
||||
class="h-11 w-full rounded-lg border-slate-200 bg-white text-sm focus:border-red-400 focus:ring-red-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col-reverse gap-2 border-t border-slate-200 bg-slate-50 px-5 py-4 sm:flex-row sm:justify-end">
|
||||
<button type="button" data-close-document-dialog class="inline-flex h-10 items-center justify-center rounded-lg border border-slate-300 bg-white px-4 text-sm font-bold text-slate-700 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50">Hủy</button>
|
||||
<button id="documentUploadSubmit" type="submit" class="inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-red-600 px-5 text-sm font-bold text-white shadow-sm transition hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60">
|
||||
<span class="material-symbols-outlined text-base">upload_file</span>
|
||||
<span>Tải lên</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
setupDocumentListeners() {
|
||||
const dialog = document.getElementById('documentUploadDialog');
|
||||
const form = document.getElementById('documentUploadForm');
|
||||
const openDialogButton = document.getElementById('openDocumentUploadDialog');
|
||||
|
||||
const closeDialog = () => {
|
||||
if (!dialog || form?.dataset.uploading === 'true') return;
|
||||
if (typeof dialog.close === 'function' && dialog.open) {
|
||||
dialog.close();
|
||||
} else {
|
||||
dialog.removeAttribute('open');
|
||||
}
|
||||
};
|
||||
|
||||
if (openDialogButton && dialog && !openDialogButton.dataset.boundClick) {
|
||||
openDialogButton.addEventListener('click', () => {
|
||||
if (typeof dialog.showModal === 'function') {
|
||||
if (!dialog.open) dialog.showModal();
|
||||
} else {
|
||||
dialog.setAttribute('open', '');
|
||||
}
|
||||
requestAnimationFrame(() => document.getElementById('documentFile')?.focus());
|
||||
});
|
||||
openDialogButton.dataset.boundClick = 'true';
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-close-document-dialog]').forEach(button => {
|
||||
if (button.dataset.boundClick) return;
|
||||
button.addEventListener('click', closeDialog);
|
||||
button.dataset.boundClick = 'true';
|
||||
});
|
||||
|
||||
if (dialog && !dialog.dataset.boundDialog) {
|
||||
dialog.addEventListener('cancel', event => {
|
||||
if (form?.dataset.uploading === 'true') event.preventDefault();
|
||||
});
|
||||
dialog.addEventListener('close', () => form?.reset());
|
||||
dialog.addEventListener('click', event => {
|
||||
if (event.target !== dialog || form?.dataset.uploading === 'true') return;
|
||||
const bounds = dialog.getBoundingClientRect();
|
||||
const outside = event.clientX < bounds.left
|
||||
|| event.clientX > bounds.right
|
||||
|| event.clientY < bounds.top
|
||||
|| event.clientY > bounds.bottom;
|
||||
if (outside) closeDialog();
|
||||
});
|
||||
dialog.dataset.boundDialog = 'true';
|
||||
}
|
||||
|
||||
if (form && !form.dataset.boundSubmit) {
|
||||
form.addEventListener('submit', event => this.handleDocumentUpload(event));
|
||||
form.dataset.boundSubmit = 'true';
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('documentSearch');
|
||||
if (searchInput && !searchInput.dataset.boundInput) {
|
||||
searchInput.addEventListener('input', event => {
|
||||
const cursor = event.target.selectionStart ?? event.target.value.length;
|
||||
this.documentSearchTerm = event.target.value;
|
||||
this.renderView('documents');
|
||||
const nextInput = document.getElementById('documentSearch');
|
||||
nextInput?.focus();
|
||||
nextInput?.setSelectionRange(cursor, cursor);
|
||||
});
|
||||
searchInput.dataset.boundInput = 'true';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.delete-document').forEach(button => {
|
||||
if (button.dataset.boundClick) return;
|
||||
button.addEventListener('click', () => {
|
||||
this.deleteDocument(button.dataset.documentId, button.dataset.documentTitle);
|
||||
});
|
||||
button.dataset.boundClick = 'true';
|
||||
});
|
||||
}
|
||||
|
||||
async handleDocumentUpload(event) {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const fileInput = form.querySelector('#documentFile');
|
||||
const titleInput = form.querySelector('#documentTitle');
|
||||
const submitButton = form.querySelector('#documentUploadSubmit');
|
||||
const file = fileInput?.files?.[0];
|
||||
if (!file) {
|
||||
this.notifyWarning('Vui lòng chọn file PDF');
|
||||
return;
|
||||
}
|
||||
|
||||
const extensionIsPdf = String(file.name || '').toLowerCase().endsWith('.pdf');
|
||||
if (!extensionIsPdf || (file.type && file.type !== 'application/pdf')) {
|
||||
this.notifyFailure('Chỉ chấp nhận file PDF');
|
||||
return;
|
||||
}
|
||||
if (file.size > this.documentMaxFileSizeMb * 1024 * 1024) {
|
||||
this.notifyFailure(`File vượt quá ${this.documentMaxFileSizeMb} MB`);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = new FormData();
|
||||
payload.append('file', file);
|
||||
payload.append('title', String(titleInput?.value || '').trim());
|
||||
form.dataset.uploading = 'true';
|
||||
form.querySelectorAll('[data-close-document-dialog]').forEach(button => {
|
||||
button.disabled = true;
|
||||
});
|
||||
if (submitButton) {
|
||||
submitButton.disabled = true;
|
||||
submitButton.querySelector('span:last-child').textContent = 'Đang tải...';
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/documents`, {
|
||||
method: 'POST',
|
||||
body: payload
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || 'Không thể tải tài liệu lên');
|
||||
}
|
||||
|
||||
this.documentSearchTerm = '';
|
||||
await this.fetchDocuments();
|
||||
this.renderView('documents');
|
||||
this.notifySuccess(data.message || 'Đã lưu tài liệu PDF');
|
||||
} catch (err) {
|
||||
console.error('Upload document error:', err);
|
||||
this.notifyFailure(err.message || 'Không thể tải tài liệu lên');
|
||||
delete form.dataset.uploading;
|
||||
form.querySelectorAll('[data-close-document-dialog]').forEach(button => {
|
||||
button.disabled = false;
|
||||
});
|
||||
if (submitButton) {
|
||||
submitButton.disabled = false;
|
||||
submitButton.querySelector('span:last-child').textContent = 'Tải lên';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDocument(documentId, title) {
|
||||
const parsedId = Number.parseInt(documentId, 10);
|
||||
if (!Number.isInteger(parsedId) || parsedId <= 0) return;
|
||||
if (!window.confirm(`Xóa tài liệu “${String(title || 'Tài liệu')}”? Thao tác này không thể hoàn tác.`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/documents/${parsedId}`, { method: 'DELETE' });
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || 'Không thể xóa tài liệu');
|
||||
}
|
||||
await this.fetchDocuments();
|
||||
this.renderView('documents');
|
||||
this.notifySuccess(data.message || 'Đã xóa tài liệu');
|
||||
} catch (err) {
|
||||
console.error('Delete document error:', err);
|
||||
this.notifyFailure(err.message || 'Không thể xóa tài liệu');
|
||||
}
|
||||
}
|
||||
|
||||
getApplicationsContent() {
|
||||
const filteredApps = this.getFilteredApplications();
|
||||
const pageInfo = this.getPaged(filteredApps, this.appPage, this.appPageSize);
|
||||
|
||||
Reference in New Issue
Block a user