feat: Sprint 4 complete — frontend MVP (admin dashboard + member portal)

Shadboard starter-kit (Next.js 15 + React 19 + shadcn/ui + Tailwind 4)

Sprint 4.a — Admin Dashboard:
- Auth: NextAuth.js v5, login page, middleware, token rotation
- Dashboard: KPI cards, Recharts stock chart, quick actions
- Members: TanStack Table (search/sort/paginate), add/edit forms
- Distributions: multi-step form, real-time quota check, history
- Stock: batch management, recall dialog, bar chart
- Reports: monthly/member-list/recall, PDF/CSV download, preview

Sprint 4.b — Member Portal:
- Separate route group with top-nav layout (mobile-first)
- Quota dashboard with radial SVG progress indicators
- Distribution history with month filter
- Profile/settings with password change

Cross-cutting:
- i18n: German (default) + English via next-intl
- Dark + light mode (next-themes, user-togglable)
- Playwright E2E tests (6/6 green)
- Docker multi-stage build (node:22-alpine)
- API proxy via Next.js rewrites

Tech: Next.js 15.2.8, React 19, Tailwind 4, NextAuth v5,
TanStack Table, Recharts, Zod, React Hook Form, Playwright
This commit is contained in:
Patrick Plate
2026-06-12 17:18:38 +02:00
parent a1d4ba44e3
commit fe6e96dd3f
143 changed files with 23568 additions and 0 deletions
+296
View File
@@ -0,0 +1,296 @@
{
"common": {
"appName": "CannaManage",
"loading": "Laden...",
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"create": "Erstellen",
"search": "Suchen",
"filter": "Filtern",
"export": "Exportieren",
"back": "Zurück",
"next": "Weiter",
"confirm": "Bestätigen",
"yes": "Ja",
"no": "Nein",
"noData": "Keine Daten vorhanden"
},
"nav": {
"dashboard": "Dashboard",
"members": "Mitglieder",
"stock": "Bestand",
"distributions": "Ausgaben",
"compliance": "Compliance",
"reports": "Berichte",
"settings": "Einstellungen",
"staff": "Personal",
"portal": "Mitgliederportal"
},
"auth": {
"login": "Anmelden",
"logout": "Abmelden",
"email": "E-Mail-Adresse",
"password": "Passwort",
"forgotPassword": "Passwort vergessen?",
"resetPassword": "Passwort zurücksetzen",
"loginButton": "Anmelden",
"loggingIn": "Wird angemeldet...",
"loginSubtitle": "Melde dich bei deinem Anbauverein an",
"invalidCredentials": "Ungültige E-Mail-Adresse oder Passwort.",
"networkError": "Verbindungsfehler. Bitte versuche es erneut.",
"sessionExpired": "Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.",
"emailInvalid": "Bitte gib eine gültige E-Mail-Adresse ein.",
"passwordRequired": "Bitte gib dein Passwort ein.",
"passwordTooShort": "Passwort muss mindestens 8 Zeichen lang sein.",
"footerText": "Sichere Verwaltung für deinen Cannabis-Anbauverein"
},
"dashboard": {
"title": "Dashboard",
"activeMembers": "Aktive Mitglieder",
"distributionsToday": "Ausgaben heute",
"stockLevel": "Lagerbestand",
"monthlyQuota": "Monatliches Kontingent",
"quickActions": "Schnellaktionen",
"newDistribution": "Neue Ausgabe",
"addMember": "Mitglied hinzufügen",
"recentDistributions": "Letzte Ausgaben",
"stockByStrain": "Bestand nach Sorte",
"date": "Datum",
"member": "Mitglied",
"strain": "Sorte",
"amount": "Menge (g)",
"staff": "Personal",
"grams": "g",
"today": "Heute",
"trend": "+{value}% ggü. Vormonat",
"quotaUsed": "{value}% verbraucht",
"distributionCount": "{count} Ausgaben, {grams}g"
},
"members": {
"title": "Mitgliederverwaltung",
"addMember": "Mitglied hinzufügen",
"name": "Name",
"email": "E-Mail",
"status": "Status",
"memberSince": "Mitglied seit",
"quota": "Kontingent",
"actions": "Aktionen",
"edit": "Bearbeiten",
"active": "Aktiv",
"suspended": "Gesperrt",
"expelled": "Ausgeschlossen",
"back": "Zurück zur Liste",
"save": "Speichern",
"create": "Mitglied anlegen",
"firstName": "Vorname",
"lastName": "Nachname",
"dateOfBirth": "Geburtsdatum",
"phone": "Telefon",
"memberNumber": "Mitgliedsnummer",
"joinedAt": "Beitrittsdatum",
"notes": "Notizen",
"notesPlaceholder": "Optionale Anmerkungen zum Mitglied...",
"under21Warning": "Unter 21 — reduziertes Kontingent (30g/Monat)",
"ageError": "Mitglieder müssen mindestens 18 Jahre alt sein.",
"saved": "Änderungen gespeichert.",
"created": "Mitglied erfolgreich angelegt.",
"search": "Name oder E-Mail suchen...",
"perPage": "Pro Seite",
"showing": "{from}{to} von {total}",
"previous": "Zurück",
"next": "Weiter",
"noResults": "Keine Mitglieder gefunden.",
"notFound": "Mitglied nicht gefunden.",
"personalInfo": "Persönliche Daten",
"membershipInfo": "Mitgliedschaft"
},
"stock": {
"title": "Lager & Chargen",
"newBatch": "Neue Charge",
"stockOverview": "Bestandsübersicht",
"batchId": "Chargen-ID",
"strain": "Sorte",
"thc": "THC %",
"cbd": "CBD %",
"status": "Status",
"available": "Verfügbar",
"availableGrams": "Verfügbar (g)",
"receivedAt": "Eingangsdatum",
"actions": "Aktionen",
"statusAvailable": "Verfügbar",
"statusRecalled": "Rückruf",
"statusDepleted": "Aufgebraucht",
"recall": "Rückruf",
"recallConfirm": "Charge wirklich zurückrufen? Alle offenen Ausgaben mit dieser Charge werden blockiert.",
"recallTitle": "Charge zurückrufen",
"recallSuccess": "Charge zurückgerufen.",
"totalBatches": "Chargen gesamt",
"availableStock": "Verfügbarer Bestand",
"recalledBatches": "Zurückgerufene Chargen",
"strainCount": "Sorten",
"filterAll": "Alle",
"filterAvailable": "Nur verfügbar",
"filterRecalled": "Nur Rückrufe",
"addBatch": "Charge anlegen",
"strainName": "Sortenname",
"amount": "Menge (g)",
"supplier": "Lieferant / Herkunft",
"harvestDate": "Erntedatum",
"notes": "Notizen",
"notesPlaceholder": "Optionale Bemerkungen zur Charge...",
"created": "Charge erfolgreich angelegt.",
"grams": "g",
"confirmRecall": "Rückruf bestätigen",
"lowStock": "Niedrig"
},
"distributions": {
"title": "Ausgaben",
"newDistribution": "Neue Ausgabe",
"todaySummary": "Heute: {count} Ausgaben, {grams}g verteilt",
"dateTime": "Datum/Uhrzeit",
"member": "Mitglied",
"strain": "Sorte",
"amount": "Menge (g)",
"staff": "Personal",
"status": "Status",
"completed": "Abgeschlossen",
"locked": "Gesperrt (unveränderbar)",
"filterToday": "Heute",
"filterWeek": "Diese Woche",
"filterMonth": "Diesen Monat",
"searchMember": "Mitglied suchen...",
"step1": "Mitglied auswählen",
"step2": "Kontingent prüfen",
"step3": "Sorte & Menge",
"step4": "Bestätigung",
"selectMember": "Mitglied suchen (Name oder Nummer)...",
"memberBlocked": "Mitglied ist gesperrt — keine Ausgabe möglich.",
"under21Info": "Reduziertes Kontingent: 30g/Monat (unter 21)",
"dailyRemaining": "Tagesrest",
"monthlyRemaining": "Monatsrest",
"selectBatch": "Charge auswählen",
"available": "verfügbar",
"amountLabel": "Menge in Gramm",
"exceedsDaily": "Überschreitet das Tageslimit ({limit}g).",
"exceedsMonthly": "Überschreitet das Monatslimit ({limit}g).",
"exceedsBatch": "Nicht genügend Bestand in dieser Charge.",
"confirm": "Ausgabe bestätigen",
"summary": "Zusammenfassung",
"success": "Ausgabe erfolgreich erfasst.",
"grams": "g",
"date": "Datum",
"monthlyQuota": "Monatsquote",
"remaining": "Verbleibend"
},
"reports": {
"title": "Berichte",
"monthly": "Monatsbericht",
"monthlyDesc": "Übersicht aller Ausgaben im gewählten Monat, inkl. Mitglieder-Kontingente und Lagerveränderungen.",
"memberList": "Mitgliederliste",
"memberListDesc": "Vollständige Mitgliederliste mit Status, Kontingent-Auslastung und Kontaktdaten.",
"recall": "Rückruf-Bericht",
"recallDesc": "Alle Chargen mit Rückruf-Status und betroffene Ausgaben für Behörden-Meldung.",
"downloadPdf": "Als PDF herunterladen",
"downloadCsv": "Als CSV herunterladen",
"preview": "Vorschau anzeigen",
"generating": "Bericht wird generiert...",
"downloaded": "{name} heruntergeladen.",
"selectMonth": "Monat wählen",
"selectStatus": "Status filtern",
"allStatuses": "Alle",
"activeOnly": "Aktiv",
"suspendedOnly": "Gesperrt",
"dateFrom": "Von",
"dateTo": "Bis",
"previewTitle": "Berichts-Vorschau",
"totalDistributions": "Ausgaben gesamt",
"totalGrams": "Gramm gesamt",
"uniqueMembers": "Verschiedene Mitglieder",
"averagePerMember": "Ø pro Mitglied",
"topStrains": "Top-Sorten",
"affectedDistributions": "Betroffene Ausgaben",
"affectedMembers": "Betroffene Mitglieder",
"recalledBatches": "Zurückgerufene Chargen",
"close": "Schließen",
"complianceNote": "Dieser Bericht ist für die Vorlage bei der zuständigen Behörde geeignet.",
"complianceBadge": "§19 KCanG konform",
"auditTrail": "Alle Berichte werden mit Zeitstempel generiert. Die zugrunde liegenden Ausgabe-Daten sind unveränderbar (Audit-Trail).",
"memberNumber": "Nr.",
"name": "Name",
"status": "Status",
"joinedAt": "Beitritt",
"usage": "Verbrauch",
"strain": "Sorte",
"grams": "Gramm",
"percent": "Anteil",
"batchId": "Chargen-ID",
"recalledAt": "Rückruf am",
"reason": "Grund",
"distributed": "Verteilt",
"original": "Original"
},
"portal": {
"title": "Mein Bereich",
"login": "Mitglieder-Login",
"loginSubtitle": "Melde dich im Mitgliederportal an",
"email": "E-Mail-Adresse",
"password": "Passwort",
"loginButton": "Anmelden",
"loggingIn": "Wird angemeldet...",
"invalidCredentials": "Ungültige E-Mail-Adresse oder Passwort.",
"networkError": "Verbindungsfehler. Bitte versuche es erneut.",
"welcome": "Willkommen zurück, {name}!",
"dashboard": "Übersicht",
"quota": "Mein Kontingent",
"history": "Ausgabe-Verlauf",
"profile": "Profil",
"settings": "Einstellungen",
"logout": "Abmelden",
"dailyQuota": "Tageskontingent",
"monthlyQuota": "Monatskontingent",
"remaining": "verbleibend",
"used": "verbraucht",
"of": "von",
"lastDistribution": "Letzte Ausgabe",
"noDistributions": "Noch keine Ausgaben in diesem Monat.",
"memberSince": "Mitglied seit",
"memberNumber": "Mitgliedsnummer",
"nextAvailable": "Nächste Verfügbarkeit",
"nextAvailableTomorrow": "Morgen ab 00:00 Uhr",
"changePassword": "Passwort ändern",
"currentPassword": "Aktuelles Passwort",
"newPassword": "Neues Passwort",
"confirmPassword": "Passwort bestätigen",
"passwordChanged": "Passwort erfolgreich geändert.",
"passwordMismatch": "Passwörter stimmen nicht überein.",
"club": "Mein Verein",
"quotaWarning": "Achtung: Du hast bereits {percent}% deines Monatskontingents verbraucht.",
"under21Notice": "Für Mitglieder unter 21: Reduziertes Kontingent von 30g/Monat (§19 Abs. 3 KCanG).",
"grams": "g",
"date": "Datum",
"strain": "Sorte",
"amount": "Menge",
"recordedBy": "Ausgegeben von",
"noHistory": "Noch keine Ausgaben vorhanden.",
"personalInfo": "Persönliche Daten",
"language": "Sprache",
"theme": "Design",
"themeLight": "Hell",
"themeDark": "Dunkel",
"themeSystem": "System",
"german": "Deutsch",
"english": "Englisch",
"quickInfo": "Kurzinfo",
"todayAvailable": "Heute noch verfügbar",
"monthAvailable": "Diesen Monat noch verfügbar",
"limitReached": "Limit erreicht",
"pagination": "{from}{to} von {total}",
"previous": "Zurück",
"next": "Weiter",
"allMonths": "Alle Monate",
"footerText": "Cannabis-Anbauverein — Sichere Mitgliederverwaltung",
"adminLogin": "Zum Admin-Login"
}
}