fix: T6 browser-check findings (compliance, treatments, labels)
Backend Image / build-and-push (push) Successful in 6m39s
Frontend Image / build-and-push (push) Successful in 14m30s

- api-client accepts 2xx answers without body (revoke showed an error)
- authority notices: sentOn in the future is rejected (Berlin day) on
  create, mark-sent and PATCH; dialog defaults to the Berlin day
- service worker never caches /api/v1/site-settings
- treatments/new no longer overflows at 390 px (min-w-0 on form
  wrappers); dose with more than 3 decimals is an error, never rounded
- medicines: expired lots flagged, disposal over the rest warns
- label designer: side panel at lg, sample values instead of
  placeholders, compliance panel reopens on lg, labelled zoom buttons
- skip link to #main-content, headings on no-access pages

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-15 18:34:52 +02:00
1 parent 6c390efe7b
commit e712c5d4d1
64 files changed
+1497 -323

No files matched your search

@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { NoComplianceAccess } from "./no-compliance-access";
describe("NoComplianceAccess", () => {
it("names the area in a heading above the missing-right text", () => {
render(<NoComplianceAccess />);
expect(screen.getByRole("heading", { level: 1, name: "Prüfbereitschaft" })).toBeInTheDocument();
expect(screen.getByText("Dazu fehlt dir das Recht.")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Zurück zum Start" })).toHaveAttribute("href", "/dashboard");
});
});
@@ -4,6 +4,7 @@ import Link from "next/link";
export function NoComplianceAccess() {
return (
<main className="flex flex-col gap-3 p-4 sm:p-6">
<h1 className="font-heading text-xl font-semibold tracking-tight">Prüfbereitschaft</h1>
<p role="status" className="text-sm">
Dazu fehlt dir das Recht.
</p>
@@ -48,7 +48,7 @@ export function Field({
const Caption = group ? "span" : "label";
return (
<div className={cn("flex flex-col gap-1.5", className)}>
<div className={cn("flex min-w-0 flex-col gap-1.5", className)}>
<Caption
id={`${id}-label`}
htmlFor={group ? undefined : id}
@@ -17,6 +17,8 @@ interface FormSectionProps {
collapsible?: boolean;
defaultOpen?: boolean;
fieldCount?: number;
/** Shows the content although the section is folded, e.g. while a field inside has a problem. */
forceOpen?: boolean;
children: ReactNode;
}
@@ -27,9 +29,11 @@ export function FormSection({
collapsible = false,
defaultOpen = true,
fieldCount,
forceOpen = false,
children,
}: FormSectionProps) {
const [open, setOpen] = useState(collapsible ? defaultOpen : true);
const [openState, setOpen] = useState(collapsible ? defaultOpen : true);
const open = openState || forceOpen;
const heading = (
<span className="flex items-center gap-2">
{step !== undefined ? (
@@ -45,7 +49,7 @@ export function FormSection({
);
return (
<fieldset className="flex flex-col gap-4 border-t border-border/60 pt-4 first:border-t-0 first:pt-0">
<fieldset className="flex min-w-0 flex-col gap-4 border-t border-border/60 pt-4 first:border-t-0 first:pt-0">
{collapsible ? (
<legend className="contents">
<button
@@ -77,7 +81,7 @@ export function FormSection({
{description && open ? (
<p className="-mt-2 text-sm text-muted-foreground">{description}</p>
) : null}
{open ? <div className="flex flex-col gap-4">{children}</div> : null}
{open ? <div className="flex min-w-0 flex-col gap-4">{children}</div> : null}
</fieldset>
);
}
@@ -124,4 +124,14 @@ describe("RecordSelect", () => {
expect(onChange).toHaveBeenCalledWith("");
expect(screen.getByRole("combobox")).toHaveTextContent("Kein Bienenstand");
});
it("truncates a long chosen label in the trigger instead of widening the form", () => {
render(<Harness initial="cmg3xabc" />);
const trigger = screen.getByRole("combobox");
expect(trigger.className).toContain("min-w-0");
const label = screen.getByText("Obstwiese");
expect(label.className).toContain("truncate");
expect(label.className).toContain("min-w-0");
});
});
@@ -82,12 +82,15 @@ export function RecordSelect<T extends { id: string }>({
id={id}
aria-label={ariaLabel}
data-field={dataField}
className={cn("h-11 w-full", className)}
className={cn("h-11 w-full min-w-0", className)}
aria-describedby={describedBy}
aria-invalid={invalid}
>
<SelectValue placeholder={placeholder}>
{triggerLabel({ value, labelItems, getLabel, placeholder, noneLabel })}
<SelectValue placeholder={placeholder} className="min-w-0">
{/* A long label is cut with an ellipsis; the list shows it in full. */}
<span className="min-w-0 truncate" data-slot="record-select-label">
{triggerLabel({ value, labelItems, getLabel, placeholder, noneLabel })}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
@@ -0,0 +1,20 @@
import { AlertTriangle } from "lucide-react";
import { todayLocalIsoDate } from "@/lib/dates";
import { formatDay } from "@/lib/medicines/format";
/**
* A "best before" day of a medicine or pack. A day in the past is red AND
* carries a symbol and the word "abgelaufen", so the state does not rely on
* colour alone. The day itself still counts as valid.
*/
export function ExpiryText({ day, today }: { day: string | null; today?: string }) {
if (day && day < (today ?? todayLocalIsoDate())) {
return (
<span className="inline-flex max-w-full items-start gap-1 font-semibold whitespace-normal text-destructive">
<AlertTriangle className="mt-0.5 size-4 shrink-0" aria-hidden />
{formatDay(day)} · abgelaufen
</span>
);
}
return <span>{formatDay(day)}</span>;
}
@@ -0,0 +1,12 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { NoReadAccess } from "./no-read-access";
describe("NoReadAccess", () => {
it("names the area in a heading above the missing-right text", () => {
render(<NoReadAccess />);
expect(screen.getByRole("heading", { level: 1, name: "Tierarzneimittel" })).toBeInTheDocument();
expect(screen.getByText("Dazu fehlt dir das Recht.")).toBeInTheDocument();
});
});
@@ -4,6 +4,7 @@ import Link from "next/link";
export function NoReadAccess() {
return (
<main className="flex flex-col gap-3 p-4 sm:p-6">
<h1 className="font-heading text-xl font-semibold tracking-tight">Tierarzneimittel</h1>
<p role="status" className="text-sm">
Dazu fehlt dir das Recht.
</p>
@@ -7,7 +7,7 @@ import { MEDICINE_UNIT_LABELS } from "@/lib/medicines/format";
import type { LotView, MedicineView } from "@/lib/medicines/types";
import { fetchApiaryWeather } from "@/lib/weather/api";
import { bookingLine } from "@/lib/treatments/booking-line";
import { parsedDose } from "@/lib/treatments/dose";
import { doseFormatError, parsedDose } from "@/lib/treatments/dose";
import { lotLabel, lotWarnings } from "@/lib/treatments/lot-text";
import { temperatureHint } from "@/lib/treatments/temperature-hint";
import type { CatalogueFilled } from "@/lib/treatments/use-catalogue-prefill";
@@ -51,6 +51,8 @@ export interface MedicineLotFieldsProps {
doseQuantity: string;
productError?: string;
doseError?: string;
/** A note under the dose, e.g. that it was cleared because the unit changed. */
doseNotice?: string;
/** Which fields carry the note „aus Katalog“. */
filled: CatalogueFilled;
onProductChange: (product: string) => void;
@@ -75,6 +77,7 @@ export function MedicineLotFields({
doseQuantity,
productError,
doseError,
doseNotice,
filled,
onProductChange,
onMedicineChange,
@@ -148,6 +151,7 @@ export function MedicineLotFields({
hasLot={medicineLotId !== ""}
value={doseQuantity}
error={doseError}
notice={doseNotice}
isFromCatalogue={filled.doseQuantity}
onChange={onDoseChange}
/>
@@ -226,6 +230,7 @@ function DoseField({
hasLot,
value,
error,
notice,
isFromCatalogue,
onChange,
}: {
@@ -234,6 +239,7 @@ function DoseField({
hasLot: boolean;
value: string;
error: string | undefined;
notice: string | undefined;
isFromCatalogue: boolean | undefined;
onChange: (dose: string) => void;
}) {
@@ -242,7 +248,12 @@ function DoseField({
? "Je Volk. Ohne Dosis wird nichts abgebucht."
: "Je Volk, wie auf der Packungsbeilage.";
return (
<Field label="Dosis je Volk" id={`${idPrefix}-dose`} hint={hint} error={error}>
<Field
label="Dosis je Volk"
id={`${idPrefix}-dose`}
hint={hint}
error={error ?? doseFormatError(value)}
>
{({ id, describedBy, invalid }) => (
<>
<div className="flex items-center gap-2">
@@ -260,6 +271,11 @@ function DoseField({
{unit ? <span className="text-sm text-muted-foreground">{unit}</span> : null}
</div>
<CatalogueNote id={id} shown={isFromCatalogue} />
<div role="status" aria-live="polite" className="empty:hidden">
{notice ? (
<p className="rounded-lg bg-warning/20 px-3 py-2 text-sm">{notice}</p>
) : null}
</div>
</>
)}
</Field>