Files
AnnasRechnungsManager/app/routes/admin.mandanten.tsx
T
hwinkel b22e5baa5c
Build and Push Docker Image / build (push) Successful in 1m23s
feat: add client-side validation utilities and debugging tools
- Implemented client-side validation functions for tax ID, VAT ID, IBAN, BIC, and website URL.
- Added debug logging functionality to assist in development.
- Created a comprehensive validation function for company form data.

feat: initialize database with Prisma migrations

- Added a server-side script to run Prisma migrations and check database health.
- Ensured safe initialization of the database to prevent concurrent migrations.

feat: comprehensive server-side error logging

- Developed an error logging system that captures detailed error context, including request details and stack traces.
- Implemented logging functions for different error types (route, action, database, API, startup).

fix: validate user ID existence in audit logs

- Updated the logging function to validate that the user ID exists in the database before logging actions.

fix: update schemas for optional fields and validation

- Modified schemas to allow for nullable fields and refined validation logic for tax ID, VAT ID, IBAN, and BIC.

feat: enhance error boundary for better debugging

- Improved error boundary to log detailed error information in development mode.
- Added a debug panel to the main application layout for real-time error tracking.

feat: implement company deletion functionality in admin routes

- Added a new API route for deleting companies with appropriate logging.
- Integrated delete confirmation in the admin interface for better user experience.

fix: handle API errors gracefully

- Wrapped API actions in try-catch blocks to log errors and return appropriate responses.

feat: generate and save invoice PDFs

- Implemented functionality to generate and save invoice PDFs upon status updates.
- Added a new column in the database for storing the URL of the generated PDF.

chore: update Docker image reference

- Changed the Docker image reference to point to the new Git repository.

chore: update package dependencies

- Added @radix-ui/react-tooltip for enhanced UI components.
- Updated package-lock.json to reflect new dependencies.
2026-05-03 08:46:58 +02:00

176 lines
6.2 KiB
TypeScript

import { Link, useLoaderData } from "react-router";
import { requireAdmin } from "@/session.server";
import prisma from "@/lib/prisma.server";
import { Badge } from "@/components/ui/badge";
import { Building2, Archive, Trash2 } from "lucide-react";
import { useState } from "react";
export async function loader({ request }: { request: Request }) {
await requireAdmin(request);
const companies = await prisma.company.findMany({
include: {
user: { select: { id: true, name: true, email: true } },
_count: { select: { invoices: true, customers: true } },
},
orderBy: [{ archived: "asc" }, { name: "asc" }],
});
return {
companies: companies.map((c) => ({
...c,
archivedAt: c.archivedAt?.toISOString() ?? null,
})),
};
}
export default function AdminMandanten() {
const { companies } = useLoaderData<typeof loader>();
const active = companies.filter((c) => !c.archived);
const archived = companies.filter((c) => c.archived);
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-900">Alle Mandanten</h1>
<p className="text-sm text-slate-500 mt-1">
{companies.length} Mandanten gesamt · {active.length} aktiv · {archived.length} archiviert
</p>
</div>
<MandantenTabelle companies={active} title="Aktive Mandanten" />
{archived.length > 0 && (
<div className="mt-8">
<MandantenTabelle companies={archived} title="Archivierte Mandanten" archived />
</div>
)}
</div>
);
}
type Company = {
id: string;
name: string;
legalForm: string | null;
city: string;
email: string | null;
archived: boolean;
user: { id: string; name: string; email: string };
_count: { invoices: number; customers: number };
};
function MandantenTabelle({
companies,
title,
archived = false,
}: {
companies: Company[];
title: string;
archived?: boolean;
}) {
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const handleDelete = async (companyId: string, companyName: string) => {
if (deleteConfirm !== companyId) {
setDeleteConfirm(companyId);
return;
}
setIsDeleting(true);
try {
const response = await fetch(`/api/admin/companies/${companyId}/delete`, {
method: "DELETE",
});
if (response.ok) {
// Reload the page to refresh the list
window.location.reload();
} else {
const error = await response.json();
alert(`Fehler beim Löschen: ${error.error || response.statusText}`);
setDeleteConfirm(null);
}
} catch (error) {
alert(`Fehler beim Löschen: ${error}`);
setDeleteConfirm(null);
} finally {
setIsDeleting(false);
}
};
if (companies.length === 0) return null;
return (
<div>
<h2 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-3">
{title}
</h2>
<div className="rounded-lg border border-slate-200 overflow-hidden bg-white">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200 bg-slate-50">
<th className="text-left px-4 py-3 font-medium text-slate-600">Mandant</th>
<th className="text-left px-4 py-3 font-medium text-slate-600">Ort</th>
<th className="text-left px-4 py-3 font-medium text-slate-600">Benutzer</th>
<th className="text-right px-4 py-3 font-medium text-slate-600">Rechnungen</th>
<th className="text-right px-4 py-3 font-medium text-slate-600">Kunden</th>
<th className="text-right px-4 py-3 font-medium text-slate-600"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{companies.map((company) => (
<tr key={company.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-slate-400 shrink-0" />
<div>
<div className="font-medium text-slate-900 flex items-center gap-2">
{company.name}
{archived && (
<Archive className="w-3.5 h-3.5 text-slate-400" />
)}
</div>
{company.legalForm && (
<div className="text-xs text-slate-400">{company.legalForm}</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3 text-slate-600">{company.city}</td>
<td className="px-4 py-3">
<div className="text-slate-700">{company.user.name}</div>
<div className="text-xs text-slate-400">{company.user.email}</div>
</td>
<td className="px-4 py-3 text-right text-slate-600">{company._count.invoices}</td>
<td className="px-4 py-3 text-right text-slate-600">{company._count.customers}</td>
<td className="px-4 py-3 text-right space-x-2 flex justify-end">
<Link
to={`/companies/${company.id}`}
className="text-indigo-600 hover:text-indigo-800 font-medium text-xs"
>
Öffnen
</Link>
<button
onClick={() => handleDelete(company.id, company.name)}
disabled={isDeleting}
className={`text-xs font-medium flex items-center gap-1 px-2 py-1 rounded transition-colors ${
deleteConfirm === company.id
? "bg-red-100 text-red-700 hover:bg-red-200"
: "text-slate-500 hover:text-red-600"
} ${isDeleting ? "opacity-50 cursor-not-allowed" : ""}`}
>
<Trash2 className="w-3.5 h-3.5" />
{deleteConfirm === company.id ? "Bestätigen?" : "Löschen"}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}