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
@@ -0,0 +1,97 @@
import type { BatchSummary, ClubStats, Distribution } from "@/types/api"
export const mockClubStats: ClubStats = {
totalMembers: 52,
activeMembers: 45,
distributionsToday: 8,
gramsDistributedToday: 142,
totalStockGrams: 2350,
monthlyQuotaUsagePercent: 63,
}
export const mockRecentDistributions: Distribution[] = [
{
id: "d-001",
memberId: "m-012",
memberName: "Max Müller",
strainName: "Amnesia Haze",
amountGrams: 25,
recordedBy: "Anna Schmidt",
recordedAt: "2026-06-12T14:30:00Z",
},
{
id: "d-002",
memberId: "m-007",
memberName: "Lisa Weber",
strainName: "White Widow",
amountGrams: 15,
recordedBy: "Anna Schmidt",
recordedAt: "2026-06-12T13:15:00Z",
},
{
id: "d-003",
memberId: "m-023",
memberName: "Jonas Fischer",
strainName: "Northern Lights",
amountGrams: 20,
recordedBy: "Tom Bauer",
recordedAt: "2026-06-12T11:45:00Z",
},
{
id: "d-004",
memberId: "m-031",
memberName: "Sarah Braun",
strainName: "Jack Herer",
amountGrams: 30,
recordedBy: "Anna Schmidt",
recordedAt: "2026-06-12T10:20:00Z",
},
{
id: "d-005",
memberId: "m-045",
memberName: "Kai Hoffmann",
strainName: "Blue Dream",
amountGrams: 22,
recordedBy: "Tom Bauer",
recordedAt: "2026-06-12T09:00:00Z",
},
]
export const mockStockByStrain: BatchSummary[] = [
{
id: "b-001",
strainName: "Amnesia Haze",
availableGrams: 520,
status: "AVAILABLE",
},
{
id: "b-002",
strainName: "White Widow",
availableGrams: 430,
status: "AVAILABLE",
},
{
id: "b-003",
strainName: "Northern Lights",
availableGrams: 380,
status: "AVAILABLE",
},
{
id: "b-004",
strainName: "Jack Herer",
availableGrams: 450,
status: "AVAILABLE",
},
{
id: "b-005",
strainName: "Blue Dream",
availableGrams: 340,
status: "AVAILABLE",
},
{
id: "b-006",
strainName: "Girl Scout Cookies",
availableGrams: 230,
status: "AVAILABLE",
},
]
@@ -0,0 +1,530 @@
import type {
AvailableBatch,
DistributionRecord,
QuotaStatus,
} from "@/types/api"
// Generate dates spread across last 30 days
function daysAgo(days: number, hours: number, minutes: number): string {
const d = new Date()
d.setDate(d.getDate() - days)
d.setHours(hours, minutes, 0, 0)
return d.toISOString()
}
export const mockDistributions: DistributionRecord[] = [
// Today (8 records)
{
id: "dist-001",
memberId: "m-001",
memberName: "Thomas Müller",
batchId: "b-001",
strainName: "Amnesia Haze",
amountGrams: 5,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(0, 16, 10),
status: "COMPLETED",
},
{
id: "dist-002",
memberId: "m-002",
memberName: "Anna Schmidt",
batchId: "b-003",
strainName: "Northern Lights",
amountGrams: 3,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(0, 15, 30),
status: "COMPLETED",
},
{
id: "dist-003",
memberId: "m-003",
memberName: "Markus Weber",
batchId: "b-002",
strainName: "White Widow",
amountGrams: 7,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(0, 14, 45),
status: "COMPLETED",
},
{
id: "dist-004",
memberId: "m-006",
memberName: "Lisa Hoffmann",
batchId: "b-004",
strainName: "Jack Herer",
amountGrams: 10,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(0, 13, 20),
status: "COMPLETED",
},
{
id: "dist-005",
memberId: "m-008",
memberName: "Sabine Koch",
batchId: "b-005",
strainName: "Blue Dream",
amountGrams: 4,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(0, 12, 0),
status: "COMPLETED",
},
{
id: "dist-006",
memberId: "m-009",
memberName: "Christian Braun",
batchId: "b-001",
strainName: "Amnesia Haze",
amountGrams: 8,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(0, 11, 15),
status: "COMPLETED",
},
{
id: "dist-007",
memberId: "m-010",
memberName: "Petra Richter",
batchId: "b-006",
strainName: "Girl Scout Cookies",
amountGrams: 6,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(0, 10, 30),
status: "COMPLETED",
},
{
id: "dist-008",
memberId: "m-004",
memberName: "Julia Fischer",
batchId: "b-002",
strainName: "White Widow",
amountGrams: 2.5,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(0, 9, 45),
status: "COMPLETED",
},
// Yesterday (6 records)
{
id: "dist-009",
memberId: "m-011",
memberName: "Michael Wagner",
batchId: "b-003",
strainName: "Northern Lights",
amountGrams: 12,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(1, 16, 0),
status: "COMPLETED",
},
{
id: "dist-010",
memberId: "m-012",
memberName: "Sandra Klein",
batchId: "b-001",
strainName: "Amnesia Haze",
amountGrams: 5,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(1, 14, 30),
status: "COMPLETED",
},
{
id: "dist-011",
memberId: "m-001",
memberName: "Thomas Müller",
batchId: "b-004",
strainName: "Jack Herer",
amountGrams: 8,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(1, 13, 0),
status: "COMPLETED",
},
{
id: "dist-012",
memberId: "m-003",
memberName: "Markus Weber",
batchId: "b-005",
strainName: "Blue Dream",
amountGrams: 3.5,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(1, 11, 45),
status: "COMPLETED",
},
{
id: "dist-013",
memberId: "m-006",
memberName: "Lisa Hoffmann",
batchId: "b-002",
strainName: "White Widow",
amountGrams: 6,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(1, 10, 20),
status: "COMPLETED",
},
{
id: "dist-014",
memberId: "m-007",
memberName: "Daniel Schäfer",
batchId: "b-006",
strainName: "Girl Scout Cookies",
amountGrams: 4,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(1, 9, 15),
status: "COMPLETED",
},
// 2 days ago (5 records)
{
id: "dist-015",
memberId: "m-002",
memberName: "Anna Schmidt",
batchId: "b-001",
strainName: "Amnesia Haze",
amountGrams: 10,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(2, 15, 30),
status: "COMPLETED",
},
{
id: "dist-016",
memberId: "m-008",
memberName: "Sabine Koch",
batchId: "b-003",
strainName: "Northern Lights",
amountGrams: 7,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(2, 14, 0),
status: "COMPLETED",
},
{
id: "dist-017",
memberId: "m-009",
memberName: "Christian Braun",
batchId: "b-004",
strainName: "Jack Herer",
amountGrams: 15,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(2, 12, 30),
status: "COMPLETED",
},
{
id: "dist-018",
memberId: "m-010",
memberName: "Petra Richter",
batchId: "b-005",
strainName: "Blue Dream",
amountGrams: 5,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(2, 11, 0),
status: "COMPLETED",
},
{
id: "dist-019",
memberId: "m-004",
memberName: "Julia Fischer",
batchId: "b-002",
strainName: "White Widow",
amountGrams: 3,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(2, 9, 30),
status: "COMPLETED",
},
// 3-5 days ago
{
id: "dist-020",
memberId: "m-011",
memberName: "Michael Wagner",
batchId: "b-001",
strainName: "Amnesia Haze",
amountGrams: 8,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(3, 15, 0),
status: "COMPLETED",
},
{
id: "dist-021",
memberId: "m-012",
memberName: "Sandra Klein",
batchId: "b-006",
strainName: "Girl Scout Cookies",
amountGrams: 6,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(3, 13, 30),
status: "COMPLETED",
},
{
id: "dist-022",
memberId: "m-001",
memberName: "Thomas Müller",
batchId: "b-003",
strainName: "Northern Lights",
amountGrams: 10,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(4, 16, 0),
status: "COMPLETED",
},
{
id: "dist-023",
memberId: "m-002",
memberName: "Anna Schmidt",
batchId: "b-004",
strainName: "Jack Herer",
amountGrams: 4,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(4, 14, 15),
status: "COMPLETED",
},
{
id: "dist-024",
memberId: "m-006",
memberName: "Lisa Hoffmann",
batchId: "b-001",
strainName: "Amnesia Haze",
amountGrams: 9,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(5, 15, 30),
status: "COMPLETED",
},
{
id: "dist-025",
memberId: "m-008",
memberName: "Sabine Koch",
batchId: "b-005",
strainName: "Blue Dream",
amountGrams: 11,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(5, 12, 0),
status: "COMPLETED",
},
// 6-14 days ago
{
id: "dist-026",
memberId: "m-009",
memberName: "Christian Braun",
batchId: "b-002",
strainName: "White Widow",
amountGrams: 7,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(7, 14, 0),
status: "COMPLETED",
},
{
id: "dist-027",
memberId: "m-003",
memberName: "Markus Weber",
batchId: "b-006",
strainName: "Girl Scout Cookies",
amountGrams: 5,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(8, 11, 30),
status: "COMPLETED",
},
{
id: "dist-028",
memberId: "m-010",
memberName: "Petra Richter",
batchId: "b-003",
strainName: "Northern Lights",
amountGrams: 13,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(10, 15, 45),
status: "COMPLETED",
},
{
id: "dist-029",
memberId: "m-004",
memberName: "Julia Fischer",
batchId: "b-001",
strainName: "Amnesia Haze",
amountGrams: 6,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(12, 13, 20),
status: "COMPLETED",
},
{
id: "dist-030",
memberId: "m-007",
memberName: "Daniel Schäfer",
batchId: "b-004",
strainName: "Jack Herer",
amountGrams: 3,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(14, 10, 0),
status: "COMPLETED",
},
// 15-30 days ago
{
id: "dist-031",
memberId: "m-011",
memberName: "Michael Wagner",
batchId: "b-005",
strainName: "Blue Dream",
amountGrams: 8,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(16, 14, 30),
status: "COMPLETED",
},
{
id: "dist-032",
memberId: "m-012",
memberName: "Sandra Klein",
batchId: "b-002",
strainName: "White Widow",
amountGrams: 10,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(20, 11, 0),
status: "COMPLETED",
},
{
id: "dist-033",
memberId: "m-001",
memberName: "Thomas Müller",
batchId: "b-006",
strainName: "Girl Scout Cookies",
amountGrams: 4,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(22, 16, 0),
status: "COMPLETED",
},
{
id: "dist-034",
memberId: "m-006",
memberName: "Lisa Hoffmann",
batchId: "b-003",
strainName: "Northern Lights",
amountGrams: 12,
recordedBy: "Thomas Klein",
recordedAt: daysAgo(25, 13, 0),
status: "COMPLETED",
},
{
id: "dist-035",
memberId: "m-009",
memberName: "Christian Braun",
batchId: "b-001",
strainName: "Amnesia Haze",
amountGrams: 7,
recordedBy: "Maria Schulz",
recordedAt: daysAgo(28, 15, 0),
status: "COMPLETED",
},
]
// Quota mock for different members
export function getMockQuota(memberId: string): QuotaStatus {
const quotas: Record<string, QuotaStatus> = {
"m-001": {
dailyUsedGrams: 5,
dailyLimitGrams: 25,
monthlyUsedGrams: 23,
monthlyLimitGrams: 50,
isUnder21: false,
},
"m-002": {
dailyUsedGrams: 3,
dailyLimitGrams: 25,
monthlyUsedGrams: 36,
monthlyLimitGrams: 50,
isUnder21: false,
},
"m-003": {
dailyUsedGrams: 7,
dailyLimitGrams: 25,
monthlyUsedGrams: 15,
monthlyLimitGrams: 50,
isUnder21: false,
},
"m-004": {
dailyUsedGrams: 2.5,
dailyLimitGrams: 25,
monthlyUsedGrams: 44,
monthlyLimitGrams: 50,
isUnder21: false,
},
"m-006": {
dailyUsedGrams: 10,
dailyLimitGrams: 25,
monthlyUsedGrams: 27.5,
monthlyLimitGrams: 50,
isUnder21: false,
},
"m-007": {
dailyUsedGrams: 4,
dailyLimitGrams: 25,
monthlyUsedGrams: 7,
monthlyLimitGrams: 30,
isUnder21: true,
},
"m-008": {
dailyUsedGrams: 4,
dailyLimitGrams: 25,
monthlyUsedGrams: 20,
monthlyLimitGrams: 50,
isUnder21: false,
},
"m-009": {
dailyUsedGrams: 8,
dailyLimitGrams: 25,
monthlyUsedGrams: 30,
monthlyLimitGrams: 50,
isUnder21: false,
},
"m-010": {
dailyUsedGrams: 6,
dailyLimitGrams: 25,
monthlyUsedGrams: 18,
monthlyLimitGrams: 50,
isUnder21: false,
},
}
return (
quotas[memberId] ?? {
dailyUsedGrams: 0,
dailyLimitGrams: 25,
monthlyUsedGrams: 0,
monthlyLimitGrams: 50,
isUnder21: false,
}
)
}
export const mockAvailableBatches: AvailableBatch[] = [
{
id: "b-001",
strainName: "Amnesia Haze",
availableGrams: 520,
thcPercent: 22.5,
status: "AVAILABLE",
},
{
id: "b-002",
strainName: "White Widow",
availableGrams: 430,
thcPercent: 18.0,
status: "AVAILABLE",
},
{
id: "b-003",
strainName: "Northern Lights",
availableGrams: 380,
thcPercent: 16.5,
status: "AVAILABLE",
},
{
id: "b-004",
strainName: "Jack Herer",
availableGrams: 450,
thcPercent: 20.0,
status: "AVAILABLE",
},
{
id: "b-005",
strainName: "Blue Dream",
availableGrams: 340,
thcPercent: 21.0,
status: "AVAILABLE",
},
{
id: "b-006",
strainName: "Girl Scout Cookies",
availableGrams: 230,
thcPercent: 24.0,
status: "AVAILABLE",
},
]
@@ -0,0 +1,297 @@
import type { Member } from "@/types/api"
export const mockMembers: Member[] = [
{
id: "m-001",
firstName: "Thomas",
lastName: "Müller",
email: "thomas.mueller@email.de",
dateOfBirth: "1985-03-15",
phone: "+49 170 1234567",
memberNumber: "CM-2024-001",
status: "ACTIVE",
joinedAt: "2024-07-01",
monthlyQuotaUsedPercent: 45,
notes: "Gründungsmitglied",
},
{
id: "m-002",
firstName: "Anna",
lastName: "Schmidt",
email: "anna.schmidt@email.de",
dateOfBirth: "1990-08-22",
phone: "+49 171 2345678",
memberNumber: "CM-2024-002",
status: "ACTIVE",
joinedAt: "2024-07-01",
monthlyQuotaUsedPercent: 72,
},
{
id: "m-003",
firstName: "Markus",
lastName: "Weber",
email: "markus.weber@email.de",
dateOfBirth: "1978-11-03",
phone: "+49 172 3456789",
memberNumber: "CM-2024-003",
status: "ACTIVE",
joinedAt: "2024-07-15",
monthlyQuotaUsedPercent: 30,
},
{
id: "m-004",
firstName: "Julia",
lastName: "Fischer",
email: "julia.fischer@email.de",
dateOfBirth: "1995-05-10",
memberNumber: "CM-2024-004",
status: "ACTIVE",
joinedAt: "2024-08-01",
monthlyQuotaUsedPercent: 88,
notes: "Bevorzugt CBD-reiche Sorten",
},
{
id: "m-005",
firstName: "Stefan",
lastName: "Becker",
email: "stefan.becker@email.de",
dateOfBirth: "1982-01-28",
phone: "+49 173 4567890",
memberNumber: "CM-2024-005",
status: "SUSPENDED",
joinedAt: "2024-08-15",
monthlyQuotaUsedPercent: 0,
notes: "Beitrag seit 2 Monaten ausstehend",
},
{
id: "m-006",
firstName: "Lisa",
lastName: "Hoffmann",
email: "lisa.hoffmann@email.de",
dateOfBirth: "1988-09-17",
phone: "+49 174 5678901",
memberNumber: "CM-2024-006",
status: "ACTIVE",
joinedAt: "2024-09-01",
monthlyQuotaUsedPercent: 55,
},
{
id: "m-007",
firstName: "Daniel",
lastName: "Schäfer",
email: "daniel.schaefer@email.de",
dateOfBirth: "2005-04-12",
phone: "+49 175 6789012",
memberNumber: "CM-2024-007",
status: "ACTIVE",
joinedAt: "2024-09-15",
monthlyQuotaUsedPercent: 20,
notes: "Unter 21 — reduziertes Kontingent",
},
{
id: "m-008",
firstName: "Sabine",
lastName: "Koch",
email: "sabine.koch@email.de",
dateOfBirth: "1975-12-05",
memberNumber: "CM-2024-008",
status: "ACTIVE",
joinedAt: "2024-10-01",
monthlyQuotaUsedPercent: 40,
},
{
id: "m-009",
firstName: "Andreas",
lastName: "Richter",
email: "andreas.richter@email.de",
dateOfBirth: "1992-06-30",
phone: "+49 176 7890123",
memberNumber: "CM-2024-009",
status: "EXPELLED",
joinedAt: "2024-10-01",
monthlyQuotaUsedPercent: 0,
notes: "Verstoß gegen Vereinssatzung — Weitergabe an Dritte",
},
{
id: "m-010",
firstName: "Katharina",
lastName: "Wolf",
email: "katharina.wolf@email.de",
dateOfBirth: "1998-02-14",
phone: "+49 177 8901234",
memberNumber: "CM-2024-010",
status: "ACTIVE",
joinedAt: "2024-10-15",
monthlyQuotaUsedPercent: 62,
},
{
id: "m-011",
firstName: "Patrick",
lastName: "Neumann",
email: "patrick.neumann@email.de",
dateOfBirth: "1987-07-21",
memberNumber: "CM-2024-011",
status: "ACTIVE",
joinedAt: "2024-11-01",
monthlyQuotaUsedPercent: 33,
},
{
id: "m-012",
firstName: "Claudia",
lastName: "Schwarz",
email: "claudia.schwarz@email.de",
dateOfBirth: "1993-10-08",
phone: "+49 178 9012345",
memberNumber: "CM-2025-012",
status: "ACTIVE",
joinedAt: "2025-01-01",
monthlyQuotaUsedPercent: 78,
},
{
id: "m-013",
firstName: "Florian",
lastName: "Braun",
email: "florian.braun@email.de",
dateOfBirth: "2006-03-25",
phone: "+49 179 0123456",
memberNumber: "CM-2025-013",
status: "ACTIVE",
joinedAt: "2025-01-15",
monthlyQuotaUsedPercent: 15,
notes: "Unter 21 — reduziertes Kontingent",
},
{
id: "m-014",
firstName: "Monika",
lastName: "Zimmermann",
email: "monika.zimmermann@email.de",
dateOfBirth: "1970-04-02",
memberNumber: "CM-2025-014",
status: "SUSPENDED",
joinedAt: "2025-02-01",
monthlyQuotaUsedPercent: 0,
notes: "Ruhendes Mitglied auf eigenen Wunsch",
},
{
id: "m-015",
firstName: "Jens",
lastName: "Krüger",
email: "jens.krueger@email.de",
dateOfBirth: "1983-08-19",
phone: "+49 160 1234567",
memberNumber: "CM-2025-015",
status: "ACTIVE",
joinedAt: "2025-02-15",
monthlyQuotaUsedPercent: 50,
},
{
id: "m-016",
firstName: "Melanie",
lastName: "Hartmann",
email: "melanie.hartmann@email.de",
dateOfBirth: "1996-11-11",
phone: "+49 161 2345678",
memberNumber: "CM-2025-016",
status: "ACTIVE",
joinedAt: "2025-03-01",
monthlyQuotaUsedPercent: 25,
},
{
id: "m-017",
firstName: "Christian",
lastName: "Lange",
email: "christian.lange@email.de",
dateOfBirth: "1989-05-07",
memberNumber: "CM-2025-017",
status: "ACTIVE",
joinedAt: "2025-03-15",
monthlyQuotaUsedPercent: 95,
notes: "Kontingent fast ausgeschöpft",
},
{
id: "m-018",
firstName: "Nina",
lastName: "Werner",
email: "nina.werner@email.de",
dateOfBirth: "2005-09-30",
phone: "+49 162 3456789",
memberNumber: "CM-2025-018",
status: "ACTIVE",
joinedAt: "2025-04-01",
monthlyQuotaUsedPercent: 10,
notes: "Unter 21 — reduziertes Kontingent",
},
{
id: "m-019",
firstName: "Oliver",
lastName: "Schmitt",
email: "oliver.schmitt@email.de",
dateOfBirth: "1980-12-24",
phone: "+49 163 4567890",
memberNumber: "CM-2025-019",
status: "EXPELLED",
joinedAt: "2025-04-15",
monthlyQuotaUsedPercent: 0,
notes: "Wiederholte Verstöße gegen Jugendschutzauflagen",
},
{
id: "m-020",
firstName: "Sandra",
lastName: "Meyer",
email: "sandra.meyer@email.de",
dateOfBirth: "1991-07-03",
memberNumber: "CM-2025-020",
status: "ACTIVE",
joinedAt: "2025-05-01",
monthlyQuotaUsedPercent: 38,
},
{
id: "m-021",
firstName: "Michael",
lastName: "Schulz",
email: "michael.schulz@email.de",
dateOfBirth: "1976-02-18",
phone: "+49 164 5678901",
memberNumber: "CM-2025-021",
status: "ACTIVE",
joinedAt: "2025-05-15",
monthlyQuotaUsedPercent: 60,
},
{
id: "m-022",
firstName: "Lena",
lastName: "Huber",
email: "lena.huber@email.de",
dateOfBirth: "2007-01-09",
phone: "+49 165 6789012",
memberNumber: "CM-2025-022",
status: "ACTIVE",
joinedAt: "2025-06-01",
monthlyQuotaUsedPercent: 5,
notes: "Unter 21 — reduziertes Kontingent",
},
{
id: "m-023",
firstName: "Ralf",
lastName: "Keller",
email: "ralf.keller@email.de",
dateOfBirth: "1968-06-15",
memberNumber: "CM-2025-023",
status: "SUSPENDED",
joinedAt: "2025-06-01",
monthlyQuotaUsedPercent: 0,
notes: "Ärztliches Attest ausstehend",
},
{
id: "m-024",
firstName: "Petra",
lastName: "Frank",
email: "petra.frank@email.de",
dateOfBirth: "1994-04-20",
phone: "+49 166 7890123",
memberNumber: "CM-2026-024",
status: "ACTIVE",
joinedAt: "2026-01-15",
monthlyQuotaUsedPercent: 42,
},
]
@@ -0,0 +1,115 @@
export const mockPortalUser = {
id: "member-001",
firstName: "Max",
lastName: "Mustermann",
email: "max@example.de",
memberNumber: "GD-2024-0042",
joinedAt: "2024-08-15",
dateOfBirth: "1995-03-22",
isUnder21: false,
clubName: "Grüner Daumen e.V.",
}
export const mockPortalQuota = {
dailyUsedGrams: 5.0,
dailyLimitGrams: 25,
monthlyUsedGrams: 32.5,
monthlyLimitGrams: 50,
isUnder21: false,
lastDistributionAt: "2026-06-12T14:30:00Z",
}
export interface PortalDistribution {
id: string
date: string
strain: string
amountGrams: number
recordedBy: string
}
export const mockPortalHistory: PortalDistribution[] = [
{
id: "dist-001",
date: "2026-06-12T14:30:00Z",
strain: "Blue Dream",
amountGrams: 5.0,
recordedBy: "Anna M.",
},
{
id: "dist-002",
date: "2026-06-10T10:15:00Z",
strain: "Northern Lights",
amountGrams: 3.5,
recordedBy: "Thomas K.",
},
{
id: "dist-003",
date: "2026-06-07T16:45:00Z",
strain: "Blue Dream",
amountGrams: 5.0,
recordedBy: "Anna M.",
},
{
id: "dist-004",
date: "2026-06-04T11:00:00Z",
strain: "Amnesia Haze",
amountGrams: 4.0,
recordedBy: "Thomas K.",
},
{
id: "dist-005",
date: "2026-06-01T09:30:00Z",
strain: "White Widow",
amountGrams: 5.0,
recordedBy: "Anna M.",
},
{
id: "dist-006",
date: "2026-05-28T15:20:00Z",
strain: "OG Kush",
amountGrams: 5.0,
recordedBy: "Thomas K.",
},
{
id: "dist-007",
date: "2026-05-25T12:00:00Z",
strain: "Blue Dream",
amountGrams: 3.0,
recordedBy: "Anna M.",
},
{
id: "dist-008",
date: "2026-05-21T10:45:00Z",
strain: "Northern Lights",
amountGrams: 5.0,
recordedBy: "Thomas K.",
},
{
id: "dist-009",
date: "2026-05-17T14:30:00Z",
strain: "Amnesia Haze",
amountGrams: 4.5,
recordedBy: "Anna M.",
},
{
id: "dist-010",
date: "2026-05-13T11:15:00Z",
strain: "White Widow",
amountGrams: 5.0,
recordedBy: "Thomas K.",
},
{
id: "dist-011",
date: "2026-05-09T09:00:00Z",
strain: "Blue Dream",
amountGrams: 5.0,
recordedBy: "Anna M.",
},
{
id: "dist-012",
date: "2026-05-05T16:30:00Z",
strain: "OG Kush",
amountGrams: 3.5,
recordedBy: "Thomas K.",
},
]
@@ -0,0 +1,132 @@
export const mockMonthlyReportPreview = {
month: "Juni 2026",
club: "Grüner Daumen e.V.",
totalDistributions: 142,
totalGrams: 2840,
uniqueMembers: 38,
averagePerMember: 74.7,
topStrains: [
{ name: "Amnesia Haze", grams: 680, percent: 23.9 },
{ name: "White Widow", grams: 520, percent: 18.3 },
{ name: "Northern Lights", grams: 440, percent: 15.5 },
{ name: "Jack Herer", grams: 380, percent: 13.4 },
{ name: "Blue Dream", grams: 360, percent: 12.7 },
],
dailyBreakdown: [
{ date: "01.06.", distributions: 5, grams: 98 },
{ date: "02.06.", distributions: 7, grams: 135 },
{ date: "03.06.", distributions: 4, grams: 82 },
{ date: "04.06.", distributions: 6, grams: 118 },
{ date: "05.06.", distributions: 8, grams: 156 },
{ date: "06.06.", distributions: 5, grams: 95 },
{ date: "07.06.", distributions: 3, grams: 62 },
{ date: "08.06.", distributions: 6, grams: 120 },
{ date: "09.06.", distributions: 7, grams: 142 },
{ date: "10.06.", distributions: 5, grams: 105 },
{ date: "11.06.", distributions: 4, grams: 88 },
{ date: "12.06.", distributions: 6, grams: 115 },
],
}
export const mockMemberListPreview = {
generatedAt: "2026-06-12",
totalMembers: 45,
active: 40,
suspended: 3,
expelled: 2,
members: [
{
memberNumber: "M-001",
name: "Max Müller",
status: "ACTIVE" as const,
joinedAt: "2025-01-15",
monthlyUsage: 42.5,
monthlyLimit: 50,
},
{
memberNumber: "M-002",
name: "Anna Schmidt",
status: "ACTIVE" as const,
joinedAt: "2025-02-20",
monthlyUsage: 28.0,
monthlyLimit: 50,
},
{
memberNumber: "M-003",
name: "Thomas Weber",
status: "ACTIVE" as const,
joinedAt: "2025-01-30",
monthlyUsage: 50.0,
monthlyLimit: 50,
},
{
memberNumber: "M-004",
name: "Lisa Wagner",
status: "SUSPENDED" as const,
joinedAt: "2025-03-10",
monthlyUsage: 0,
monthlyLimit: 50,
},
{
memberNumber: "M-005",
name: "Felix Braun",
status: "ACTIVE" as const,
joinedAt: "2025-04-05",
monthlyUsage: 18.5,
monthlyLimit: 30,
},
{
memberNumber: "M-006",
name: "Sophie Klein",
status: "ACTIVE" as const,
joinedAt: "2025-05-12",
monthlyUsage: 35.0,
monthlyLimit: 50,
},
{
memberNumber: "M-007",
name: "Jan Hoffmann",
status: "EXPELLED" as const,
joinedAt: "2025-01-22",
monthlyUsage: 0,
monthlyLimit: 50,
},
{
memberNumber: "M-008",
name: "Marie Fischer",
status: "ACTIVE" as const,
joinedAt: "2025-06-01",
monthlyUsage: 22.0,
monthlyLimit: 50,
},
],
}
export const mockRecallReportPreview = {
dateRange: { from: "2026-05-01", to: "2026-06-12" },
recalledBatches: 2,
affectedDistributions: 12,
affectedMembers: 8,
batches: [
{
batchId: "B-2026-042",
strain: "Girl Scout Cookies",
recalledAt: "2026-05-18",
reason: "THC-Gehalt über Grenzwert (>10% Abweichung)",
originalGrams: 500,
distributedGrams: 180,
affectedMembers: 5,
affectedDistributions: 7,
},
{
batchId: "B-2026-051",
strain: "OG Kush",
recalledAt: "2026-06-02",
reason: "Schimmelbefall festgestellt bei Qualitätskontrolle",
originalGrams: 300,
distributedGrams: 95,
affectedMembers: 3,
affectedDistributions: 5,
},
],
}
+199
View File
@@ -0,0 +1,199 @@
import type { Batch, Strain } from "@/types/api"
function daysAgo(days: number): string {
const d = new Date()
d.setDate(d.getDate() - days)
d.setHours(10, 0, 0, 0)
return d.toISOString()
}
export const mockStrains: Strain[] = [
{
id: "s-001",
name: "Amnesia Haze",
defaultThcPercent: 22,
defaultCbdPercent: 1,
},
{
id: "s-002",
name: "White Widow",
defaultThcPercent: 19,
defaultCbdPercent: 2,
},
{
id: "s-003",
name: "Northern Lights",
defaultThcPercent: 18,
defaultCbdPercent: 3,
},
{
id: "s-004",
name: "Jack Herer",
defaultThcPercent: 21,
defaultCbdPercent: 1.5,
},
{
id: "s-005",
name: "Blue Dream",
defaultThcPercent: 20,
defaultCbdPercent: 2,
},
{
id: "s-006",
name: "Girl Scout Cookies",
defaultThcPercent: 25,
defaultCbdPercent: 1,
},
]
export const mockBatches: Batch[] = [
{
id: "b-001",
strainName: "Amnesia Haze",
thcPercent: 22.5,
cbdPercent: 0.8,
totalGrams: 500,
availableGrams: 342,
status: "AVAILABLE",
supplier: "GreenGrow GmbH",
harvestDate: "2026-04-15",
receivedAt: daysAgo(45),
notes: "Premium-Qualität, Labor geprüft",
},
{
id: "b-002",
strainName: "White Widow",
thcPercent: 19.2,
cbdPercent: 2.1,
totalGrams: 300,
availableGrams: 187,
status: "AVAILABLE",
supplier: "CannaCraft Berlin",
harvestDate: "2026-04-20",
receivedAt: daysAgo(40),
},
{
id: "b-003",
strainName: "Northern Lights",
thcPercent: 17.8,
cbdPercent: 3.2,
totalGrams: 400,
availableGrams: 78,
status: "AVAILABLE",
supplier: "GreenGrow GmbH",
harvestDate: "2026-05-01",
receivedAt: daysAgo(30),
notes: "Hoher CBD-Gehalt, beliebt bei Schmerzpatienten",
},
{
id: "b-004",
strainName: "Jack Herer",
thcPercent: 21.0,
cbdPercent: 1.5,
totalGrams: 600,
availableGrams: 455,
status: "AVAILABLE",
supplier: "HanfHaus Sachsen",
harvestDate: "2026-05-05",
receivedAt: daysAgo(25),
},
{
id: "b-005",
strainName: "Blue Dream",
thcPercent: 20.3,
cbdPercent: 1.8,
totalGrams: 350,
availableGrams: 220,
status: "AVAILABLE",
supplier: "CannaCraft Berlin",
harvestDate: "2026-05-10",
receivedAt: daysAgo(20),
},
{
id: "b-006",
strainName: "Girl Scout Cookies",
thcPercent: 25.1,
cbdPercent: 0.9,
totalGrams: 250,
availableGrams: 180,
status: "AVAILABLE",
supplier: "MedCanna Bayern",
harvestDate: "2026-05-12",
receivedAt: daysAgo(18),
notes: "Sehr hoher THC-Gehalt — nur erfahrene Nutzer",
},
{
id: "b-007",
strainName: "Amnesia Haze",
thcPercent: 23.0,
cbdPercent: 0.7,
totalGrams: 450,
availableGrams: 390,
status: "AVAILABLE",
supplier: "GreenGrow GmbH",
harvestDate: "2026-05-18",
receivedAt: daysAgo(12),
},
{
id: "b-008",
strainName: "White Widow",
thcPercent: 18.5,
cbdPercent: 2.3,
totalGrams: 200,
availableGrams: 52,
status: "AVAILABLE",
supplier: "HanfHaus Sachsen",
harvestDate: "2026-05-22",
receivedAt: daysAgo(8),
},
{
id: "b-009",
strainName: "Northern Lights",
thcPercent: 18.0,
cbdPercent: 3.0,
totalGrams: 500,
availableGrams: 0,
status: "DEPLETED",
supplier: "GreenGrow GmbH",
harvestDate: "2026-03-01",
receivedAt: daysAgo(75),
},
{
id: "b-010",
strainName: "Jack Herer",
thcPercent: 20.5,
cbdPercent: 1.4,
totalGrams: 350,
availableGrams: 0,
status: "DEPLETED",
supplier: "CannaCraft Berlin",
harvestDate: "2026-03-10",
receivedAt: daysAgo(70),
},
{
id: "b-011",
strainName: "Blue Dream",
thcPercent: 19.8,
cbdPercent: 2.0,
totalGrams: 300,
availableGrams: 245,
status: "RECALLED",
supplier: "MedCanna Bayern",
harvestDate: "2026-04-01",
receivedAt: daysAgo(55),
notes: "Rückruf: Schimmelbefall bei Laborkontrolle festgestellt",
},
{
id: "b-012",
strainName: "Girl Scout Cookies",
thcPercent: 24.8,
cbdPercent: 1.0,
totalGrams: 200,
availableGrams: 130,
status: "RECALLED",
supplier: "HanfHaus Sachsen",
harvestDate: "2026-04-08",
receivedAt: daysAgo(50),
notes: "Rückruf: Pestizid-Rückstände über Grenzwert",
},
]
@@ -0,0 +1,39 @@
import type { NavigationType } from "@/types"
export const navigationsData: NavigationType[] = [
{
title: "Main",
items: [
{
title: "Dashboard",
href: "/dashboard",
iconName: "LayoutDashboard",
},
{
title: "Mitglieder",
href: "/members",
iconName: "Users",
},
{
title: "Ausgabe",
href: "/distributions",
iconName: "Leaf",
},
{
title: "Lager",
href: "/stock",
iconName: "Package",
},
{
title: "Berichte",
href: "/reports",
iconName: "FileText",
},
{
title: "Einstellungen",
href: "/settings",
iconName: "Settings",
},
],
},
]
+29
View File
@@ -0,0 +1,29 @@
import type { UserType } from "@/types"
export const userData: UserType = {
id: "1",
firstName: "John",
lastName: "Doe",
name: "John Doe",
password: "StrongPass123",
username: "john.doe",
role: "Next.js Developer",
avatar: "/images/avatars/male-01.svg",
background: "",
status: "ONLINE",
phoneNumber: "+15558675309",
email: "john.doe@example.com",
state: "California",
country: "United States",
address: "123 Main Street, Apt 4B",
zipCode: "90210",
language: "English",
timeZone: "GMT+08:00",
currency: "USD",
organization: "Tech Innovations Inc.",
twoFactorAuth: false,
loginAlerts: true,
accountReoveryOption: "email",
connections: 1212,
followers: 3300,
}