diff --git a/README.md b/README.md index 2f76634..08ddd02 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,23 @@ # RFP Finanzübersicht -Material-3-orientierter MVP für die Budgetsteuerung von Vereins-AGs mit rollenbasierter Freigabelogik, PWA-Grundlage und Docker-Setup. +Material-3-orientierte Finanzübersicht für Vereins-AGs mit rollenbasierter Freigabelogik, PWA-Grundlage und Docker-Setup. ## Stack - Next.js 14 mit App Router und TypeScript -- MUI 6 fuer Material Design 3 +- MUI 6 für Material Design 3 - Prisma + PostgreSQL - NextAuth Credentials Login -- Docker Compose fuer lokalen Start +- Docker Compose für lokalen Start -## Enthalten im MVP +## Enthalten -- Horizontale Budget-Dashboard-Ansicht pro AG -- Budget-Fuellstand mit blasser/gefaerbter Visualisierung -- Automatische Freigabe fuer Ausgaben unter 50 EUR -- Drei digitale Freigaben fuer Ausgaben ab 50 EUR -- Rollen `Admin`, `Finanz-AG`, `AG-Mitglied` -- Admin-Formulare fuer Budgets und Nutzeranlage -- Statusaktionen `Freigegeben`, `Bezahlt`, `Dokumentiert` -- Demo-Accounts und Seed-Daten -- PWA-Manifest und Service-Worker-Basis fuer Offline-Shell +- Horizontale Budget-Übersicht pro AG +- Mehrstufige Freigaben mit Rollenlogik +- Budgets, Zeiträume, Nutzerverwaltung und Audit-Log +- Statusaktionen für Freigeben, Bezahlt und Dokumentiert +- CSV-Backup mit Import und Restore-Grundlage +- PWA-Manifest und Service-Worker-Basis ## Lokaler Start @@ -31,35 +28,14 @@ Material-3-orientierter MVP für die Budgetsteuerung von Vereins-AGs mit rollenb docker-compose up --build ``` -3. App oeffnen unter `http://localhost:3000` +3. App öffnen unter `http://localhost:3000` -## Demo-Accounts +## Seed -- `admin.a@raveforpeace.org` / `demo123!` -- `admin.b@raveforpeace.org` / `demo123!` -- `finance@raveforpeace.org` / `demo123!` -- `deko@raveforpeace.org` / `demo123!` -- `technik@raveforpeace.org` / `demo123!` - -## Abnahmekriterien durchspielen - -1. Als `admin.a@raveforpeace.org` anmelden und bei `AG Deko` ein Budget setzen, zum Beispiel `1200`. -2. Als `deko@raveforpeace.org` anmelden und eine Ausgabe ueber `60` EUR erfassen. -3. Als `admin.a@raveforpeace.org` wieder anmelden und `Freigeben als Vorstand A` klicken. -4. Als `admin.b@raveforpeace.org` anmelden und `Freigeben als Vorstand B` klicken. -5. Als `finance@raveforpeace.org` anmelden und die Finanzfreigabe setzen. -6. Nach der dritten Freigabe wird der Posten kraeftig dargestellt. -7. Danach `Bezahlt setzen` klicken, um den blauen Haken zu erhalten. - -## Datenmodell - -- `users`: Nutzer mit Rolle, Login und optionaler AG-Zuordnung. -- `working_groups`: AG-Name, Budget, Farbe -- `expenses`: Betrag, Status, Bezahlt-/Dokumentiert-Zeitstempel, optionaler Beleg-Link. -- `approvals`: Wer hat welche Signatur fuer welchen Posten gesetzt. +Der Seed legt die Grundeinstellungen, den aktiven Zeitraum, AGs, Budgets und Basisnutzer für die Erstkonfiguration an. Optional kann `SEED_INITIAL_PASSWORD` gesetzt werden. ## Hinweise -- Die Dokumentation eines Belegs ist im MVP als Beleg-URL umgesetzt. -- Web-Push ist architektonisch vorbereitet ueber PWA-Grundlage, aber die eigentliche Push-Auslieferung ist noch nicht implementiert. -- Fuer Produktion sollten `NEXTAUTH_SECRET`, Datenbank-Zugangsdaten und Reverse-Proxy/SSL sauber gesetzt werden. +- Die Dokumentation eines Belegs ist aktuell als Beleg-URL umgesetzt. +- Web-Push ist architektonisch vorbereitet, aber noch nicht implementiert. +- Für Produktion sollten `NEXTAUTH_SECRET`, Datenbank-Zugangsdaten und Reverse-Proxy/SSL sauber gesetzt werden. diff --git a/prisma/seed.ts b/prisma/seed.ts index 3a72a0d..9fd8f37 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -122,7 +122,8 @@ async function upsertUser(input: { } async function main() { - const passwordHash = await bcrypt.hash("demo123!", 12); + const initialPassword = process.env.SEED_INITIAL_PASSWORD?.trim() || "Bitte-sofort-aendern-2026!"; + const passwordHash = await bcrypt.hash(initialPassword, 12); await upsertAppSettings(); const currentPeriod = await upsertCurrentPeriod(); @@ -133,7 +134,7 @@ async function main() { await upsertBudget(deko.id, currentPeriod.id, "Deko Hauptbudget", 0, "#FFB94A", 0); await upsertBudget(awareness.id, currentPeriod.id, "Awareness Hauptbudget", 800, "#68A35D", 250); - const technikBudget = await upsertBudget(technik.id, currentPeriod.id, "Technik Infrastruktur", 1500, "#5677F6", 500); + await upsertBudget(technik.id, currentPeriod.id, "Technik Infrastruktur", 1500, "#5677F6", 500); await upsertUser({ username: "vorstand-a", @@ -172,56 +173,7 @@ async function main() { approvalPermissions: [] }); - const existingExpense = await prisma.expense.findFirst({ - where: { title: "Muster: Kabelbinder" } - }); - if (!existingExpense) { - const technikUser = await prisma.user.findUniqueOrThrow({ - where: { username: "technik" } - }); - - await prisma.expense.create({ - data: { - title: "Muster: Kabelbinder", - description: "Beispiel für einen automatisch freigegebenen Infrastrukturposten.", - amount: 24.5, - creatorId: technikUser.id, - agId: technik.id, - budgetId: technikBudget.id, - periodId: currentPeriod.id, - approvalStatus: ApprovalStatus.APPROVED, - proofUrl: null, - recurrence: ExpenseRecurrence.NONE - } - }); - } - - const existingSubscription = await prisma.expense.findFirst({ - where: { title: "Muster: Vereinssoftware Abo" } - }); - - if (!existingSubscription) { - const financeUser = await prisma.user.findUniqueOrThrow({ - where: { username: "finanzen" } - }); - - await prisma.expense.create({ - data: { - title: "Muster: Vereinssoftware Abo", - description: "Monatlich wiederkehrendes Beispielabo für die Übersicht.", - amount: 19, - creatorId: financeUser.id, - agId: technik.id, - budgetId: technikBudget.id, - periodId: currentPeriod.id, - approvalStatus: ApprovalStatus.APPROVED, - proofUrl: null, - recurrence: ExpenseRecurrence.MONTHLY, - recurrenceStartAt: currentPeriod.startsAt - } - }); - } } main() diff --git a/src/components/dashboard/budget-column.tsx b/src/components/dashboard/budget-column.tsx index 470561e..86f044f 100644 --- a/src/components/dashboard/budget-column.tsx +++ b/src/components/dashboard/budget-column.tsx @@ -156,10 +156,10 @@ export function BudgetColumn({ const [expandedRecurringExpenses, setExpandedRecurringExpenses] = useState>({}); const budgetCardWidth = 352; - const groupCardWidth = Math.max( - group.budgets.length * budgetCardWidth + Math.max(group.budgets.length - 1, 0) * 16 + 48, - 440 - ); + const desktopBudgetGap = 16; + const desktopBudgetListWidth = + group.budgets.length * budgetCardWidth + Math.max(group.budgets.length - 1, 0) * desktopBudgetGap; + const groupCardWidth = Math.max(desktopBudgetListWidth + 64, 456); const canEditBudgets = canManageBudgets(viewer.role); useEffect(() => { @@ -368,17 +368,19 @@ export function BudgetColumn({ ) : null} - {group.budgets.map((budget) => { @@ -529,9 +531,6 @@ export function BudgetColumn({ {`Manuell erg\u00e4nzt: ${formatCurrency(budget.releasedAmount)}`} ) : null} - - {`Auto frei unter ${formatCurrency(approvalThreshold)}.`} - @@ -860,7 +859,7 @@ export function BudgetColumn({ ); })} - + diff --git a/src/components/dashboard/dashboard-shell.tsx b/src/components/dashboard/dashboard-shell.tsx index eb5ef2d..732469a 100644 --- a/src/components/dashboard/dashboard-shell.tsx +++ b/src/components/dashboard/dashboard-shell.tsx @@ -1256,9 +1256,6 @@ export function DashboardShell({ Neue Ausgabe - - {`Auto frei unter ${currencyFormatter.format(approvalThreshold)}.`} - diff --git a/src/components/login-form.tsx b/src/components/login-form.tsx index baab7dd..57a1991 100644 --- a/src/components/login-form.tsx +++ b/src/components/login-form.tsx @@ -1,22 +1,15 @@ "use client"; -import { Alert, Box, Button, Card, CardContent, Chip, Stack, TextField, Typography } from "@mui/material"; +import { Alert, Box, Button, Card, CardContent, Stack, TextField, Typography } from "@mui/material"; import { signIn } from "next-auth/react"; import { useRouter } from "next/navigation"; import type { FormEvent } from "react"; import { useState } from "react"; -const demoAccounts = [ - { label: "Vorstand A", username: "vorstand-a" }, - { label: "Vorstand B", username: "vorstand-b" }, - { label: "Finanz-AG", username: "finanzen" }, - { label: "Deko Mitglied", username: "deko" } -]; - export function LoginForm() { const router = useRouter(); - const [identifier, setIdentifier] = useState("vorstand-a"); - const [password, setPassword] = useState("demo123!"); + const [identifier, setIdentifier] = useState(""); + const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -62,22 +55,6 @@ export function LoginForm() { - - {demoAccounts.map((account) => ( - { - setIdentifier(account.username); - setPassword("demo123!"); - }} - variant="outlined" - /> - ))} - - - {"Demo-Passwort f\u00fcr alle Konten: demo123!"} - {error ? {error} : null}