import { useState } from "react"; import { Link, useLoaderData, useRevalidator } from "react-router"; export const handle = { breadcrumbs: (data: { companyId: string; companyName: string }) => [ { label: "Mandanten", href: "/companies" }, { label: data.companyName, href: `/companies/${data.companyId}` }, { label: "Leistungen" }, ], }; import { requireUser } from "@/session.server"; import prisma from "@/lib/prisma.server"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; // CardContent used for empty state import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Briefcase, Plus, Edit, Trash2, ChevronLeft, ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { TAX_RATES, formatCurrency } from "@/lib/tax"; const schema = z.object({ name: z.string().min(1, "Pflichtfeld"), description: z.string().optional(), unit: z.string().optional(), unitPrice: z.coerce.number({ invalid_type_error: "Ungültiger Betrag" }), taxRate: z.coerce.number(), }); type FormData = z.infer; interface Service { id: string; name: string; description: string | null; unit: string | null; unitPrice: number; taxRate: number; } export async function loader({ request, params }: { request: Request; params: { id: string } }) { const user = await requireUser(request); const company = await prisma.company.findFirst({ where: { id: params.id, userId: user.id }, }); if (!company) throw new Response("Not Found", { status: 404 }); const services = await prisma.service.findMany({ where: { companyId: params.id }, orderBy: { name: "asc" }, }); return { services: services.map((s) => ({ ...s, unitPrice: Number(s.unitPrice), taxRate: Number(s.taxRate), })), companyId: params.id, companyName: company.name, }; } function ServiceForm({ defaultValues, onSubmit, submitLabel, }: { defaultValues?: Partial; onSubmit: (d: FormData) => Promise; submitLabel: string; }) { const { register, handleSubmit, setValue, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { taxRate: 19, ...defaultValues }, }); return (
{errors.name &&

{errors.name.message}

}
{errors.unitPrice &&

{errors.unitPrice.message}

}
); } type SortKey = "name" | "description" | "unit" | "unitPrice" | "taxRate"; type SortDir = "asc" | "desc"; export default function LeistungenPage() { const { services, companyId } = useLoaderData(); const { revalidate } = useRevalidator(); const [open, setOpen] = useState(false); const [editService, setEditService] = useState(null); const [sortKey, setSortKey] = useState("name"); const [sortDir, setSortDir] = useState("asc"); function toggleSort(key: SortKey) { if (sortKey === key) { setSortDir((d) => (d === "asc" ? "desc" : "asc")); } else { setSortKey(key); setSortDir("asc"); } } const sorted = [...services].sort((a, b) => { const av = a[sortKey] ?? ""; const bv = b[sortKey] ?? ""; const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av).localeCompare(String(bv), "de"); return sortDir === "asc" ? cmp : -cmp; }); async function handleCreate(data: FormData) { await fetch("/api/services", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...data, companyId }), }); setOpen(false); revalidate(); } async function handleEdit(data: FormData) { if (!editService) return; await fetch(`/api/services/${editService.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); setEditService(null); revalidate(); } async function handleDelete(serviceId: string) { if (!confirm("Leistung wirklich löschen?")) return; await fetch(`/api/services/${serviceId}`, { method: "DELETE" }); revalidate(); } return (
Zurück zum Mandanten

Leistungen

{services.length} {services.length === 1 ? "Leistung" : "Leistungen"}

Neue Leistung
!o && setEditService(null)}> Leistung bearbeiten {editService && ( )} {services.length === 0 ? (

Noch keine Leistungen angelegt

) : (
{(["name", "description", "unit", "unitPrice", "taxRate"] as SortKey[]).map((key, i) => { const labels: Record = { name: "Bezeichnung", description: "Beschreibung", unit: "Einheit", unitPrice: "Preis", taxRate: "MwSt." }; const isNum = key === "unitPrice" || key === "taxRate"; const active = sortKey === key; const Icon = active ? (sortDir === "asc" ? ChevronUp : ChevronDown) : ChevronsUpDown; return ( ); })} {sorted.map((service) => ( ))}
{service.name} {service.description ?? "-"} {service.unit ?? "-"} {formatCurrency(service.unitPrice)} {service.taxRate}%
)}
); }