feat: Oberflaechenbausteine, Anwendungsrahmen und Konventionen
Basisbausteine im shadcn/ui-Stil, aber mit drei bewussten Abweichungen: - Bedienelemente sind 44px hoch statt 36px. Ich bediene die App oft am Handy im Heim oder auf dem Amt; kleiner ist dort nicht treffsicher. - Auswahlfelder benutzen das native <select>. Android und iOS oeffnen dafuer ihre eigene Liste - schneller als jeder Nachbau, und mit Screenreader und Tastatur funktioniert es ohne Zutun. - Eingabefelder haben mindestens 16px Schrift, sonst zoomt iOS-Safari beim Hineintippen das Formular heran. <Feld> verdrahtet Beschriftung, Erklaerung und Fehler ueber aria-describedby. Der Erklaerungstext ist kein Beiwerk: die Anwendung benutzt Begriffe aus dem Betreuungsrecht, die niemand nachschlagen soll. <Rechtshinweis> ist der stehende Vermerk, dass hier keine Rechtsberatung stattfindet. Er steht an jeder Stelle mit Paragraphenbezug. Der Anwendungsrahmen fuehrt drei Bedingungen zusammen, ohne die niemand an Fachdaten kommt: angemeldet, zweiter Faktor eingerichtet, mindestens eine betreute Person zugeordnet. Die Navigation zeigt Zaehler fuer ueberfaellige Fristen und neuen Posteingang - die Frage "wo muss ich hin?" wird schon im Menue beantwortet. Das Farbschema wird per Inline-Skript mit CSP-Nonce gesetzt, bevor die Seite gezeichnet wird - sonst blitzt beim Laden das helle Thema auf. docs/KONVENTIONEN.md haelt fest, was in allen weiteren Etappen gilt: Verzeichnisaufbau, Cent-Betraege, Umgang mit verschluesselten Feldern, Zugriffspruefung, Audit-Pflicht, Ampelfarben, Commit-Trailer. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import { AppHuelle, SchnellerfassungKnopf } from '@/components/app-huelle'
|
||||
import { BenutzerMenue } from '@/components/benutzer-menue'
|
||||
import { BetreuteAuswahl } from '@/components/betreute-auswahl'
|
||||
import { NAV_STRUKTUR, type NavEintrag } from '@/components/navigation'
|
||||
import { aktiveBetreute, erfordereBenutzer, erfordereZweitenFaktor, meineBetreuten } from '@/lib/auth/sitzung'
|
||||
import { darf } from '@/lib/auth/rollen'
|
||||
import { ROLLEN_BESCHRIFTUNG } from '@/lib/auth/rollen'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
|
||||
/**
|
||||
* Rahmen aller Fachseiten.
|
||||
*
|
||||
* Hier laufen die drei Bedingungen zusammen, ohne die niemand an Daten
|
||||
* kommt: angemeldet, zweiter Faktor eingerichtet, mindestens eine betreute
|
||||
* Person zugeordnet.
|
||||
*/
|
||||
export default async function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
const benutzer = await erfordereBenutzer()
|
||||
await erfordereZweitenFaktor(benutzer)
|
||||
|
||||
const betreute = await meineBetreuten(benutzer)
|
||||
if (betreute.length === 0) {
|
||||
// Erststart: es gibt noch keine Stammdaten.
|
||||
redirect('/ersteinrichtung')
|
||||
}
|
||||
|
||||
const aktive = (await aktiveBetreute(benutzer))!
|
||||
|
||||
// Zugang der aktiven Person bestimmt, welche Menuepunkte sichtbar sind.
|
||||
const eintrag = await db.betreuungsZugang.findUnique({
|
||||
where: { userId_betreuteId: { userId: benutzer.id, betreuteId: aktive.id } },
|
||||
select: { rolle: true, freigegebeneModule: true },
|
||||
})
|
||||
const zugang = eintrag
|
||||
? { rolle: eintrag.rolle, freigegebeneModule: eintrag.freigegebeneModule }
|
||||
: benutzer.rolle === 'ADMIN'
|
||||
? { rolle: 'ADMIN' as const, freigegebeneModule: [] }
|
||||
: null
|
||||
|
||||
// Zaehler fuer die Navigation: was ist ueberfaellig, was liegt neu im
|
||||
// Posteingang? Beides beantwortet "wo muss ich hin?" schon im Menue.
|
||||
const heute = new Date()
|
||||
heute.setHours(0, 0, 0, 0)
|
||||
|
||||
const [ueberfaellig, neuImPosteingang] = await Promise.all([
|
||||
db.frist.count({
|
||||
where: { betreuteId: aktive.id, status: 'OFFEN', datum: { lt: heute }, ...nichtGeloescht },
|
||||
}),
|
||||
db.dokument.count({
|
||||
where: { betreuteId: aktive.id, posteingangStatus: 'NEU', ...nichtGeloescht },
|
||||
}),
|
||||
])
|
||||
|
||||
const navigation: NavEintrag[] = NAV_STRUKTUR.filter((e) => darf(zugang, e.modul, 'lesen')).map(
|
||||
(e) => ({
|
||||
...e,
|
||||
zaehler:
|
||||
e.href === '/fristen' ? ueberfaellig : e.href === '/dokumente' ? neuImPosteingang : undefined,
|
||||
dringend: e.href === '/fristen',
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<AppHuelle
|
||||
navigation={navigation}
|
||||
kopfzeile={
|
||||
<BetreuteAuswahl
|
||||
aktiveId={aktive.id}
|
||||
betreute={betreute.map((b) => ({
|
||||
id: b.id,
|
||||
vorname: b.vorname,
|
||||
nachname: b.nachname,
|
||||
geburtsdatum: b.geburtsdatum ? b.geburtsdatum.toISOString() : null,
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
benutzerbereich={
|
||||
<BenutzerMenue
|
||||
name={benutzer.name}
|
||||
email={benutzer.email}
|
||||
rolle={ROLLEN_BESCHRIFTUNG[benutzer.rolle]}
|
||||
/>
|
||||
}
|
||||
schnellerfassung={
|
||||
<SchnellerfassungKnopf
|
||||
eintraege={[
|
||||
{ titel: 'Beleg fotografieren', href: '/dokumente/neu?quelle=kamera' },
|
||||
{ titel: 'Notiz / Kontakt festhalten', href: '/kontakte/protokoll/neu' },
|
||||
{ titel: 'Frist anlegen', href: '/fristen/neu' },
|
||||
{ titel: 'Buchung erfassen', href: '/finanzen/buchungen/neu' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</AppHuelle>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user