{open && (
<>
setOpen(false)} />
{options.map((opt) => (
))}
>
)}
);
}
// ─────────────────────────────────────────────────────────────────────────────
// Main component
// ─────────────────────────────────────────────────────────────────────────────
const FilesList = (): ReactElement => {
const navigate = useNavigate();
const { primaryColor } = useAppTheme();
const permissions = useSelector((state: RootState) => state.auth.permissions);
// Permission checks
const canCreate = permissions.some(
(p) =>
(p.resource === "files" || p.resource === "*") &&
(p.action === "create" || p.action === "*"),
);
const canUpdate = permissions.some(
(p) =>
(p.resource === "files" || p.resource === "*") &&
(p.action === "update" || p.action === "*"),
);
const canDelete = permissions.some(
(p) =>
(p.resource === "files" || p.resource === "*") &&
(p.action === "delete" || p.action === "*"),
);
// Table columns
const columns = useMemo
[]>(
() => [
{
key: "original_name",
label: "File Name",
render: (file) => (
),
},
{
key: "file_size",
label: "Size",
render: (file) => (
{file.file_size_formatted || formatBytes(file.file_size)}
),
},
{
key: "category",
label: "Category",
render: (file) =>
file.category ? (
{file.category}
) : (
—
),
},
{
key: "source_module",
label: "Source Module",
render: (file) =>
file.source_module ? (
{file.source_module}
) : (
—
),
},
{
key: "uploaded_by_email",
label: "Uploaded By",
render: (file) =>
file.uploaded_by_email ? (
{getInitials(file.uploaded_by_email)}
{file.uploaded_by_email.split("@")[0]}
) : (
Unknown
),
},
{
key: "created_at",
label: "Upload Date",
render: (file) => (
{formatDate(file.created_at)}
),
},
{
key: "version",
label: "Version",
render: (file) => (
v{file.version}
),
},
{
key: "actions",
label: "Actions",
align: "right",
render: (file) => (
navigate(`/tenant/files/${file.id}`)}
onDownload={() => handleDownload(file)}
onEdit={
canUpdate ? () => navigate(`/tenant/files/${file.id}`) : undefined
}
onDelete={canDelete ? () => openDeleteConfirm(file) : undefined}
/>
),
},
],
[canUpdate, canDelete, navigate, primaryColor],
);
// ── State ──
const [files, setFiles] = useState([]);
const [total, setTotal] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Filters
const [search, setSearch] = useState("");
const [categoryFilter, setCategoryFilter] = useState(null);
const [moduleIdFilter, setModuleIdFilter] = useState(null);
const [modules, setModules] = useState<{ id: string; name: string }[]>([]);
// Pagination
const [currentPage, setCurrentPage] = useState(1);
const [limit, setLimit] = useState(10);
const offset = (currentPage - 1) * limit;
const totalPages = Math.max(1, Math.ceil(total / limit));
// Filter option data
const [categories, setCategories] = useState<
CategoriesFilterOptions["categories"]
>([]);
// Upload modal
const [showUpload, setShowUpload] = useState(false);
// Deleting
const [fileToDelete, setFileToDelete] = useState<{
id: string;
name: string;
} | null>(null);
const [isHardDelete, setIsHardDelete] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// ── Load categories ──
useEffect(() => {
fileAttachmentService
.getCategoriesFilterOptions()
.then((res) => {
setCategories(res.data.categories);
})
.catch(() => {});
moduleService
.getMyModules()
.then((res) => {
if (res.success) {
setModules(res.data.map((m) => ({ id: m.id, name: m.name })));
}
})
.catch(() => {});
}, []);
// ── Load files ──
const loadFiles = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const res = await fileAttachmentService.list({
search: search.trim() || undefined,
category: categoryFilter || undefined,
source_module_id: moduleIdFilter || undefined,
limit,
offset,
});
setFiles(res.data);
setTotal(res.pagination.total);
} catch (err: any) {
setError(err?.response?.data?.error?.message || "Failed to load files");
} finally {
setIsLoading(false);
}
}, [search, categoryFilter, moduleIdFilter, limit, offset]);
useEffect(() => {
void loadFiles();
}, [loadFiles]);
const categoryOptions = useMemo(
() => categories.map((c) => ({ value: c.category, label: c.category })),
[categories],
);
// ── Actions ──
const handleDownload = (file: FileAttachment) => {
fileAttachmentService.download(file.id, file.original_name).catch(() => {});
};
const handleDelete = async () => {
if (!fileToDelete) return;
setIsDeleting(true);
try {
await fileAttachmentService.delete(fileToDelete.id, isHardDelete);
setFileToDelete(null);
setIsHardDelete(false);
await loadFiles();
} catch (err: any) {
alert(err?.response?.data?.error?.message || "Failed to delete file");
} finally {
setIsDeleting(false);
}
};
const openDeleteConfirm = (file: FileAttachment) => {
setFileToDelete({ id: file.id, name: file.original_name });
setIsHardDelete(false);
};
const clearFilters = () => {
setSearch("");
setCategoryFilter(null);
setModuleIdFilter(null);
setCurrentPage(1);
};
// Mobile card renderer
const mobileCardRenderer = (file: FileAttachment) => (
{getFileIcon(file.mime_type, file.original_name, primaryColor)}
navigate(`/tenant/files/${file.id}`)}
>
{file.original_name}
{file.file_size_formatted || formatBytes(file.file_size)} • v
{file.version}
navigate(`/tenant/files/${file.id}`)}
onDownload={() => handleDownload(file)}
onEdit={
canUpdate ? () => navigate(`/tenant/files/${file.id}`) : undefined
}
onDelete={canDelete ? () => openDeleteConfirm(file) : undefined}
/>
{file.category && (
{file.category}
)}
{file.source_module && (
{file.source_module}
)}
{formatDate(file.created_at)}
);
// ─────────────────────────────────────────────────────────────────────────
// Render
// ─────────────────────────────────────────────────────────────────────────
return (
setShowUpload(true)}
className="flex items-center gap-2"
>
Upload New File
) : null,
}}
>
{/* Filter bar */}
{/* Search */}
{
setSearch(v);
setCurrentPage(1);
}}
placeholder="Search by name, ID..."
/>
{/* Category filter */}
{
setCategoryFilter(v);
setCurrentPage(1);
}}
/>
{/* Source Module filter */}
({ value: m.id, label: m.name }))}
value={moduleIdFilter}
onChange={(val) => {
setModuleIdFilter(val as string | null);
setCurrentPage(1);
}}
placeholder="All Modules"
/>
{/* More Filters label pill */}
{/* */}
{/* Table */}
file.id}
isLoading={isLoading}
error={error}
emptyMessage="No files found"
mobileCardRenderer={mobileCardRenderer}
/>
{/* Pagination */}
{total > 0 && (
)}
{/* Upload modal */}
setShowUpload(false)}
categories={categories}
onUploaded={() => {
setShowUpload(false);
void loadFiles();
}}
isTenantAdmin={canCreate}
/>
{fileToDelete && (
setFileToDelete(null)}
onConfirm={handleDelete}
title="Delete File"
message="Are you sure you want to delete this file"
itemName={fileToDelete.name}
isLoading={isDeleting}
>
)}
);
};
export default FilesList;