Files
alpenwerk-hr/components/shell/AppShell.tsx
Maximilian Stubhan 99b1df9735
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m49s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m24s
Choose whose notes reach your bell
The bell is a shared pile: every active HR person sees every open note,
regardless of who wrote it. That was agreed and it stays the default —
this narrows it, it never widens it. You can now untick colleagues whose
notes you do not want to see.

What gets stored is the *exceptions*, not the selection. The difference
shows the day someone new joins HR: had the selection been stored, she
would be invisible to everyone until each person ticked her, and nobody
would notice her follow-ups piling up. This way she is visible from day
one and hiding her is a deliberate act. Same reasoning that made notes a
shared inbox in the first place — the silent gap is worse than a row too
many.

Own notes always come through: `note_mutes` rejects a self-reference, and
the predicate says so again rather than depending on a check constraint
staying put. Notes with no author come through too — hiding one because
nobody knows who wrote it is exactly the loss this list exists to prevent.

The rule lives in lib/notes.ts as one SQL expression because two places
need it: the bell in the header and the "Anstehend" card on the dashboard.
Two copies drift, and then the card counts something the bell does not
show.

No SQL function and no audit row, unlike anything that touches employee
data — this is a personal display preference, and an audit trail recording
every tick would make finding real changes harder. Same pattern as saved
reports and hire drafts, and the owner policy on note_mutes means a row
for someone else cannot be written even with invented values.

The checkbox flips immediately and flips back if saving fails; the list
gets clicked through several at a time and a round trip per tick feels
like hesitation.

Verified: 19 tests, five mutation-checked (or→and, dropping the own-notes
clause, inverting `not exists`, inverting the default, and losing the
email fallback each turn them red). Typecheck, lint, schema drift, 477
tests and the build are clean. Not seen in a browser: login goes through
the company account and the database is unreachable — the migration is
reviewed but has not been run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 20:03:24 +02:00

49 lines
1.9 KiB
TypeScript

"use client";
import { useCallback, useState, type ReactNode } from "react";
import type { OpenNote } from "@/lib/notes";
import { Sidebar } from "./Sidebar";
import { Topbar } from "./Topbar";
// Owns the one piece of state the shell needs (is the mobile drawer open),
// so app/(app)/layout.tsx can stay a Server Component and keep doing its
// auth check and data loading on the server.
export function AppShell({
userLabel,
openNotes,
kollegen,
children,
}: {
userLabel: string;
openNotes: OpenNote[];
kollegen: { id: string; name: string; sichtbar: boolean }[];
children: ReactNode;
}) {
const [navOpen, setNavOpen] = useState(false);
const closeNav = useCallback(() => setNavOpen(false), []);
// Auf Papier gehört von der Hülle nichts hin: Navigation, Glocke und
// Abmelde-Knopf sind Bedienelemente, und gedruckt sind sie bestenfalls
// Zierrat — auf einer Checkliste, die in eine Personalakte geht, sogar
// irreführend. Die Regel steht hier und nicht auf der einzelnen Druckseite,
// damit jede weitere sie mitbekommt, statt sie zu wiederholen.
//
// Auch die Ränder fallen weg: die Satzfläche bestimmt @page auf der
// Druckseite selbst, und die Polsterung der Hülle käme obendrauf.
return (
<div className="min-h-dvh print:min-h-0">
<div className="print:hidden">
<Sidebar open={navOpen} onClose={closeNav} />
</div>
<div className="flex min-h-dvh flex-col lg:ml-[236px] print:ml-0 print:block print:min-h-0">
<div className="print:hidden">
<Topbar userLabel={userLabel} openNotes={openNotes} kollegen={kollegen} onOpenNav={() => setNavOpen(true)} />
</div>
<main className="flex-1 px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] sm:px-6 sm:py-6 print:p-0">
<div className="mx-auto w-full max-w-[1280px] print:max-w-none">{children}</div>
</main>
</div>
</div>
);
}