feat: platform backbone and field capture, first part (checkpoint)
Platform backbone (plan 2026-10-13-plattform-rueckgrat), reviewed tasks: - run roles api/worker/job/all with shared startup checks (Task 1) - schema and migration for DomainEvent, deliveries, JobRun, ThrottlerHit (Task 2) - audit, notifications and domain events written in the same transaction (Task 3) - dispatcher with SKIP LOCKED claims, retries and stale release (Task 4) - worker building blocks: tick, LISTEN/NOTIFY wake-up, health (Task 5) - worker process and entry points (Task 6) - throttler counters in Postgres (Task 19) - separate keys per purpose with key ring (Task 20) - public origin page without exact location (Task 22) - shop keys split into read and write (Task 23) - Kustomize base, components, overlay encry, CI validation (Task 26) Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), reviewed tasks: - brood frames, read endpoints, apiary overview and offline pack - network basics, offline queue v3, navigation sheets, form building blocks - new inspection with last-time hints and weather, varroa per day, treatment prefill, apiary page with round - inspection photo (Task 10) is implemented, review pending Not rolled out. Remaining tasks follow in later commits. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
10cbe9bf2d
commit
c01a38835a
340 files changed
+25695
-1808
No files matched your search
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
BarChart3,
|
||||
Boxes,
|
||||
CalendarDays,
|
||||
Crown,
|
||||
Database,
|
||||
Droplet,
|
||||
Factory,
|
||||
FlaskConical,
|
||||
History,
|
||||
Layers,
|
||||
LayoutDashboard,
|
||||
ListTodo,
|
||||
type LucideIcon,
|
||||
MapPin,
|
||||
Package,
|
||||
Scissors,
|
||||
Search,
|
||||
ShoppingBag,
|
||||
Snowflake,
|
||||
Users,
|
||||
Warehouse,
|
||||
Wrench,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import type { AuthUser } from "@/lib/auth/auth-context";
|
||||
|
||||
/**
|
||||
* The single source of truth for every area of the app: the desktop sidebar,
|
||||
* the phone bottom nav and its „Mehr" sheet all read from here, so an area
|
||||
* can never be reachable on one screen size and missing on another.
|
||||
*/
|
||||
export interface NavItem {
|
||||
label: string;
|
||||
/** Short name for the cramped bottom nav. */
|
||||
shortLabel?: string;
|
||||
href: string;
|
||||
icon: LucideIcon;
|
||||
// Any one of these grants visibility - used for hub pages that fold
|
||||
// together several previously-separate settings pages, each gated by its
|
||||
// own permission.
|
||||
permission?: string | string[];
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
label: string | null;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
export const NAV_GROUPS: NavGroup[] = [
|
||||
{
|
||||
label: null,
|
||||
items: [
|
||||
{
|
||||
label: "Dashboard",
|
||||
shortLabel: "Start",
|
||||
href: "/dashboard",
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{ label: "Suche", href: "/search", icon: 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 },
|
||||
{
|
||||
label: "Überwinterung",
|
||||
href: "/winter",
|
||||
icon: Snowflake,
|
||||
permission: "winter-checks:read",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Arbeit",
|
||||
items: [
|
||||
{ label: "Aufgaben", href: "/tasks", icon: ListTodo },
|
||||
{ label: "Kalender", href: "/calendar", icon: CalendarDays },
|
||||
],
|
||||
},
|
||||
{
|
||||
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 },
|
||||
{
|
||||
label: "Rohstoffe",
|
||||
href: "/raw-materials",
|
||||
icon: Boxes,
|
||||
permission: "raw-material:read",
|
||||
},
|
||||
{
|
||||
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 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Auswertung",
|
||||
items: [{ label: "Statistik", href: "/statistics", icon: BarChart3 }],
|
||||
},
|
||||
];
|
||||
|
||||
export const ADMIN_GROUP: NavGroup = {
|
||||
label: "Verwaltung",
|
||||
items: [
|
||||
{
|
||||
label: "Benutzer & Rollen",
|
||||
href: "/settings/users-roles",
|
||||
icon: Users,
|
||||
permission: ["users:read", "roles:read"],
|
||||
},
|
||||
{
|
||||
label: "Automatisierung",
|
||||
href: "/settings/automation",
|
||||
icon: Zap,
|
||||
permission: [
|
||||
"webhooks:read",
|
||||
"automation:read",
|
||||
"task-templates:read",
|
||||
"label-templates:read",
|
||||
"site-settings:read",
|
||||
"calendar:configure",
|
||||
"api-keys:manage",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Backup",
|
||||
href: "/settings/backup",
|
||||
icon: Database,
|
||||
permission: ["backup:export", "backup:import", "import:execute"],
|
||||
},
|
||||
{
|
||||
label: "Audit-Log",
|
||||
href: "/settings/audit-log",
|
||||
icon: History,
|
||||
permission: "audit:read",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** The four favourites on the phone; everything else sits behind „Mehr". */
|
||||
export const BOTTOM_NAV_HREFS = [
|
||||
"/dashboard",
|
||||
"/colonies",
|
||||
"/tasks",
|
||||
"/search",
|
||||
] as const;
|
||||
|
||||
function isVisible(
|
||||
user: Pick<AuthUser, "permissions">,
|
||||
permission: NavItem["permission"],
|
||||
): boolean {
|
||||
if (!permission) {
|
||||
return true;
|
||||
}
|
||||
const required = Array.isArray(permission) ? permission : [permission];
|
||||
return required.some((key) => user.permissions.includes(key));
|
||||
}
|
||||
|
||||
/** All groups the user may see, without empty ones; admin group last. */
|
||||
export function visibleNavGroups(user: Pick<AuthUser, "permissions">): NavGroup[] {
|
||||
return [...NAV_GROUPS, ADMIN_GROUP]
|
||||
.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter((item) => isVisible(user, item.permission)),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0);
|
||||
}
|
||||
|
||||
/** 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a nav entry is the current area. Matches on a path boundary, so
|
||||
* `/queens` does not light up for `/queens-archive`.
|
||||
*/
|
||||
export function isNavActive(pathname: string, href: string): boolean {
|
||||
return pathname === href || pathname.startsWith(`${href}/`);
|
||||
}
|
||||
Reference in new issue
Block a user