Implementiert den auf die eigene brigadeId beschränkten Wehr-Bereich: Profil (inkl. Inline-Geocoding via geocodeAddress), Fuhrpark (Fahrzeug per Vorlage oder frei, typisierter Merkmal-Editor), Geräte (Kategorie, Werte, Zuordnung Fahrzeug/„im Gerätehaus") und Benutzerkonten (wehr_admin/wehr_read). - Schema importiert (nicht neu definiert); ASCII-Property wehrfuehrer. - Default-deny dreifach: Layout-Guard requireWehrAdmin() + jede Server Action beginnt mit requireWehrAdmin(); fremde Entities -> notFound() (404). - Validierung an der Grenze (Zod): buildMerkmalValuesSchema validiert Werte typgerecht gegen die serverseitig aufgelösten Definitionen; Rolle auf wehr_admin|wehr_read beschränkt (platform_admin abgelehnt). - upsertMerkmalValues delete-then-insert mit typisierter Drizzle-Tx (kein any); boolean false/num 0 gelten als gesetzt. - argon2id-Einmalpasswort beim Benutzeranlegen; Selbst-Deaktivierung verhindert. - Audit vollständig: brigade.profile_update, vehicle.create/update/delete/status, equipment.create/update/delete/status, user.create/deactivate. - Vorgabewerte aus drei typisierten Spalten (vorgabewert_num/_text/_bool). - i18n via zentraler de.ts; loading/empty/error-konforme Listen. Tests: 22 neue Unit-Tests (vehicle/equipment/brigade-user-Validierung, upsertMerkmalValues) grün; Playwright-Specs verwaltung-fuhrpark + -scoping geschrieben (deferred: kein Server/DB in der Sandbox). Verifikation offline: tsc --noEmit clean, eslint clean, vitest 147 passed, next build exit 0 (alle /verwaltung/*-Routen), drizzle-kit check ohne Drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
2.4 KiB
TypeScript
90 lines
2.4 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import { de } from "@/lib/i18n/de";
|
|
import { assetStatusEnum } from "@/db/schema";
|
|
import {
|
|
setEquipmentStatus,
|
|
deleteEquipment,
|
|
} from "@/server/actions/equipment";
|
|
|
|
type Status = (typeof assetStatusEnum.enumValues)[number];
|
|
|
|
const STATUS_LABEL: Record<Status, string> = {
|
|
einsatzbereit: de.status.einsatzbereit,
|
|
wartung: de.status.wartung,
|
|
ausser_dienst: de.status.ausser_dienst,
|
|
};
|
|
|
|
export function EquipmentControls({
|
|
equipmentId,
|
|
status,
|
|
}: {
|
|
equipmentId: string;
|
|
status: Status;
|
|
}) {
|
|
const router = useRouter();
|
|
const [pending, startTransition] = React.useTransition();
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
|
|
function onStatus(next: Status) {
|
|
setError(null);
|
|
startTransition(async () => {
|
|
const res = await setEquipmentStatus({ id: equipmentId, status: next });
|
|
if (!res.ok) {
|
|
setError(res.error);
|
|
return;
|
|
}
|
|
router.refresh();
|
|
});
|
|
}
|
|
|
|
function onDelete() {
|
|
if (!window.confirm(de.verwaltung.loeschenBestaetigen)) return;
|
|
setError(null);
|
|
startTransition(async () => {
|
|
const res = await deleteEquipment({ id: equipmentId });
|
|
if (!res.ok) {
|
|
setError(res.error);
|
|
return;
|
|
}
|
|
router.push("/verwaltung/geraete");
|
|
router.refresh();
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-3 rounded border border-rand bg-white p-4">
|
|
<label className="text-sm font-medium text-anthrazit" htmlFor="estatus">
|
|
{de.verwaltung.status}
|
|
</label>
|
|
<select
|
|
id="estatus"
|
|
className="h-9 rounded border border-rand bg-white px-3 text-sm text-anthrazit focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-navy"
|
|
value={status}
|
|
disabled={pending}
|
|
onChange={(e) => onStatus(e.target.value as Status)}
|
|
>
|
|
{assetStatusEnum.enumValues.map((v) => (
|
|
<option key={v} value={v}>
|
|
{STATUS_LABEL[v]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<span className="flex-1" />
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={pending}
|
|
onClick={onDelete}
|
|
>
|
|
{de.verwaltung.loeschen}
|
|
</Button>
|
|
{error ? <span className="w-full text-sm text-signal">{error}</span> : null}
|
|
</div>
|
|
);
|
|
}
|