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"
}
}
+296
View File
@@ -0,0 +1,296 @@
{
"common": {
"appName": "CannaManage",
"loading": "Loading...",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"search": "Search",
"filter": "Filter",
"export": "Export",
"back": "Back",
"next": "Next",
"confirm": "Confirm",
"yes": "Yes",
"no": "No",
"noData": "No data available"
},
"nav": {
"dashboard": "Dashboard",
"members": "Members",
"stock": "Stock",
"distributions": "Distributions",
"compliance": "Compliance",
"reports": "Reports",
"settings": "Settings",
"staff": "Staff",
"portal": "Member Portal"
},
"auth": {
"login": "Sign In",
"logout": "Sign Out",
"email": "Email address",
"password": "Password",
"forgotPassword": "Forgot password?",
"resetPassword": "Reset Password",
"loginButton": "Sign In",
"loggingIn": "Signing in...",
"loginSubtitle": "Sign in to your cannabis club",
"invalidCredentials": "Invalid email address or password.",
"networkError": "Connection error. Please try again.",
"sessionExpired": "Your session has expired. Please sign in again.",
"emailInvalid": "Please enter a valid email address.",
"passwordRequired": "Please enter your password.",
"passwordTooShort": "Password must be at least 8 characters.",
"footerText": "Secure management for your cannabis cultivation club"
},
"dashboard": {
"title": "Dashboard",
"activeMembers": "Active Members",
"distributionsToday": "Distributions Today",
"stockLevel": "Stock Level",
"monthlyQuota": "Monthly Quota",
"quickActions": "Quick Actions",
"newDistribution": "New Distribution",
"addMember": "Add Member",
"recentDistributions": "Recent Distributions",
"stockByStrain": "Stock by Strain",
"date": "Date",
"member": "Member",
"strain": "Strain",
"amount": "Amount (g)",
"staff": "Staff",
"grams": "g",
"today": "Today",
"trend": "+{value}% vs last month",
"quotaUsed": "{value}% used",
"distributionCount": "{count} distributions, {grams}g"
},
"members": {
"title": "Member Management",
"addMember": "Add Member",
"name": "Name",
"email": "Email",
"status": "Status",
"memberSince": "Member Since",
"quota": "Quota",
"actions": "Actions",
"edit": "Edit",
"active": "Active",
"suspended": "Suspended",
"expelled": "Expelled",
"back": "Back to List",
"save": "Save",
"create": "Create Member",
"firstName": "First Name",
"lastName": "Last Name",
"dateOfBirth": "Date of Birth",
"phone": "Phone",
"memberNumber": "Member Number",
"joinedAt": "Joined At",
"notes": "Notes",
"notesPlaceholder": "Optional notes about the member...",
"under21Warning": "Under 21 — reduced quota (30g/month)",
"ageError": "Members must be at least 18 years old.",
"saved": "Changes saved.",
"created": "Member created successfully.",
"search": "Search name or email...",
"perPage": "Per page",
"showing": "{from}{to} of {total}",
"previous": "Previous",
"next": "Next",
"noResults": "No members found.",
"notFound": "Member not found.",
"personalInfo": "Personal Information",
"membershipInfo": "Membership"
},
"stock": {
"title": "Stock & Batches",
"newBatch": "New Batch",
"stockOverview": "Stock Overview",
"batchId": "Batch ID",
"strain": "Strain",
"thc": "THC %",
"cbd": "CBD %",
"status": "Status",
"available": "Available",
"availableGrams": "Available (g)",
"receivedAt": "Received",
"actions": "Actions",
"statusAvailable": "Available",
"statusRecalled": "Recalled",
"statusDepleted": "Depleted",
"recall": "Recall",
"recallConfirm": "Really recall this batch? All open distributions with this batch will be blocked.",
"recallTitle": "Recall Batch",
"recallSuccess": "Batch recalled.",
"totalBatches": "Total Batches",
"availableStock": "Available Stock",
"recalledBatches": "Recalled Batches",
"strainCount": "Strains",
"filterAll": "All",
"filterAvailable": "Available only",
"filterRecalled": "Recalled only",
"addBatch": "Add Batch",
"strainName": "Strain Name",
"amount": "Amount (g)",
"supplier": "Supplier / Origin",
"harvestDate": "Harvest Date",
"notes": "Notes",
"notesPlaceholder": "Optional notes about the batch...",
"created": "Batch created successfully.",
"grams": "g",
"confirmRecall": "Confirm Recall",
"lowStock": "Low"
},
"distributions": {
"title": "Distributions",
"newDistribution": "New Distribution",
"todaySummary": "Today: {count} distributions, {grams}g distributed",
"dateTime": "Date/Time",
"member": "Member",
"strain": "Strain",
"amount": "Amount (g)",
"staff": "Staff",
"status": "Status",
"completed": "Completed",
"locked": "Locked (immutable)",
"filterToday": "Today",
"filterWeek": "This Week",
"filterMonth": "This Month",
"searchMember": "Search member...",
"step1": "Select Member",
"step2": "Check Quota",
"step3": "Strain & Amount",
"step4": "Confirmation",
"selectMember": "Search member (name or number)...",
"memberBlocked": "Member is blocked — distribution not possible.",
"under21Info": "Reduced quota: 30g/month (under 21)",
"dailyRemaining": "Daily remaining",
"monthlyRemaining": "Monthly remaining",
"selectBatch": "Select batch",
"available": "available",
"amountLabel": "Amount in grams",
"exceedsDaily": "Exceeds daily limit ({limit}g).",
"exceedsMonthly": "Exceeds monthly limit ({limit}g).",
"exceedsBatch": "Insufficient stock in this batch.",
"confirm": "Confirm Distribution",
"summary": "Summary",
"success": "Distribution recorded successfully.",
"grams": "g",
"date": "Date",
"monthlyQuota": "Monthly Quota",
"remaining": "Remaining"
},
"reports": {
"title": "Reports",
"monthly": "Monthly Report",
"monthlyDesc": "Overview of all distributions in the selected month, including member quotas and stock changes.",
"memberList": "Member List",
"memberListDesc": "Complete member list with status, quota utilization and contact details.",
"recall": "Recall Report",
"recallDesc": "All batches with recall status and affected distributions for regulatory reporting.",
"downloadPdf": "Download as PDF",
"downloadCsv": "Download as CSV",
"preview": "Show Preview",
"generating": "Generating report...",
"downloaded": "{name} downloaded.",
"selectMonth": "Select month",
"selectStatus": "Filter by status",
"allStatuses": "All",
"activeOnly": "Active",
"suspendedOnly": "Suspended",
"dateFrom": "From",
"dateTo": "To",
"previewTitle": "Report Preview",
"totalDistributions": "Total Distributions",
"totalGrams": "Total Grams",
"uniqueMembers": "Unique Members",
"averagePerMember": "Avg per Member",
"topStrains": "Top Strains",
"affectedDistributions": "Affected Distributions",
"affectedMembers": "Affected Members",
"recalledBatches": "Recalled Batches",
"close": "Close",
"complianceNote": "This report is suitable for submission to the responsible authority.",
"complianceBadge": "§19 KCanG compliant",
"auditTrail": "All reports are generated with timestamps. The underlying distribution data is immutable (audit trail).",
"memberNumber": "No.",
"name": "Name",
"status": "Status",
"joinedAt": "Joined",
"usage": "Usage",
"strain": "Strain",
"grams": "Grams",
"percent": "Share",
"batchId": "Batch ID",
"recalledAt": "Recalled on",
"reason": "Reason",
"distributed": "Distributed",
"original": "Original"
},
"portal": {
"title": "My Area",
"login": "Member Login",
"loginSubtitle": "Sign in to the member portal",
"email": "Email address",
"password": "Password",
"loginButton": "Sign In",
"loggingIn": "Signing in...",
"invalidCredentials": "Invalid email address or password.",
"networkError": "Connection error. Please try again.",
"welcome": "Welcome back, {name}!",
"dashboard": "Overview",
"quota": "My Quota",
"history": "Distribution History",
"profile": "Profile",
"settings": "Settings",
"logout": "Sign Out",
"dailyQuota": "Daily Quota",
"monthlyQuota": "Monthly Quota",
"remaining": "remaining",
"used": "used",
"of": "of",
"lastDistribution": "Last Distribution",
"noDistributions": "No distributions this month yet.",
"memberSince": "Member since",
"memberNumber": "Member number",
"nextAvailable": "Next available",
"nextAvailableTomorrow": "Tomorrow at 00:00",
"changePassword": "Change Password",
"currentPassword": "Current Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"passwordChanged": "Password changed successfully.",
"passwordMismatch": "Passwords do not match.",
"club": "My Club",
"quotaWarning": "Warning: You have already used {percent}% of your monthly quota.",
"under21Notice": "For members under 21: Reduced quota of 30g/month (§19 Abs. 3 KCanG).",
"grams": "g",
"date": "Date",
"strain": "Strain",
"amount": "Amount",
"recordedBy": "Recorded by",
"noHistory": "No distributions recorded yet.",
"personalInfo": "Personal Information",
"language": "Language",
"theme": "Theme",
"themeLight": "Light",
"themeDark": "Dark",
"themeSystem": "System",
"german": "German",
"english": "English",
"quickInfo": "Quick Info",
"todayAvailable": "Available today",
"monthAvailable": "Available this month",
"limitReached": "Limit reached",
"pagination": "{from}{to} of {total}",
"previous": "Previous",
"next": "Next",
"allMonths": "All months",
"footerText": "Cannabis cultivation club — Secure member management",
"adminLogin": "Go to Admin Login"
}
}