278 lines
10 KiB
TypeScript
278 lines
10 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import Button from "../../components/Button";
|
|
import Card from "../../components/Card";
|
|
import PageTitle from "../../components/PageTitle";
|
|
import StatusBadge from "../../components/StatusBadge";
|
|
import ModuleSubnav, { type ModuleSubnavGroup } from "../../layout/ModuleSubnav";
|
|
|
|
type StorageRecord = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
type: string;
|
|
scope: string;
|
|
status: string;
|
|
files: number;
|
|
used: string;
|
|
retention: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
type StorageSection = "browse" | "upload" | "settings" | "retention" | "bulk" | "activity";
|
|
|
|
const storages: StorageRecord[] = [
|
|
{
|
|
id: "campaign-files",
|
|
name: "Campaign files",
|
|
description: "Files uploaded or referenced for campaign attachments.",
|
|
type: "Local path / planned object storage",
|
|
scope: "Campaigns",
|
|
status: "available",
|
|
files: 124,
|
|
used: "2.4 GB",
|
|
retention: "Keep sent evidence",
|
|
updatedAt: "2026-06-08 15:36"
|
|
},
|
|
{
|
|
id: "template-assets",
|
|
name: "Template assets",
|
|
description: "Images and reusable assets for message templates.",
|
|
type: "Planned object storage",
|
|
scope: "Templates",
|
|
status: "planned",
|
|
files: 8,
|
|
used: "34 MB",
|
|
retention: "Manual cleanup",
|
|
updatedAt: "2026-06-06 10:12"
|
|
},
|
|
{
|
|
id: "shared-library",
|
|
name: "Shared library",
|
|
description: "Tenant or group-wide files available to multiple campaigns.",
|
|
type: "Planned object storage",
|
|
scope: "Tenant / groups",
|
|
status: "planned",
|
|
files: 0,
|
|
used: "0 MB",
|
|
retention: "Policy pending",
|
|
updatedAt: "Not connected"
|
|
}
|
|
];
|
|
|
|
const storageSubnav = (onBack: () => void): ModuleSubnavGroup<StorageSection>[] => [
|
|
{
|
|
items: [{ actionId: "file-storages", label: "← File storages", primary: true, onClick: onBack }]
|
|
},
|
|
{
|
|
title: "STORAGE",
|
|
items: [
|
|
{ id: "browse", label: "Browse" },
|
|
{ id: "upload", label: "Upload" },
|
|
{ id: "settings", label: "Settings" },
|
|
{ id: "retention", label: "Retention" },
|
|
{ id: "bulk", label: "Bulk actions" },
|
|
{ id: "activity", label: "Activity" }
|
|
]
|
|
}
|
|
];
|
|
|
|
const demoFiles = [
|
|
{ name: "statement_1001.pdf", path: "/2026/05/statement_1001.pdf", size: "142 KB", updatedAt: "2026-06-08 15:36", status: "ready" },
|
|
{ name: "statement_1002.pdf", path: "/2026/05/statement_1002.pdf", size: "148 KB", updatedAt: "2026-06-08 15:36", status: "ready" },
|
|
{ name: "global_notice.pdf", path: "/shared/global_notice.pdf", size: "81 KB", updatedAt: "2026-06-06 09:44", status: "ready" }
|
|
];
|
|
|
|
export default function FilesPage() {
|
|
const [selectedStorageId, setSelectedStorageId] = useState<string | null>(null);
|
|
const [active, setActive] = useState<StorageSection>("browse");
|
|
const selectedStorage = useMemo(
|
|
() => storages.find((storage) => storage.id === selectedStorageId) ?? null,
|
|
[selectedStorageId]
|
|
);
|
|
|
|
function openStorage(storageId: string) {
|
|
setSelectedStorageId(storageId);
|
|
setActive("browse");
|
|
}
|
|
|
|
if (selectedStorage) {
|
|
return (
|
|
<div className="workspace module-workspace">
|
|
<ModuleSubnav active={active} groups={storageSubnav(() => setSelectedStorageId(null))} onSelect={setActive} />
|
|
<section className="workspace-content">
|
|
<div className="content-pad workspace-data-page">
|
|
<div className="page-heading split workspace-heading">
|
|
<div>
|
|
<PageTitle>{selectedStorage.name}</PageTitle>
|
|
<p>{selectedStorage.description}</p>
|
|
</div>
|
|
<div className="button-row compact-actions">
|
|
<Button disabled>Upload</Button>
|
|
<Button disabled>Download</Button>
|
|
<Button variant="danger" disabled>Delete</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{active === "browse" && <StorageBrowse storage={selectedStorage} />}
|
|
{active === "upload" && <StorageUpload storage={selectedStorage} />}
|
|
{active === "settings" && <StorageSettings storage={selectedStorage} />}
|
|
{active === "retention" && <StorageRetention storage={selectedStorage} />}
|
|
{active === "bulk" && <StorageBulkActions />}
|
|
{active === "activity" && <StorageActivity />}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="content-pad workspace-data-page module-entry-page">
|
|
<div className="page-heading split workspace-heading">
|
|
<div>
|
|
<PageTitle>Files</PageTitle>
|
|
<p>Manage file storages first. Open a storage to browse content, upload files and configure retention.</p>
|
|
</div>
|
|
<div className="button-row compact-actions">
|
|
<Button disabled>Refresh</Button>
|
|
<Button variant="primary" disabled>Add storage</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Card
|
|
title={
|
|
<div className="module-card-heading">
|
|
<h2>File storages</h2>
|
|
<span>Storage endpoints are placeholders until the backend model is added</span>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="app-table-wrap compact-table-wrap module-table-wrap">
|
|
<table className="app-table module-table module-entry-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Storage</th>
|
|
<th>Type</th>
|
|
<th>Scope</th>
|
|
<th>Files</th>
|
|
<th>Used</th>
|
|
<th>Retention</th>
|
|
<th>Updated</th>
|
|
<th aria-label="Actions" />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{storages.map((storage) => (
|
|
<tr key={storage.id}>
|
|
<td>
|
|
<div className="module-title-cell">
|
|
<strong>{storage.name}</strong>
|
|
<span>{storage.description}</span>
|
|
</div>
|
|
</td>
|
|
<td>{storage.type}</td>
|
|
<td>{storage.scope}</td>
|
|
<td>{storage.files}</td>
|
|
<td>{storage.used}</td>
|
|
<td>{storage.retention}</td>
|
|
<td><span className="muted small-text">{storage.updatedAt}</span></td>
|
|
<td className="table-action-cell"><Button onClick={() => openStorage(storage.id)}>Open</Button></td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StorageBrowse({ storage }: { storage: StorageRecord }) {
|
|
return (
|
|
<Card
|
|
title="Browse content"
|
|
actions={<div className="button-row compact-actions"><Button disabled>Upload</Button><Button disabled>Download selected</Button><Button variant="danger" disabled>Delete selected</Button></div>}
|
|
>
|
|
<div className="app-table-wrap compact-table-wrap module-table-wrap">
|
|
<table className="app-table module-table">
|
|
<thead><tr><th>Name</th><th>Path</th><th>Size</th><th>Updated</th><th>Status</th><th></th></tr></thead>
|
|
<tbody>
|
|
{(storage.id === "campaign-files" ? demoFiles : []).map((file) => (
|
|
<tr key={file.path}>
|
|
<td>{file.name}</td>
|
|
<td><code>{file.path}</code></td>
|
|
<td>{file.size}</td>
|
|
<td><span className="muted small-text">{file.updatedAt}</span></td>
|
|
<td><StatusBadge status={file.status} /></td>
|
|
<td className="table-action-cell"><Button disabled>Download</Button></td>
|
|
</tr>
|
|
))}
|
|
{storage.id !== "campaign-files" && <tr><td colSpan={6} className="muted">Files will appear here when this storage is connected.</td></tr>}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function StorageUpload({ storage }: { storage: StorageRecord }) {
|
|
return (
|
|
<Card title="Upload files" actions={<Button variant="primary" disabled>Select files…</Button>}>
|
|
<p className="muted">Upload will target <strong>{storage.name}</strong>. The backend will later provide chunked upload, duplicate handling and progress state.</p>
|
|
<div className="placeholder-stack">
|
|
<span>Drag and drop upload area</span>
|
|
<span>Duplicate handling: ask, replace, keep both</span>
|
|
<span>Optional file tagging after upload</span>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function StorageSettings({ storage }: { storage: StorageRecord }) {
|
|
return (
|
|
<div className="dashboard-grid settings-dashboard-grid">
|
|
<Card title="Storage settings">
|
|
<dl className="detail-list compact-detail-list">
|
|
<div><dt>Type</dt><dd>{storage.type}</dd></div>
|
|
<div><dt>Scope</dt><dd>{storage.scope}</dd></div>
|
|
<div><dt>Status</dt><dd><StatusBadge status={storage.status} /></dd></div>
|
|
</dl>
|
|
</Card>
|
|
<Card title="Backend requirements">
|
|
<p className="muted">This view is prepared for local path, Garage/S3 and tenant/group/user storage settings.</p>
|
|
<div className="placeholder-stack"><span>Storage backend</span><span>Access policy</span><span>Quota</span><span>Encryption / lifecycle</span></div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StorageRetention({ storage }: { storage: StorageRecord }) {
|
|
return (
|
|
<Card title="Retention policy" actions={<Button disabled>Save policy</Button>}>
|
|
<p className="muted">Current policy: {storage.retention}. Retention must respect audit-safe campaigns and sent attachments.</p>
|
|
<div className="placeholder-stack">
|
|
<span>Keep files for sent campaigns</span>
|
|
<span>Prune unused draft uploads after a configurable period</span>
|
|
<span>Export manifest before destructive cleanup</span>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function StorageBulkActions() {
|
|
return (
|
|
<Card title="Bulk actions">
|
|
<p className="muted">Bulk download and delete should be available from the Browse view as well as from a dedicated filtered action view.</p>
|
|
<div className="button-row page-bottom-actions"><Button disabled>Download filtered files</Button><Button variant="danger" disabled>Delete filtered files</Button></div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function StorageActivity() {
|
|
return (
|
|
<Card title="Activity">
|
|
<p className="muted">Storage activity will show uploads, downloads, deletions and retention cleanup runs once backend audit events are available.</p>
|
|
<div className="placeholder-stack"><span>Last upload</span><span>Last bulk delete</span><span>Retention cleanup result</span></div>
|
|
</Card>
|
|
);
|
|
}
|