Files
2026-03-13 14:20:07 +01:00

139 lines
5.2 KiB
TypeScript

import { Link, useLoaderData } from "react-router";
import { requireUser } from "@/session.server";
import prisma from "@/lib/prisma.server";
import { formatCurrency } from "@/lib/tax";
import { Archive, Building2, FileText, Users, ArchiveRestore } from "lucide-react";
import { useRevalidator } from "react-router";
export const handle = {
breadcrumbs: () => [{ label: "Archiv" }],
};
export async function loader({ request }: { request: Request }) {
const user = await requireUser(request);
const companies = await prisma.company.findMany({
where: { userId: user.id, archived: true },
include: {
_count: { select: { invoices: true, customers: true } },
invoices: {
where: { status: "PAID" },
select: { grossTotal: true },
},
},
orderBy: { archivedAt: "desc" },
});
return {
isAdmin: user.role === "ADMIN",
companies: companies.map((c) => ({
...c,
archivedAt: c.archivedAt?.toISOString() ?? null,
revenue: c.invoices.reduce((s, inv) => s + Number(inv.grossTotal), 0),
invoices: undefined,
})),
};
}
export default function ArchivPage() {
const { companies, isAdmin } = useLoaderData<typeof loader>();
const { revalidate } = useRevalidator();
async function restore(id: string) {
if (!confirm("Archivierung aufheben?")) return;
await fetch(`/api/companies/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ archived: false }),
});
revalidate();
}
return (
<div className="animate-fade-in">
<div className="mb-8">
<div className="flex items-center gap-3 mb-1">
<Archive className="h-6 w-6 text-slate-400" />
<h1 className="text-2xl font-bold text-slate-900">Archiv</h1>
</div>
<p className="text-slate-500 text-sm mt-1">
{companies.length === 0
? "Keine archivierten Mandanten"
: `${companies.length} archivierte ${companies.length === 1 ? "Mandant" : "Mandanten"}`}
</p>
</div>
{companies.length === 0 ? (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm py-16 text-center">
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-slate-100 mx-auto mb-4">
<Archive className="h-7 w-7 text-slate-300" />
</div>
<p className="text-slate-500 font-medium mb-1">Archiv ist leer</p>
<p className="text-slate-400 text-sm">Archivierte Mandanten erscheinen hier.</p>
</div>
) : (
<div className="space-y-3">
{companies.map((company) => (
<div
key={company.id}
className="bg-white rounded-xl border border-slate-200 shadow-sm p-5 flex items-center gap-4"
>
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-slate-100 shrink-0">
<Building2 className="h-5 w-5 text-slate-400" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Link
to={`/companies/${company.id}`}
className="font-semibold text-slate-700 hover:text-indigo-600 transition-colors text-sm truncate"
>
{company.name}
</Link>
{company.legalForm && (
<span className="text-xs text-slate-400">{company.legalForm}</span>
)}
</div>
<div className="flex items-center gap-4 mt-1 text-xs text-slate-400">
<span className="flex items-center gap-1">
<FileText className="h-3 w-3" />
{company._count.invoices} Rechnungen
</span>
<span className="flex items-center gap-1">
<Users className="h-3 w-3" />
{company._count.customers} Kunden
</span>
<span>Umsatz (bezahlt): {formatCurrency(company.revenue)}</span>
{company.archivedAt && (
<span>
Archiviert: {new Date(company.archivedAt).toLocaleDateString("de-DE")}
</span>
)}
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<Link
to={`/companies/${company.id}`}
className="text-xs text-slate-500 hover:text-slate-700 px-3 py-1.5 rounded-lg border border-slate-200 hover:bg-slate-50 transition-colors"
>
Öffnen
</Link>
{isAdmin && (
<button
onClick={() => restore(company.id)}
className="flex items-center gap-1.5 text-xs text-indigo-600 hover:text-indigo-800 px-3 py-1.5 rounded-lg border border-indigo-100 hover:bg-indigo-50 transition-colors"
>
<ArchiveRestore className="h-3.5 w-3.5" />
Wiederherstellen
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
);
}