Die Flaechenverteilung war verkehrt herum. Blau lag auf dem Groessten, was die Anmeldeseite zu vergeben hat, und Rosa nur in Andeutungen daneben — bei einer Marke, deren Schriftzug auf Rosa steht und deren Manual Rosa zuerst nennt. Sichtbar wurde es am Logo. Es bringt sein rosa Feld selbst mit, weil §1.2 den Schriftzug nur zur Gaenze auf Rosa zulaesst. Auf einer blauen Flaeche steht dieses Feld als ausgeschnittenes Rechteck darauf. Auf Weiss ist es dagegen richtig — §4.1 verlangt fuer weissen Untergrund genau das, ein rechteckiges rosa Feld. Falsch war also nicht das Logo, sondern die blaue Flaeche darunter. Rosa bekommen jetzt: das Panel der Anmeldeseite und das Band ueber der Anwendung aus Kopfzeile und Logokopf der Seitenleiste. Beide zusammen sind ein durchgehender Streifen, in dem das Logo aufgeht statt darauf zu liegen — was §3.1 mit "in einem grossflaechigen rosa Umfeld eingebettet" meint. Blau bleibt, was es ist: Knopf, Link, Fokusring, ausgewaehlter Eintrag. Das ist dasselbe Verhaeltnis, aus dem der Schriftzug besteht — blaue Lettern auf rosa Feld — und es ist das einzige, das traegt. Weiss auf Rosa sind 2.18:1. Auf den rosa Flaechen steht deshalb nichts Weisses und nichts Gedaempftes mehr. ink kommt auf 7.47:1, brand-700 auf 6.60:1; ink-muted lag bei 2.33:1 und ist aus Kopfzeile, Glocke und Logokopf verschwunden. Gestuft wird ueber Groesse und Gewicht statt ueber Transparenz — eine aufgehellte Schrift ist genau das, was auf dieser Flaeche durchfaellt. Das Raster auf dem Panel ist von Weiss auf ink gewechselt: auf Rosa war es nicht mehr zu sehen. Lint, Typen, Schemaabgleich, 552 Tests und der Build sind sauber. Im Browser nicht gesehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
173 lines
8.0 KiB
TypeScript
173 lines
8.0 KiB
TypeScript
"use client";
|
|
|
|
import { Bell, Users } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { completeEmployeeNote } from "@/actions/employees";
|
|
import { setNotizSichtbarkeit } from "@/actions/notes";
|
|
import { useToast } from "@/components/ui/Toast";
|
|
import { NOTE_CATEGORY_STYLES } from "@/lib/colors";
|
|
import { fmtDate } from "@/lib/format";
|
|
import type { OpenNote } from "@/lib/notes";
|
|
|
|
export type Kollege = { id: string; name: string; sichtbar: boolean };
|
|
|
|
// Click-outside mechanics borrowed from CountryPicker (useRef + mousedown
|
|
// listener) — without its draft-text reset, which has no equivalent here.
|
|
export function NotesBell({ notes, kollegen }: { notes: OpenNote[]; kollegen: Kollege[] }) {
|
|
const { showToast } = useToast();
|
|
const router = useRouter();
|
|
const [open, setOpen] = useState(false);
|
|
const [completingId, setCompletingId] = useState<string | null>(null);
|
|
const [zeigeEinstellung, setZeigeEinstellung] = useState(false);
|
|
// Der Haken springt sofort um und wird zurückgedreht, wenn das Speichern
|
|
// scheitert. Ohne das fühlt sich jeder Klick wie eine halbe Sekunde
|
|
// Bedenkzeit an — die Liste wird beim Durchgehen mehrfach angetippt.
|
|
const [eigen, setEigen] = useState<Record<string, boolean>>({});
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const sichtbarkeit = (k: Kollege) => eigen[k.id] ?? k.sichtbar;
|
|
const dazugewaehlt = kollegen.filter((k) => sichtbarkeit(k)).length;
|
|
|
|
async function schalte(k: Kollege) {
|
|
const neu = !sichtbarkeit(k);
|
|
setEigen((v) => ({ ...v, [k.id]: neu }));
|
|
const result = await setNotizSichtbarkeit({ kollegeId: k.id, sichtbar: neu });
|
|
if (result.success) {
|
|
router.refresh();
|
|
} else {
|
|
setEigen((v) => ({ ...v, [k.id]: !neu }));
|
|
showToast(result.error ?? "Fehler beim Speichern.", "error");
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
function onClickOutside(e: MouseEvent) {
|
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", onClickOutside);
|
|
return () => document.removeEventListener("mousedown", onClickOutside);
|
|
}, []);
|
|
|
|
async function handleComplete(note: OpenNote) {
|
|
setCompletingId(note.id);
|
|
const result = await completeEmployeeNote({ note_id: note.id, employee_id: note.employee_id });
|
|
setCompletingId(null);
|
|
if (result.success) {
|
|
showToast("Notiz erledigt.");
|
|
router.refresh();
|
|
} else {
|
|
showToast(result.error ?? "Fehler beim Speichern.", "error");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div ref={containerRef} className="relative">
|
|
{/* ink statt ink-muted: die Glocke sitzt im rosa Markenband der Kopfzeile,
|
|
und ink-muted kommt darauf nur auf 2.33:1. */}
|
|
<button type="button" onClick={() => setOpen((o) => !o)} aria-label="Meine Notizen" className="relative rounded p-1.5 text-ink hover:bg-accent-100">
|
|
<Bell className="h-4 w-4" />
|
|
{notes.length > 0 && (
|
|
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-danger-solid px-1 text-[10px] font-bold text-white">
|
|
{notes.length}
|
|
</span>
|
|
)}
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="absolute right-0 z-20 mt-2 max-h-[28rem] w-96 overflow-y-auto rounded border border-border bg-white shadow-[var(--shadow-card-hover)]">
|
|
<div className="flex items-center justify-between gap-2 border-b border-border px-4 py-2.5">
|
|
<span className="text-xs font-bold uppercase tracking-wide text-ink-muted">
|
|
Meine Notizen ({notes.length})
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setZeigeEinstellung((v) => !v)}
|
|
aria-expanded={zeigeEinstellung}
|
|
className={`flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold hover:bg-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500 ${
|
|
zeigeEinstellung ? "text-brand-700" : "text-ink-muted"
|
|
}`}
|
|
>
|
|
<Users className="h-3.5 w-3.5" />
|
|
Kolleg:innen
|
|
{/* Nur wenn jemand dazugewählt ist: eine „0" bei jedem
|
|
Aufschlagen zu lesen, sagt nichts. */}
|
|
{dazugewaehlt > 0 && <span className="text-brand-700">+{dazugewaehlt}</span>}
|
|
</button>
|
|
</div>
|
|
|
|
{zeigeEinstellung && (
|
|
<div className="border-b border-border bg-surface px-4 py-3">
|
|
{/* Dass die Auswahl auch die Entwürfe auf der Übersicht
|
|
steuert, steht hier und nicht nur dort: sie wird hier
|
|
getroffen, und eine Wirkung, die zwei Klicks entfernt
|
|
sichtbar wird, sucht sonst niemand. */}
|
|
<p className="mb-2 text-xs text-ink-muted">
|
|
Wessen Notizen hier und wessen Entwürfe auf der Übersicht zusätzlich erscheinen. Die
|
|
eigenen sind immer dabei.
|
|
</p>
|
|
{kollegen.length === 0 ? (
|
|
<p className="py-2 text-sm text-ink-muted">Keine weiteren HR-Kolleg:innen freigeschaltet.</p>
|
|
) : (
|
|
<ul className="flex flex-col gap-1">
|
|
{kollegen.map((k) => (
|
|
<li key={k.id}>
|
|
<label className="flex cursor-pointer items-center gap-2 rounded px-1 py-1 text-sm hover:bg-white">
|
|
<input
|
|
type="checkbox"
|
|
checked={sichtbarkeit(k)}
|
|
onChange={() => schalte(k)}
|
|
className="h-4 w-4 rounded border-border text-brand-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500"
|
|
/>
|
|
<span className={sichtbarkeit(k) ? "text-ink" : "text-ink-muted line-through"}>{k.name}</span>
|
|
</label>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{notes.length === 0 ? (
|
|
<p className="px-4 py-6 text-center text-sm text-ink-muted">Keine offenen Notizen.</p>
|
|
) : (
|
|
<ul className="divide-y divide-border">
|
|
{notes.map((n) => (
|
|
<li key={n.id} className="px-4 py-3">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className={`rounded-full px-2 py-0.5 text-xs font-semibold ${NOTE_CATEGORY_STYLES[n.category]}`}>{n.category}</span>
|
|
<Link href={`/employees/${n.employee_id}`} className="text-sm font-semibold text-ink hover:text-brand-700 hover:underline">
|
|
{n.employeeName}
|
|
</Link>
|
|
<span className="text-xs text-ink-muted">{fmtDate(n.created_at)}</span>
|
|
</div>
|
|
<p className="mt-1 text-sm text-ink-body">{n.note_text}</p>
|
|
{/* Von wem die Notiz stammt. Sichtbar sind die eigenen und
|
|
die hinzugewählten — ohne diese Zeile wäre nicht zu
|
|
unterscheiden, welche davon man gerade liest. „Ich"
|
|
statt des eigenen Namens: den kennt man. */}
|
|
<p className="mt-1 text-xs text-ink-muted">
|
|
{n.vonMir ? "von mir" : `von ${n.author_name}`}
|
|
</p>
|
|
{n.due_date && <p className="mt-1 text-xs text-warning-text">🔔 fällig {fmtDate(n.due_date)}</p>}
|
|
<button
|
|
type="button"
|
|
onClick={() => handleComplete(n)}
|
|
disabled={completingId === n.id}
|
|
className="mt-2 text-xs font-semibold text-success-text hover:underline disabled:opacity-50"
|
|
>
|
|
✓ Erledigt
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|