From 258e90828f8b906753bf869a7ce6b1c37dbf07fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20Rosenl=C3=B6cher?= Date: Fri, 16 Oct 2026 16:47:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(t5):=20Bereiche=20mit=20eigenem=20Symbol?= =?UTF-8?q?=20und=20Ton,=20BrandMark,=20Verl=C3=A4ufe=20raus=20(T5a=20A3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AREAS (37 Bereiche, je Symbol genau einmal; neu compliance, medicines, voice, hive-systems, system) als einzige Quelle für Kopfzeilen, Menü und Schnellanlage; PageHeader, DetailHeader und EntityCard nehmen area statt icon/gradient (Codemod über 72 Dateien). Doppelte Symbole aufgelöst. BrandMark (Wabe) ersetzt das Verlaufs-Sechseck; Honig-Banner auf der Startseite durch Überschrift mit Datum ersetzt; Verläufe aus 33 Verbrauchern entfernt (Token folgen in A4). Refs #26 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/app/(app)/apiaries/[id]/page.tsx | 4 +- .../src/app/(app)/apiaries/map/page.tsx | 2 +- .../src/app/(app)/apiaries/new/page.tsx | 4 +- .../src/app/(app)/apiaries/page.tsx | 4 +- .../src/app/(app)/bottling/[id]/page.tsx | 3 +- .../src/app/(app)/bottling/new/page.tsx | 4 +- .../src/app/(app)/bottling/page.tsx | 4 +- .../src/app/(app)/calendar/page.tsx | 3 +- .../src/app/(app)/colonies/[id]/page.tsx | 4 +- .../src/app/(app)/colonies/new/page.tsx | 4 +- .../src/app/(app)/colonies/page.tsx | 4 +- .../(app)/compliance/notices/[id]/page.tsx | 4 +- .../src/app/(app)/compliance/notices/page.tsx | 2 +- .../src/app/(app)/compliance/page.tsx | 13 +- .../(app)/compliance/restrictions/page.tsx | 4 +- .../app/(app)/compliance/settings/page.tsx | 4 +- .../src/app/(app)/dashboard/page.spec.tsx | 11 ++ .../src/app/(app)/dashboard/page.tsx | 25 +-- .../src/app/(app)/equipment/[id]/page.tsx | 4 +- .../src/app/(app)/equipment/new/page.tsx | 5 +- .../src/app/(app)/equipment/page.tsx | 6 +- .../src/app/(app)/extractions/[id]/page.tsx | 4 +- .../src/app/(app)/extractions/new/page.tsx | 4 +- .../src/app/(app)/extractions/page.tsx | 4 +- .../src/app/(app)/feedings/new/page.tsx | 4 +- .../src/app/(app)/harvests/[id]/page.tsx | 4 +- .../src/app/(app)/harvests/new/page.tsx | 4 +- .../src/app/(app)/harvests/page.tsx | 4 +- .../src/app/(app)/honey-batches/[id]/page.tsx | 4 +- .../src/app/(app)/honey-batches/page.tsx | 4 +- .../src/app/(app)/inspections/new/page.tsx | 5 +- ApiCore-Frontend/src/app/(app)/layout.tsx | 13 +- .../src/app/(app)/medicines/[id]/page.tsx | 8 +- .../app/(app)/medicines/lots/[id]/page.tsx | 4 +- .../src/app/(app)/medicines/page.tsx | 4 +- ApiCore-Frontend/src/app/(app)/nav-config.ts | 162 ++++++++---------- .../src/app/(app)/offline/apiary/page.tsx | 3 +- .../src/app/(app)/offline/colony/page.tsx | 4 +- .../src/app/(app)/offline/page.tsx | 2 +- .../(app)/production-batches/[id]/page.tsx | 4 +- .../app/(app)/production-batches/new/page.tsx | 4 +- .../src/app/(app)/production-batches/page.tsx | 4 +- .../src/app/(app)/products/[id]/page.tsx | 4 +- .../src/app/(app)/products/new/page.tsx | 4 +- .../src/app/(app)/products/page.tsx | 4 +- .../src/app/(app)/profile/page.tsx | 5 +- .../src/app/(app)/queens/[id]/page.tsx | 4 +- .../src/app/(app)/queens/new/page.tsx | 5 +- .../src/app/(app)/queens/page.tsx | 6 +- .../src/app/(app)/quick-actions.ts | 69 ++++---- .../src/app/(app)/quick-add-sheet.spec.tsx | 2 +- .../src/app/(app)/quick-add-sheet.tsx | 7 +- .../src/app/(app)/raw-materials/[id]/page.tsx | 4 +- .../src/app/(app)/raw-materials/new/page.tsx | 4 +- .../src/app/(app)/raw-materials/page.tsx | 4 +- .../src/app/(app)/search/page.tsx | 2 +- .../src/app/(app)/settings/audit-log/page.tsx | 3 +- .../automation/label-templates-tab.tsx | 3 +- .../app/(app)/settings/automation/page.tsx | 5 +- .../automation/task-templates-tab.tsx | 3 +- .../src/app/(app)/settings/backup/page.tsx | 5 +- .../app/(app)/settings/hive-systems/page.tsx | 2 +- .../settings/label-templates/[id]/page.tsx | 4 +- .../settings/label-templates/new/page.tsx | 5 +- .../src/app/(app)/settings/system/page.tsx | 4 +- .../app/(app)/settings/users-roles/page.tsx | 5 +- .../(app)/settings/users-roles/roles-tab.tsx | 3 +- .../src/app/(app)/settings/voice/page.tsx | 4 +- ApiCore-Frontend/src/app/(app)/sidebar.tsx | 14 +- .../src/app/(app)/statistics/page.tsx | 5 +- .../src/app/(app)/storage/page.tsx | 16 +- .../src/app/(app)/suggestions/page.tsx | 3 +- .../src/app/(app)/swarm-control/new/page.tsx | 4 +- .../src/app/(app)/tasks/new/page.tsx | 5 +- ApiCore-Frontend/src/app/(app)/tasks/page.tsx | 6 +- .../src/app/(app)/treatments/new/page.tsx | 5 +- .../src/app/(app)/varroa/new/page.tsx | 4 +- .../src/app/(app)/voice/new/page.tsx | 6 +- ApiCore-Frontend/src/app/(app)/voice/page.tsx | 2 +- .../src/app/(app)/winter/page.tsx | 2 +- .../src/app/(auth)/login/page.tsx | 9 +- ApiCore-Frontend/src/app/globals.css | 2 +- .../src/app/herkunft/[lot]/page.tsx | 9 +- .../src/components/area-icon.spec.tsx | 30 ++++ ApiCore-Frontend/src/components/area-icon.tsx | 44 +++++ .../src/components/area-migration.spec.ts | 70 ++++++++ .../src/components/brand-mark.spec.tsx | 25 +++ .../src/components/brand-mark.tsx | 48 ++++++ .../src/components/detail-header.spec.tsx | 5 +- .../src/components/detail-header.tsx | 13 +- .../src/components/entity-card.tsx | 16 +- .../hive/editor/editor-stage-overlay.tsx | 2 +- .../src/components/page-header.spec.tsx | 14 ++ .../src/components/page-header.tsx | 21 +-- ApiCore-Frontend/src/components/ui/button.tsx | 2 +- ApiCore-Frontend/src/lib/areas.spec.ts | 55 ++++++ ApiCore-Frontend/src/lib/areas.ts | 149 ++++++++++++++++ 97 files changed, 732 insertions(+), 395 deletions(-) create mode 100644 ApiCore-Frontend/src/components/area-icon.spec.tsx create mode 100644 ApiCore-Frontend/src/components/area-icon.tsx create mode 100644 ApiCore-Frontend/src/components/area-migration.spec.ts create mode 100644 ApiCore-Frontend/src/components/brand-mark.spec.tsx create mode 100644 ApiCore-Frontend/src/components/brand-mark.tsx create mode 100644 ApiCore-Frontend/src/components/page-header.spec.tsx create mode 100644 ApiCore-Frontend/src/lib/areas.spec.ts create mode 100644 ApiCore-Frontend/src/lib/areas.ts diff --git a/ApiCore-Frontend/src/app/(app)/apiaries/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/apiaries/[id]/page.tsx index 85097ac..dd03f78 100644 --- a/ApiCore-Frontend/src/app/(app)/apiaries/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/apiaries/[id]/page.tsx @@ -2,7 +2,7 @@ import { useParams } from "next/navigation"; import { useMutation, useQuery } from "@tanstack/react-query"; -import { FileText, MapPin, Plus } from "lucide-react"; +import { FileText, Plus } from "lucide-react"; import { toast } from "sonner"; import { fetchApiary } from "@/lib/apiaries/api"; import { useAuth } from "@/lib/auth/auth-context"; @@ -50,7 +50,7 @@ export default function ApiaryDetailPage() { return (
diff --git a/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx b/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx index 64b7ae2..ed67e50 100644 --- a/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/apiaries/page.tsx @@ -40,7 +40,7 @@ export default function ApiariesPage() { return (
diff --git a/ApiCore-Frontend/src/app/(app)/bottling/page.tsx b/ApiCore-Frontend/src/app/(app)/bottling/page.tsx index 5c3e8d8..dd9bf79 100644 --- a/ApiCore-Frontend/src/app/(app)/bottling/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/bottling/page.tsx @@ -23,7 +23,7 @@ export default function BottlingBatchesPage() { return (
{bottlingBatch.lotNumber}} subtitle={bottlingBatch.product?.name ?? bottlingBatch.honeyBatch.batchNumber} fields={[ diff --git a/ApiCore-Frontend/src/app/(app)/calendar/page.tsx b/ApiCore-Frontend/src/app/(app)/calendar/page.tsx index 1a32ff0..a17f2bc 100644 --- a/ApiCore-Frontend/src/app/(app)/calendar/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/calendar/page.tsx @@ -259,10 +259,9 @@ export default function CalendarPage() { return (
diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/page.tsx index 99086e3..036054c 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; import { useMutation, useQuery } from "@tanstack/react-query"; -import { ClipboardCheck, FileText, Layers } from "lucide-react"; +import { ClipboardCheck, FileText } from "lucide-react"; import { toast } from "sonner"; import { fetchColony } from "@/lib/colonies/api"; import { COLONY_STATUS_LABELS, COLONY_TYPE_LABELS, colonyStrengthText } from "@/lib/colonies/labels"; @@ -56,7 +56,7 @@ export default function ColonyDetailPage() { return (
} diff --git a/ApiCore-Frontend/src/app/(app)/colonies/new/page.tsx b/ApiCore-Frontend/src/app/(app)/colonies/new/page.tsx index 5dccc4f..6719650 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/new/page.tsx @@ -4,7 +4,7 @@ import { FormEvent, useState } from "react"; import { useRouter } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; -import { Layers } from "lucide-react"; + import { createColony } from "@/lib/colonies/api"; import { COLONY_TYPES, COLONY_TYPE_LABELS, MAX_OCCUPIED_FRAMES } from "@/lib/colonies/labels"; import { fetchApiaries } from "@/lib/apiaries/api"; @@ -110,7 +110,7 @@ export default function NewColonyPage() { draft={draft} /> diff --git a/ApiCore-Frontend/src/app/(app)/colonies/page.tsx b/ApiCore-Frontend/src/app/(app)/colonies/page.tsx index e133c62..c84dae5 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/page.tsx @@ -55,7 +55,7 @@ export default function ColoniesPage() { return (
{colony.code}} subtitle={COLONY_TYPE_LABELS[colony.colonyType]} badge={ diff --git a/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/page.tsx index b501494..8c2e97b 100644 --- a/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/compliance/notices/[id]/page.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from "react"; import { useParams } from "next/navigation"; -import { BookOpen, FileText, RotateCcw } from "lucide-react"; +import { FileText, RotateCcw } from "lucide-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { NoComplianceAccess } from "@/components/compliance/no-compliance-access"; import { NoticeStatus } from "@/components/compliance/notice-status"; @@ -138,7 +138,7 @@ export default function NoticeDetailPage() { return (
} diff --git a/ApiCore-Frontend/src/app/(app)/compliance/notices/page.tsx b/ApiCore-Frontend/src/app/(app)/compliance/notices/page.tsx index 2350fd0..5cb6eb9 100644 --- a/ApiCore-Frontend/src/app/(app)/compliance/notices/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/compliance/notices/page.tsx @@ -44,7 +44,7 @@ export default function NoticesPage() { return (
{ expect(await within(row).findByText("V-07")).toBeInTheDocument(); expect(fetchColonies).toHaveBeenCalledTimes(1); }); + + it("the dashboard header has no honey banner and no white text", async () => { + vi.mocked(fetchAutopilotToday).mockResolvedValue(todayFixture({})); + const { container } = renderPage(); + + const heading = await screen.findByRole("heading", { level: 1 }); + const header = heading.closest("header") as HTMLElement; + expect(header.className).not.toMatch(/text-white|bg-|gradient/); + expect(header.getAttribute("style")).toBeNull(); + expect(container.innerHTML).not.toMatch(/text-white|--gradient/); + }); }); diff --git a/ApiCore-Frontend/src/app/(app)/dashboard/page.tsx b/ApiCore-Frontend/src/app/(app)/dashboard/page.tsx index 9ec76fd..8aa0948 100644 --- a/ApiCore-Frontend/src/app/(app)/dashboard/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/dashboard/page.tsx @@ -24,7 +24,6 @@ import { DashboardAlert, DashboardAlertType } from "@/lib/dashboard/types"; import { fetchTasks } from "@/lib/tasks/api"; import { tasksForToday } from "@/lib/tasks/today"; import { Button } from "@/components/ui/button"; -import { HoneycombPattern } from "@/components/honeycomb-pattern"; import { StandNote } from "@/components/offline/stand-note"; import { ComplianceLine } from "@/components/today/compliance-line"; import { TodayPanel } from "@/components/today/today-panel"; @@ -157,21 +156,15 @@ export default function DashboardPage() { return (
-
- -
-

- {greeting(now)} - {firstName ? `, ${firstName}` : ""} -

-

- {now.toLocaleDateString("de-DE", { weekday: "long", day: "numeric", month: "long" })} - {todayCount !== null ? ` · ${todaySummary(todayCount)}` : ""} -

-
+
+

+ {greeting(now)} + {firstName ? `, ${firstName}` : ""} +

+

+ {now.toLocaleDateString("de-DE", { weekday: "long", day: "numeric", month: "long" })} + {todayCount !== null ? ` · ${todaySummary(todayCount)}` : ""} +

diff --git a/ApiCore-Frontend/src/app/(app)/equipment/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/equipment/[id]/page.tsx index 17d1847..ab0d68d 100644 --- a/ApiCore-Frontend/src/app/(app)/equipment/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/equipment/[id]/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useParams } from "next/navigation"; -import { Wrench } from "lucide-react"; + import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { fetchEquipment, updateEquipment } from "@/lib/equipment/api"; @@ -42,7 +42,7 @@ export default function EquipmentDetailPage() { return (
{draft.pending ? ( }> + Neuer Gegenstand @@ -55,8 +54,7 @@ export default function EquipmentPage() { }> diff --git a/ApiCore-Frontend/src/app/(app)/extractions/new/page.tsx b/ApiCore-Frontend/src/app/(app)/extractions/new/page.tsx index 5c13544..b782274 100644 --- a/ApiCore-Frontend/src/app/(app)/extractions/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/extractions/new/page.tsx @@ -4,7 +4,7 @@ import { FormEvent, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; -import { Droplet } from "lucide-react"; + import { fetchHarvests } from "@/lib/harvests/api"; import { createExtraction, @@ -150,7 +150,7 @@ export default function NewExtractionPage() { draft={draft} /> diff --git a/ApiCore-Frontend/src/app/(app)/extractions/page.tsx b/ApiCore-Frontend/src/app/(app)/extractions/page.tsx index 8e839db..c381a12 100644 --- a/ApiCore-Frontend/src/app/(app)/extractions/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/extractions/page.tsx @@ -16,7 +16,7 @@ export default function ExtractionsPage() { return (
diff --git a/ApiCore-Frontend/src/app/(app)/harvests/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/harvests/[id]/page.tsx index 1cbaff7..25ffd19 100644 --- a/ApiCore-Frontend/src/app/(app)/harvests/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/harvests/[id]/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useParams } from "next/navigation"; -import { Boxes } from "lucide-react"; + import { useQuery } from "@tanstack/react-query"; import { fetchHarvest } from "@/lib/harvests/api"; import { Button } from "@/components/ui/button"; @@ -27,7 +27,7 @@ export default function HarvestDetailPage() { return (
diff --git a/ApiCore-Frontend/src/app/(app)/harvests/page.tsx b/ApiCore-Frontend/src/app/(app)/harvests/page.tsx index e103d3e..2286737 100644 --- a/ApiCore-Frontend/src/app/(app)/harvests/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/harvests/page.tsx @@ -21,7 +21,7 @@ export default function HarvestsPage() { return (
{honeyBatch.batchNumber}} subtitle={honeyBatch.honeyType ?? "—"} badge={ diff --git a/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx b/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx index b11c744..0905dac 100644 --- a/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/inspections/new/page.tsx @@ -4,7 +4,7 @@ import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; -import { AlertTriangle, ClipboardCheck } from "lucide-react"; +import { AlertTriangle } from "lucide-react"; import { createInspection } from "@/lib/inspections/api"; import { fetchColonies } from "@/lib/colonies/api"; import { fetchApiaryWeather } from "@/lib/weather/api"; @@ -668,10 +668,9 @@ function InspectionCaptureForm({
) : null} {draft.pending ? ( - - - - ApiCore +
diff --git a/ApiCore-Frontend/src/app/(app)/medicines/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/medicines/[id]/page.tsx index a0a5814..05284f2 100644 --- a/ApiCore-Frontend/src/app/(app)/medicines/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/medicines/[id]/page.tsx @@ -2,7 +2,7 @@ import { useId, type ReactNode } from "react"; import { useParams } from "next/navigation"; -import { Package, Pill } from "lucide-react"; +import { Pill } from "lucide-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { useAuth } from "@/lib/auth/auth-context"; @@ -62,7 +62,7 @@ export default function MedicineDetailPage() { return (
{medicine.lots.length === 0 ? ( @@ -192,7 +192,7 @@ function LotGrid({ lots }: { lots: LotView[] }) { diff --git a/ApiCore-Frontend/src/app/(app)/medicines/page.tsx b/ApiCore-Frontend/src/app/(app)/medicines/page.tsx index 7642d7e..0bb68c4 100644 --- a/ApiCore-Frontend/src/app/(app)/medicines/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/medicines/page.tsx @@ -40,7 +40,7 @@ export default function MedicinesPage() { return (
: undefined} @@ -90,7 +90,7 @@ function MedicineGrid({ medicines }: { medicines: MedicineView[] }) { Deaktiviert} diff --git a/ApiCore-Frontend/src/app/(app)/nav-config.ts b/ApiCore-Frontend/src/app/(app)/nav-config.ts index 6ddbe31..861b03d 100644 --- a/ApiCore-Frontend/src/app/(app)/nav-config.ts +++ b/ApiCore-Frontend/src/app/(app)/nav-config.ts @@ -1,33 +1,5 @@ -import { - Activity, - BarChart3, - Box, - Boxes, - CalendarDays, - Crown, - Database, - Droplet, - Factory, - FlaskConical, - History, - Layers, - LayoutDashboard, - Lightbulb, - ListTodo, - type LucideIcon, - MapPin, - Mic, - Package, - Scissors, - Search, - ShieldCheck, - ShoppingBag, - Snowflake, - Users, - Warehouse, - Wrench, - Zap, -} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { AREAS, type AreaKey } from "@/lib/areas"; import type { AuthUser } from "@/lib/auth/auth-context"; /** @@ -36,6 +8,8 @@ import type { AuthUser } from "@/lib/auth/auth-context"; * can never be reachable on one screen size and missing on another. */ export interface NavItem { + /** The area this entry opens. */ + area: AreaKey; label: string; /** Short name for the cramped bottom nav. */ shortLabel?: string; @@ -51,6 +25,14 @@ export interface NavItem { counter?: "suggestions"; } +/** An entry for an area; the icon always comes from AREAS (one icon per area). */ +function navItem( + area: AreaKey, + props: Omit, +): NavItem { + return { area, icon: AREAS[area].icon, ...props }; +} + export interface NavGroup { label: string | null; items: NavItem[]; @@ -60,99 +42,101 @@ export const NAV_GROUPS: NavGroup[] = [ { label: null, items: [ - { + navItem("dashboard", { label: "Dashboard", shortLabel: "Start", href: "/dashboard", - icon: LayoutDashboard, - }, - { label: "Suche", href: "/search", icon: Search }, + }), + navItem("search", { label: "Suche", href: "/search" }), ], }, { label: "Betrieb", items: [ - { label: "Bienenstände", href: "/apiaries", icon: MapPin }, - { label: "Völker", href: "/colonies", icon: Layers }, - { label: "Königinnen", href: "/queens", icon: Crown }, - { + navItem("apiaries", { label: "Bienenstände", href: "/apiaries" }), + navItem("colonies", { label: "Völker", href: "/colonies" }), + navItem("queens", { label: "Königinnen", href: "/queens" }), + navItem("compliance", { label: "Prüfbereitschaft", href: "/compliance", - icon: ShieldCheck, + permission: "compliance:read", - }, - { + }), + navItem("winter", { label: "Überwinterung", href: "/winter", - icon: Snowflake, + permission: "winter-checks:read", - }, + }), ], }, { label: "Arbeit", items: [ - { label: "Aufgaben", href: "/tasks", icon: ListTodo }, - { + navItem("tasks", { label: "Aufgaben", href: "/tasks" }), + navItem("suggestions", { label: "Vorschläge", href: "/suggestions", - icon: Lightbulb, + permission: "tasks:read", counter: "suggestions", - }, - { label: "Kalender", href: "/calendar", icon: CalendarDays }, - { + }), + navItem("calendar", { label: "Kalender", href: "/calendar" }), + navItem("voice", { label: "Sprachnotizen", href: "/voice", - icon: Mic, + permission: "voice-captures:use", alsoRequires: ["inspections:create"], - }, + }), ], }, { label: "Ernte & Lager", items: [ - { label: "Ernten", href: "/harvests", icon: Scissors }, - { label: "Schleuderungen", href: "/extractions", icon: FlaskConical }, - { label: "Honigchargen", href: "/honey-batches", icon: Droplet }, - { label: "Abfüllungen", href: "/bottling", icon: Package }, - { + navItem("harvests", { label: "Ernten", href: "/harvests" }), + navItem("extractions", { label: "Schleuderungen", href: "/extractions" }), + navItem("honey-batches", { + label: "Honigchargen", + href: "/honey-batches", + }), + navItem("bottling", { label: "Abfüllungen", href: "/bottling" }), + navItem("raw-materials", { label: "Rohstoffe", href: "/raw-materials", - icon: Boxes, + permission: "raw-material:read", - }, - { + }), + navItem("production-batches", { label: "Herstellungschargen", href: "/production-batches", - icon: Factory, + permission: "production-batch:read", - }, - { label: "Produkte", href: "/products", icon: ShoppingBag }, - { label: "Lager", href: "/storage", icon: Warehouse }, - { label: "Inventar", href: "/equipment", icon: Wrench }, + }), + navItem("products", { label: "Produkte", href: "/products" }), + navItem("storage", { label: "Lager", href: "/storage" }), + navItem("equipment", { label: "Inventar", href: "/equipment" }), ], }, { label: "Auswertung", - items: [{ label: "Statistik", href: "/statistics", icon: BarChart3 }], + items: [navItem("statistics", { label: "Statistik", href: "/statistics" })], }, ]; export const ADMIN_GROUP: NavGroup = { label: "Verwaltung", items: [ - { + navItem("users-roles", { label: "Benutzer & Rollen", href: "/settings/users-roles", - icon: Users, + permission: ["users:read", "roles:read"], - }, - { + }), + navItem("automation", { label: "Automatisierung", href: "/settings/automation", - icon: Zap, + permission: [ "webhooks:read", "automation:read", @@ -162,31 +146,31 @@ export const ADMIN_GROUP: NavGroup = { "calendar:configure", "api-keys:manage", ], - }, - { + }), + navItem("hive-systems", { label: "Beutensysteme", href: "/settings/hive-systems", - icon: Box, + permission: "hive-systems:manage", - }, - { + }), + navItem("backup", { label: "Backup", href: "/settings/backup", - icon: Database, + permission: ["backup:export", "backup:import", "import:execute"], - }, - { + }), + navItem("audit-log", { label: "Audit-Log", href: "/settings/audit-log", - icon: History, + permission: "audit:read", - }, - { + }), + navItem("system", { label: "System", href: "/settings/system", - icon: Activity, + permission: "system:read", - }, + }), ], }; @@ -210,14 +194,18 @@ function isVisible( } /** All groups the user may see, without empty ones; admin group last. */ -export function visibleNavGroups(user: Pick): NavGroup[] { +export function visibleNavGroups( + user: Pick, +): NavGroup[] { return [...NAV_GROUPS, ADMIN_GROUP] .map((group) => ({ ...group, items: group.items.filter( (item) => isVisible(user, item.permission) && - (item.alsoRequires ?? []).every((key) => user.permissions.includes(key)), + (item.alsoRequires ?? []).every((key) => + user.permissions.includes(key), + ), ), })) .filter((group) => group.items.length > 0); @@ -226,7 +214,9 @@ export function visibleNavGroups(user: Pick): NavGroup[ /** The bottom nav entries, resolved from the config in their fixed order. */ export function bottomNavItems(): NavItem[] { const all = NAV_GROUPS.flatMap((group) => group.items); - return BOTTOM_NAV_HREFS.flatMap((href) => all.filter((item) => item.href === href)); + return BOTTOM_NAV_HREFS.flatMap((href) => + all.filter((item) => item.href === href), + ); } /** diff --git a/ApiCore-Frontend/src/app/(app)/offline/apiary/page.tsx b/ApiCore-Frontend/src/app/(app)/offline/apiary/page.tsx index 720b039..b91aaf2 100644 --- a/ApiCore-Frontend/src/app/(app)/offline/apiary/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/offline/apiary/page.tsx @@ -2,7 +2,6 @@ import { Suspense } from "react"; import { useSearchParams } from "next/navigation"; -import { MapPin } from "lucide-react"; import { useCurrentUserId } from "@/lib/auth/auth-context"; import type { StoredPack } from "@/lib/offline/db"; import { DetailHeader } from "@/components/detail-header"; @@ -74,7 +73,7 @@ function OfflineApiaryView({ stored, readAt, returnHref }: OfflineApiaryViewProp return ( } diff --git a/ApiCore-Frontend/src/app/(app)/offline/colony/page.tsx b/ApiCore-Frontend/src/app/(app)/offline/colony/page.tsx index 17c2e70..c8154ad 100644 --- a/ApiCore-Frontend/src/app/(app)/offline/colony/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/offline/colony/page.tsx @@ -4,7 +4,7 @@ import { Suspense } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; -import { Layers } from "lucide-react"; + import { useAuth, useCurrentUserId } from "@/lib/auth/auth-context"; import { OFFLINE_PACKS_QUERY_KEY, @@ -126,7 +126,7 @@ function OfflineColonyView({ stored, colony, readAt, returnHref }: OfflineColony return ( diff --git a/ApiCore-Frontend/src/app/(app)/production-batches/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/production-batches/[id]/page.tsx index d9b6db5..3f269cd 100644 --- a/ApiCore-Frontend/src/app/(app)/production-batches/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/production-batches/[id]/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; -import { Factory } from "lucide-react"; + import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { @@ -60,7 +60,7 @@ export default function ProductionBatchDetailPage() { return (
diff --git a/ApiCore-Frontend/src/app/(app)/production-batches/page.tsx b/ApiCore-Frontend/src/app/(app)/production-batches/page.tsx index a419271..201c95f 100644 --- a/ApiCore-Frontend/src/app/(app)/production-batches/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/production-batches/page.tsx @@ -32,7 +32,7 @@ export default function ProductionBatchesPage() { return (
{batch.lotNumber}} subtitle={batch.product.name} fields={[ diff --git a/ApiCore-Frontend/src/app/(app)/products/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/products/[id]/page.tsx index d5f49b7..f2b08c0 100644 --- a/ApiCore-Frontend/src/app/(app)/products/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/products/[id]/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useParams } from "next/navigation"; -import { ShoppingBag } from "lucide-react"; + import { useQuery } from "@tanstack/react-query"; import { fetchProduct } from "@/lib/products/api"; import { fetchProductionBatches } from "@/lib/production-batches/api"; @@ -54,7 +54,7 @@ export default function ProductDetailPage() { return (
diff --git a/ApiCore-Frontend/src/app/(app)/products/page.tsx b/ApiCore-Frontend/src/app/(app)/products/page.tsx index 3a10bbc..5f968bd 100644 --- a/ApiCore-Frontend/src/app/(app)/products/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/products/page.tsx @@ -32,7 +32,7 @@ export default function ProductsPage() { return (
( diff --git a/ApiCore-Frontend/src/app/(app)/profile/page.tsx b/ApiCore-Frontend/src/app/(app)/profile/page.tsx index 94b1340..f1c3357 100644 --- a/ApiCore-Frontend/src/app/(app)/profile/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/profile/page.tsx @@ -1,7 +1,7 @@ "use client"; import { ChangeEvent, FormEvent, useRef, useState } from "react"; -import { UserRound } from "lucide-react"; + import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; import { useAuth } from "@/lib/auth/auth-context"; @@ -297,10 +297,9 @@ export default function ProfilePage() { return (
diff --git a/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx index 6edb848..d595caf 100644 --- a/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx @@ -2,7 +2,7 @@ import { useParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; -import { Crown } from "lucide-react"; + import { fetchQueen } from "@/lib/queens/api"; import { QUEEN_STATUS_LABELS } from "@/lib/queens/labels"; import { QrCodeDialog } from "@/components/qr-code-dialog"; @@ -31,7 +31,7 @@ export default function QueenDetailPage() { return (
} diff --git a/ApiCore-Frontend/src/app/(app)/queens/new/page.tsx b/ApiCore-Frontend/src/app/(app)/queens/new/page.tsx index 6c79dc7..836bac7 100644 --- a/ApiCore-Frontend/src/app/(app)/queens/new/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/queens/new/page.tsx @@ -4,7 +4,7 @@ import { FormEvent, useState } from "react"; import { useRouter } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; -import { Crown } from "lucide-react"; + import { createQueen, fetchQueens } from "@/lib/queens/api"; import { useFormDraft } from "@/lib/forms/use-form-draft"; import { errorMessage } from "@/lib/forms/error-message"; @@ -121,10 +121,9 @@ export default function NewQueenPage() { draft={draft} /> {draft.pending ? ( }> + Neue Königin @@ -136,8 +135,7 @@ function QueenGrid({ queens }: { queens: Queen[] }) { , +): QuickAction { + return { area, icon: AREAS[area].icon, ...props }; +} + export const QUICK_ACTIONS: readonly QuickAction[] = [ - { + quickAction("inspections", { label: "Durchsicht", href: "/inspections/new", - icon: ClipboardCheck, + permission: "inspections:create", - }, - { + }), + quickAction("voice", { label: "Sprachnotiz", href: "/voice/new", - icon: Mic, + permission: "voice-captures:use", alsoPermission: "inspections:create", needsVoiceRecording: true, - }, - { + }), + quickAction("varroa", { label: "Varroa-Messung", href: "/varroa/new", - icon: Bug, + permission: "varroa:create", - }, - { + }), + quickAction("swarm-control", { label: "Schwarmkontrolle", href: "/swarm-control/new", - icon: Sprout, + permission: "swarm-control:create", - }, - { + }), + quickAction("treatments", { label: "Behandlung", href: "/treatments/new", - icon: Syringe, + permission: "treatments:create", - }, - { + }), + quickAction("feedings", { label: "Fütterung", href: "/feedings/new", - icon: Utensils, + permission: "feedings:create", - }, - { + }), + quickAction("tasks", { label: "Aufgabe", href: "/tasks/new", - icon: ListTodo, + permission: "tasks:create", - }, - { + }), + quickAction("harvests", { label: "Ernte", href: "/harvests/new", - icon: Scissors, + permission: "harvests:create", - }, + }), ]; diff --git a/ApiCore-Frontend/src/app/(app)/quick-add-sheet.spec.tsx b/ApiCore-Frontend/src/app/(app)/quick-add-sheet.spec.tsx index 73b6c1e..eef3da9 100644 --- a/ApiCore-Frontend/src/app/(app)/quick-add-sheet.spec.tsx +++ b/ApiCore-Frontend/src/app/(app)/quick-add-sheet.spec.tsx @@ -45,7 +45,7 @@ describe("QuickAddSheet", () => { const link = await screen.findByRole("link", { name: "Durchsicht" }); expect(link).toHaveAttribute("href", "/inspections/new"); expect(link).toHaveClass("min-h-14"); - expect(link.querySelector("svg.size-6")).not.toBeNull(); + expect(link.querySelector("[data-tone=erfassung] svg")).not.toBeNull(); await screen.findByRole("link", { name: "Sprachnotiz" }); expect(screen.getAllByRole("link")).toHaveLength(QUICK_ACTIONS.length); }); diff --git a/ApiCore-Frontend/src/app/(app)/quick-add-sheet.tsx b/ApiCore-Frontend/src/app/(app)/quick-add-sheet.tsx index b2a1cc6..61bcf16 100644 --- a/ApiCore-Frontend/src/app/(app)/quick-add-sheet.tsx +++ b/ApiCore-Frontend/src/app/(app)/quick-add-sheet.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { Plus } from "lucide-react"; +import { AreaIcon } from "@/components/area-icon"; import { Button } from "@/components/ui/button"; import { SheetTrigger } from "@/components/ui/sheet"; import type { AuthUser } from "@/lib/auth/auth-context"; @@ -84,7 +85,6 @@ export function QuickAddSheet({ user, variant }: QuickAddSheetProps) { >