Cảnh báo thời hạn hiệu lực hồ sơ
Đang lọc danh sách các hồ sơ sắp đến hạn (trong 30 ngày) hoặc đã quá hạn.
Tổng tất cả các hồ sơ
Hiển thị 0 hồ sơ • Quyền truy cập: Toàn quyền (Admin)
Đang lọc danh sách các hồ sơ sắp đến hạn (trong 30 ngày) hoặc đã quá hạn.
Hiển thị 0 hồ sơ • Quyền truy cập: Toàn quyền (Admin)
Quản lý linh hoạt danh mục hồ sơ (icon, màu sắc), phòng ban, dự án, nhà cung cấp, thư mục Google Drive và phân quyền từng User.
Đường dẫn thư mục Google Drive đã được khóa bảo vệ trong mã nguồn để tránh khách xem demo thay đổi nhầm.
Tùy chỉnh biểu tượng, màu sắc và xem số lượng văn bản trong từng danh mục.
Thêm không giới hạn tài khoản nhân viên/phòng ban, tích chọn từng danh mục được phép truy cập và quyền thao tác (Xem, Thêm, Sửa, Xóa).
Giao diện này đã được tích hợp sẵn cầu nối google.script.run tự động. Bạn có thể tải về dùng ngay trên máy hoặc gắn vào Google Sheets + Google Drive chỉ với 2 tệp (Code.gs và Index.html).
Mở file Google Sheets mới → Tiện ích mở rộng → Apps Script. Xóa mã cũ trong Code.gs, dán mã bên dưới vào, chọn hàm setupDatabase trên thanh công cụ và nhấn Chạy (Run).
Trong Apps Script, nhấn dấu + (Thêm tệp) → HTML, đặt tên chính xác là Index (tạo thành Index.html). Nhấn nút Sao chép mã Index.html ở góc trên và dán đè vào.
Nhấn Triển khai → Tùy chọn triển khai mới → Ứng dụng web. Chọn thực thi dưới tên Tôi (Me) và quyền truy cập Bất kỳ ai (Anyone) để lấy đường dẫn Webapp sử dụng.
Dữ liệu & Danh mục mặc định
Sửa danh sách Phòng ban, Dự án, Nhà cung cấp, Tài khoản Admin/User mặc định.
Cột hiển thị bảng Hồ Sơ
Thêm/bớt cột hiển thị trên bảng chính hoặc đổi định dạng tiền tệ, nhãn trạng thái.
Công thức cảnh báo hết hạn
Đang đặt cảnh báo vàng khi còn <= 30 ngày. Bạn có thể đổi thành 15 hoặc 60 ngày.
Xử lý lưu tệp Google Drive
Nằm trong Code.gs, tự động giải mã Base64, tạo file vào Drive và trả về link Preview.
/**
* WEBAPP QUẢN LÝ TÀI LIỆU, VĂN BẢN (v1.0) - GOOGLE SHEETS & DRIVE
* Tương thích 100% với tệp giao diện Index.html
*/
// KHÓA CỨNG ID THƯ MỤC GOOGLE DRIVE CHO BẢN DEMO (Khách không thể sửa từ giao diện)
const LOCKED_DRIVE_FOLDER_ID = '10-d91Cf-_xYpx54B0uR-Fzvc48Jcwl8-';
// Hỗ trợ vừa chạy trực tiếp bằng HtmlService vừa làm REST API cho GitHub Pages
function doGet(e) {
setupDatabase();
if (e && e.parameter && e.parameter.action === 'getInitialData') {
return ContentService.createTextOutput(JSON.stringify({ ok: true, data: getInitialData() }))
.setMimeType(ContentService.MimeType.JSON);
}
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('Webapp Quản Lý Tài Liệu, Văn Bản (v1.0)')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function doPost(e) {
try {
const body = JSON.parse(e.postData.contents || '{}');
if (body.action === 'syncState') {
return ContentService.createTextOutput(JSON.stringify(syncFullStateToSheets(body.state)))
.setMimeType(ContentService.MimeType.JSON);
}
if (body.action === 'uploadFile') {
body.payload.folderId = LOCKED_DRIVE_FOLDER_ID;
return ContentService.createTextOutput(JSON.stringify(uploadFileToDrive(body.payload)))
.setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput(JSON.stringify({ ok: false, message: 'Unknown action' }))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({ ok: false, message: err.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function setupDatabase() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheets = {
'HoSo': ['ID', 'SoHoSo', 'TenTaiLieu', 'DanhMucID', 'PhongBan', 'DuAn', 'NhaCungCap', 'NgayBanHanh', 'NgayKetThuc', 'GiaTri', 'TinhTrang', 'MoTa', 'FileName', 'FileType', 'NguoiPhuTrach', 'PreviewUrl'],
'DanhMuc': ['ID', 'TenDanhMuc', 'Icon', 'MauSac'],
'CauHinh': ['Key', 'JSONValue'],
'NguoiDung': ['Username', 'Password', 'HoTen', 'VaiTro', 'DanhMucDuocXemJSON']
};
Object.keys(sheets).forEach(name => {
let sh = ss.getSheetByName(name);
if (!sh) sh = ss.insertSheet(name);
if (sh.getLastRow() === 0) sh.appendRow(sheets[name]);
});
}
function getInitialData() {
setupDatabase();
const ss = SpreadsheetApp.getActiveSpreadsheet();
const shConfig = ss.getSheetByName('CauHinh');
if (shConfig.getLastRow() >= 2) {
const rawState = shConfig.getRange(2, 2).getValue();
if (rawState) {
try { return JSON.parse(rawState); } catch (e) {}
}
}
return null;
}
function syncFullStateToSheets(stateObj) {
setupDatabase();
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. Lưu JSON tổng vào CauHinh để tốc độ tải trang tức thì
const shConfig = ss.getSheetByName('CauHinh');
shConfig.clearContents();
shConfig.appendRow(['Key', 'JSONValue']);
shConfig.appendRow(['APP_STATE', JSON.stringify(stateObj)]);
// 2. Đồng bộ bảng phẳng ra Sheet HoSo để dễ xem và lọc trực tiếp trên Google Sheets
const shHoSo = ss.getSheetByName('HoSo');
shHoSo.clearContents();
shHoSo.appendRow(['ID', 'SoHoSo', 'TenTaiLieu', 'DanhMucID', 'PhongBan', 'DuAn', 'NhaCungCap', 'NgayBanHanh', 'NgayKetThuc', 'GiaTri', 'TinhTrang', 'MoTa', 'FileName', 'FileType', 'NguoiPhuTrach', 'PreviewUrl']);
if (stateObj.documents && stateObj.documents.length > 0) {
const rows = stateObj.documents.map(d => [
d.id, d.code, d.title, d.categoryId, d.department, d.project, d.vendor,
d.issueDate, d.expiryDate, d.value || 0, d.status, d.description,
d.fileName, d.fileType, d.owner, d.drivePreviewUrl || ''
]);
shHoSo.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
}
return { ok: true };
}
function verifyDriveFolder(folderInput) {
try {
const idMatch = String(folderInput || '').match(/[-\w]{25,}/);
const folderId = idMatch ? idMatch[0] : String(folderInput || '').trim();
const folder = DriveApp.getFolderById(folderId);
return { ok: true, id: folderId, name: folder.getName() };
} catch (err) {
return { ok: false, message: 'Không tìm thấy thư mục hoặc chưa cấp quyền: ' + err.message };
}
}
function uploadFileToDrive(payload) {
try {
const idMatch = String(payload.folderId || '').match(/[-\w]{25,}/);
const folderId = idMatch ? idMatch[0] : String(payload.folderId || '').trim();
const folder = folderId ? DriveApp.getFolderById(folderId) : DriveApp.getRootFolder();
const bytes = Utilities.base64Decode(payload.base64Data);
const blob = Utilities.newBlob(bytes, payload.mimeType || 'application/octet-stream', payload.fileName);
const file = folder.createFile(blob);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
return {
ok: true,
fileId: file.getId(),
fileName: file.getName(),
previewUrl: 'https://drive.google.com/file/d/' + file.getId() + '/preview',
viewUrl: file.getUrl()
};
} catch (err) {
return { ok: false, message: err.message };
}
}