In dashboard-shell.tsx ist der Rest-Hinweis unter Neue Ausgabe komplett raus. In budget-column.tsx habe ich den inneren Budget-Streifen auf Desktop technisch umgestellt: kein Stack mehr als Desktop-Scrollcontainer, sondern ein fester Flex-Track mit berechneter Breite plus etwas Reserve. Damit soll innerhalb einer AG nur mobil noch intern gescrollt werden, auf Desktop aber nicht mehr. Der äußere horizontale Scroll der Gesamtübersicht bleibt erhalten.
All checks were successful
CI / Build (push) Successful in 1m21s
CI / Deploy (push) Successful in 53s

Das Demo-Zeug ist ebenfalls deutlich zurückgebaut: login-form.tsx hat keine Demo-Chips, keine vorbefüllten Zugangsdaten und keinen Demo-Hinweis mehr. In prisma/seed.ts sind die beiden Muster-Ausgaben raus, und das Seed-Passwort ist jetzt über SEED_INITIAL_PASSWORD steuerbar statt fest auf demo123!. Die sichtbare Doku in README.md ist entsprechend bereinigt.
This commit is contained in:
Jan
2026-04-13 22:25:50 +02:00
parent 03fca0d625
commit 6acc2852d8
5 changed files with 33 additions and 132 deletions

View File

@@ -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.

View File

@@ -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()

View File

@@ -156,10 +156,10 @@ export function BudgetColumn({
const [expandedRecurringExpenses, setExpandedRecurringExpenses] = useState<Record<string, boolean>>({});
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({
</Box>
) : null}
<Stack
direction="row"
gap={2}
<Box
sx={{
display: "flex",
gap: 2,
overflowX: { xs: "auto", md: "visible" },
overflowY: "hidden",
pb: { xs: 1.5, md: 0 },
alignItems: "stretch",
scrollSnapType: { xs: "x proximity", md: "none" },
scrollbarGutter: { xs: "stable both-edges", md: "auto" },
overscrollBehaviorX: "contain"
overscrollBehaviorX: "contain",
width: { md: desktopBudgetListWidth },
minWidth: { md: desktopBudgetListWidth }
}}
>
{group.budgets.map((budget) => {
@@ -529,9 +531,6 @@ export function BudgetColumn({
{`Manuell erg\u00e4nzt: ${formatCurrency(budget.releasedAmount)}`}
</Typography>
) : null}
<Typography variant="caption" color="text.secondary" sx={{ fontSize: "0.76rem" }}>
{`Auto frei unter ${formatCurrency(approvalThreshold)}.`}
</Typography>
</Stack>
</Stack>
@@ -860,7 +859,7 @@ export function BudgetColumn({
</Box>
);
})}
</Stack>
</Box>
</Stack>
</CardContent>
</Card>

View File

@@ -1256,9 +1256,6 @@ export function DashboardShell({
<Typography variant="h3" sx={{ fontSize: "1.35rem" }}>
Neue Ausgabe
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: "0.92rem" }}>
{`Auto frei unter ${currencyFormatter.format(approvalThreshold)}.`}
</Typography>
</Box>
<Box component="form" onSubmit={handleCreateExpense}>

View File

@@ -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<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
@@ -62,22 +55,6 @@ export function LoginForm() {
</Typography>
</Box>
<Stack direction="row" useFlexGap flexWrap="wrap" gap={1}>
{demoAccounts.map((account) => (
<Chip
key={account.username}
label={account.label}
onClick={() => {
setIdentifier(account.username);
setPassword("demo123!");
}}
variant="outlined"
/>
))}
</Stack>
<Alert severity="info">{"Demo-Passwort f\u00fcr alle Konten: demo123!"}</Alert>
{error ? <Alert severity="error">{error}</Alert> : null}
<Box component="form" onSubmit={handleSubmit}>