121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { FormEvent, useEffect, useState } from "react";
|
|
import { api, apiForm } from "@/lib/api";
|
|
import { Modal, RegistryTable } from "@/components/registry-table";
|
|
|
|
type OperationFile = {
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
sizeBytes: number;
|
|
createdAt: string;
|
|
};
|
|
|
|
function formatSize(bytes: number) {
|
|
if (bytes < 1024) {
|
|
return `${bytes} B`;
|
|
}
|
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
}
|
|
|
|
export default function ArquivosPage() {
|
|
const [rows, setRows] = useState<OperationFile[]>([]);
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [open, setOpen] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
async function load() {
|
|
setRows(await api<OperationFile[]>("/api/files"));
|
|
}
|
|
|
|
useEffect(() => {
|
|
load().catch((err: unknown) => setError(err instanceof Error ? err.message : "Erro"));
|
|
}, []);
|
|
|
|
function closeModal() {
|
|
setOpen(false);
|
|
setFile(null);
|
|
setError("");
|
|
}
|
|
|
|
async function onSubmit(event: FormEvent) {
|
|
event.preventDefault();
|
|
if (!file) {
|
|
setError("Selecione um arquivo.");
|
|
return;
|
|
}
|
|
setError("");
|
|
setSaving(true);
|
|
try {
|
|
const body = new FormData();
|
|
body.append("file", file);
|
|
await apiForm("/api/files", body);
|
|
closeModal();
|
|
await load();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Não foi possível anexar.");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-[#82C85C]">Arquivos</p>
|
|
<h1 className="font-display mt-2 text-4xl font-extrabold uppercase">Anexos da operação</h1>
|
|
{error && !open ? <p className="mt-4 text-sm text-red-300">{error}</p> : null}
|
|
|
|
<RegistryTable
|
|
countLabel={`${rows.length} arquivo${rows.length === 1 ? "" : "s"}`}
|
|
columns={["Nome", "Tipo", "Tamanho", "Data", ""]}
|
|
isEmpty={rows.length === 0}
|
|
empty="Nenhum arquivo anexado."
|
|
action={
|
|
<button className="btn-sinka px-4 py-2 text-xs" type="button" onClick={() => setOpen(true)}>
|
|
Anexar
|
|
</button>
|
|
}
|
|
>
|
|
{rows.map((row) => (
|
|
<tr key={row.id} className="border-t border-white/10">
|
|
<td className="px-5 py-3 font-medium">{row.name}</td>
|
|
<td className="px-5 py-3 text-[#E8F4E5]/70">{row.type}</td>
|
|
<td className="px-5 py-3 text-[#E8F4E5]/70">{formatSize(row.sizeBytes)}</td>
|
|
<td className="px-5 py-3 text-[#E8F4E5]/70">{new Date(row.createdAt).toLocaleString("pt-BR")}</td>
|
|
<td className="px-5 py-3 text-right">
|
|
<a href={`/api/files/${row.id}/download`} className="btn-ghost cursor-pointer px-3 py-1.5 text-[11px]">
|
|
Baixar
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</RegistryTable>
|
|
|
|
{open ? (
|
|
<Modal onClose={closeModal}>
|
|
<form className="flex flex-col gap-3" onSubmit={onSubmit}>
|
|
<h2 className="font-display text-2xl font-bold uppercase">Anexar arquivo</h2>
|
|
{error ? <p className="text-sm text-red-300">{error}</p> : null}
|
|
<input
|
|
className="text-sm file:mr-3 file:rounded-lg file:border-0 file:bg-[#82C85C] file:px-3 file:py-2 file:text-xs file:font-semibold file:text-black"
|
|
type="file"
|
|
accept=".csv,.xlsx,.xls,.pdf,.txt"
|
|
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
|
|
/>
|
|
<div className="mt-2 flex gap-2">
|
|
<button className="btn-sinka" disabled={saving} type="submit">
|
|
{saving ? "Enviando…" : "Anexar"}
|
|
</button>
|
|
<button className="btn-ghost" type="button" onClick={closeModal}>
|
|
Cancelar
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|