fix(tapops): deteksi format file dan format tag

This commit is contained in:
Dodo
2026-06-04 18:53:39 +07:00
parent 36eebac312
commit 5c5c6aa42c
13 changed files with 548 additions and 144 deletions
+137 -4
View File
@@ -7,6 +7,56 @@ export class DocumentFormatter {
{ key: 'praktik', match: ['praktik', 'praktek', 'kerja praktik', 'kp'] },
];
static TAG_ABBREVIATIONS = new Set(['kp', 'sop', 'ta', 'krs', 'khs', 'sks', 'pkl', 'mbkm']);
static FILE_TYPE_RULES = [
{ extensions: ['pdf'], mimeTypes: ['application/pdf'], icon: 'fas fa-file-pdf', css: 'doc-type-pdf' },
{
extensions: ['doc', 'docx'],
mimeTypes: [
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
],
fallbackType: 'docx',
icon: 'fas fa-file-word',
css: 'doc-type-docx',
},
{
extensions: ['ppt', 'pptx'],
mimeTypes: [
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
],
fallbackType: 'pptx',
icon: 'fas fa-file-powerpoint',
css: 'doc-type-pptx',
},
{
extensions: ['xls', 'xlsx'],
mimeTypes: [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
fallbackType: 'xlsx',
icon: 'fas fa-file-excel',
css: 'doc-type-xlsx',
},
{ extensions: ['csv'], mimeTypes: ['text/csv'], icon: 'fas fa-file-csv', css: 'doc-type-xlsx' },
{ extensions: ['txt', 'rtf', 'md'], mimeTypes: ['text/plain', 'application/rtf'], icon: 'fas fa-file-alt', css: 'doc-type-txt' },
{
extensions: ['zip', 'rar', '7z', 'tar', 'gz'],
mimeTypes: ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed', 'application/gzip'],
icon: 'fas fa-file-archive',
css: 'doc-type-archive',
},
{
extensions: ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'],
mimePrefix: 'image/',
icon: 'fas fa-file-image',
css: 'doc-type-image',
},
];
static formatDate(iso) {
if (!iso) return '';
const date = new Date(iso);
@@ -26,14 +76,83 @@ export class DocumentFormatter {
}
static fileTypeFromName(name) {
const lower = (name || '').toLowerCase();
if (lower.endsWith('.docx') || lower.endsWith('.doc')) return 'docx';
return 'pdf';
return this.fileInfoFromName(name).type;
}
static fileInfoFromName(name) {
const extension = this.extensionFromName(name);
const rule = this.FILE_TYPE_RULES.find(item => item.extensions.includes(extension));
if (!rule) {
return {
type: extension || 'file',
icon: 'fas fa-file',
css: 'doc-type-file',
};
}
return {
type: extension,
icon: rule.icon,
css: rule.css,
};
}
static fileInfoFromItem(item) {
const file = item?.file && typeof item.file === 'object' ? item.file : {};
const nameCandidates = [
file.filename_download,
file.filename_disk,
item?.nama_berkas,
];
for (const name of nameCandidates) {
const info = this.fileInfoFromName(name);
if (info.type !== 'file') return info;
}
return this.fileInfoFromMime(file.type);
}
static fileInfoFromMime(type) {
const mime = String(type || '').trim().toLowerCase();
if (!mime) return this.fileInfoFromName('');
const rule = this.FILE_TYPE_RULES.find(item => (
item.mimeTypes?.includes(mime) || (item.mimePrefix && mime.startsWith(item.mimePrefix))
));
if (!rule) return this.fileInfoFromName('');
return {
type: rule.fallbackType || rule.extensions[0],
icon: rule.icon,
css: rule.css,
};
}
static extensionFromName(name) {
const normalized = String(name || '').trim().toLowerCase();
const lastSegment = normalized.split(/[\\/]/).pop() || '';
const extensionStart = lastSegment.lastIndexOf('.');
if (extensionStart <= 0 || extensionStart === lastSegment.length - 1) return '';
return lastSegment.slice(extensionStart + 1);
}
static downloadFilename(name, type) {
const ext = `.${type}`;
return name.toLowerCase().endsWith(ext) ? name : `${name}${ext}`;
if (!name) return `dokumen${ext}`;
return name.toLowerCase().endsWith(ext.toLowerCase()) ? name : `${name}${ext}`;
}
static downloadFilenameFromItem(item) {
const file = item?.file && typeof item.file === 'object' ? item.file : {};
const originalFilename = file.filename_download || file.filename_disk;
if (originalFilename) return originalFilename;
const fileType = this.fileInfoFromItem(item).type;
return this.downloadFilename(item?.nama_berkas || 'dokumen', fileType);
}
static escapeHtml(str) {
@@ -50,6 +169,20 @@ export class DocumentFormatter {
return this.escapeHtml(str);
}
static formatTag(tag) {
const value = String(tag).trim().replace(/\s+/g, ' ');
if (!value) return value;
return value.split(' ').map(word => {
const lower = word.toLowerCase();
if (this.TAG_ABBREVIATIONS.has(lower) || /^[A-Z0-9]{2,}$/.test(word)) {
return lower.toUpperCase();
}
return lower.charAt(0).toUpperCase() + lower.slice(1);
}).join(' ');
}
static parseTags(raw) {
if (!raw) return [];
const str = String(raw).trim();