From ae3a00a29808092a9d76851cc3d851e3bf0a761c Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 12 May 2026 00:58:34 +0200 Subject: [PATCH] Monatsauswahl und Spendenbearbeitung ergaenzen --- src/app/page.tsx | 12 ++ src/components/dashboard/budget-column.tsx | 202 ++++++++++++++++++- src/components/dashboard/dashboard-shell.tsx | 40 +++- src/lib/dashboard-types.ts | 9 + 4 files changed, 254 insertions(+), 9 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 2b0c214..ad9375c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -178,12 +178,17 @@ export default async function DashboardPage() { const periodCutoffById = new Map(periodCutoffs.map((period) => [period.id, period])); const expenseCutoffById = new Map(expenseCutoffs.map((expense) => [expense.id, expense.cutoff_phase])); const donationsByExpenseId = new Map(); + const donationRowsByExpenseId = new Map(); for (const donation of donationRows) { if (donation.expense_id) { donationsByExpenseId.set( donation.expense_id, (donationsByExpenseId.get(donation.expense_id) ?? 0) + Number(donation.amount) ); + donationRowsByExpenseId.set(donation.expense_id, [ + ...(donationRowsByExpenseId.get(donation.expense_id) ?? []), + donation + ]); } } @@ -245,6 +250,13 @@ export default async function DashboardPage() { recurrenceStartAt, cutoffPhase: expenseCutoffById.get(expense.id) ?? "PRE", donationAmount: donationsByExpenseId.get(expense.id) ?? 0, + donations: (donationRowsByExpenseId.get(expense.id) ?? []).map((donation) => ({ + id: donation.id, + title: donation.title, + description: donation.description, + amount: Number(donation.amount), + donatedAt: donation.donated_at.toISOString() + })), netPeriodAmount: Math.max( 0, getExpensePeriodAmount(amount, expense.recurrence, occurrences.length) - diff --git a/src/components/dashboard/budget-column.tsx b/src/components/dashboard/budget-column.tsx index 518d712..335c902 100644 --- a/src/components/dashboard/budget-column.tsx +++ b/src/components/dashboard/budget-column.tsx @@ -32,7 +32,13 @@ import { alpha, useTheme } from "@mui/material/styles"; import { useEffect, useMemo, useState } from "react"; import { ColorPickerField } from "@/components/dashboard/color-picker-field"; -import type { DashboardBudget, DashboardExpense, DashboardViewer, DashboardWorkingGroup } from "@/lib/dashboard-types"; +import type { + DashboardBudget, + DashboardExpense, + DashboardExpenseDonation, + DashboardViewer, + DashboardWorkingGroup +} from "@/lib/dashboard-types"; import { approvalLabel, canDeleteExpense, @@ -72,6 +78,19 @@ type BudgetColumnProps = { cutoffPhase: "PRE" | "POST"; } ) => Promise; + onUpdateDonation: ( + donationId: string, + draft: { + title: string; + description: string; + amount: string; + donatedAt: string; + target: "GENERAL" | "EXPENSE"; + workingGroupId: string; + expenseId: string; + } + ) => Promise; + onDeleteDonation: (donationId: string, title: string) => Promise; }; type BudgetDraft = { name: string; @@ -79,6 +98,16 @@ type BudgetDraft = { colorCode: string; }; +type AssignedDonationDraft = { + title: string; + description: string; + amount: string; + donatedAt: string; + target: "GENERAL" | "EXPENSE"; + workingGroupId: string; + expenseId: string; +}; + const currencyFormatter = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" @@ -109,7 +138,15 @@ function createDraft(budget: DashboardBudget): BudgetDraft { }; } -function StatusChips({ expense }: { expense: DashboardExpense }) { +function StatusChips({ + expense, + canEditDonations, + onEditDonation +}: { + expense: DashboardExpense; + canEditDonations: boolean; + onEditDonation: (donation: DashboardExpenseDonation) => void; +}) { return ( } sx={wrappingChipSx} /> ) : null} - {expense.donationAmount > 0 ? ( + {expense.donations.map((donation) => ( onEditDonation(donation) : undefined} + deleteIcon={canEditDonations ? : undefined} sx={{ ...wrappingChipSx, bgcolor: "#F6C343", color: "#1F1600", - fontWeight: 700 + fontWeight: 700, + "& .MuiChip-deleteIcon": { + color: "#1F1600" + } }} /> - ) : null} + ))} {expense.recurrence === "MONTHLY" ? ( >({}); const [expandedExpenseDetails, setExpandedExpenseDetails] = useState>({}); const [editingExpenseId, setEditingExpenseId] = useState(null); + const [editingAssignedDonationId, setEditingAssignedDonationId] = useState(null); const [expenseDrafts, setExpenseDrafts] = useState< Record >({}); + const [assignedDonationDrafts, setAssignedDonationDrafts] = useState>({}); const budgetCardWidth = 286; const desktopBudgetGap = 12; @@ -204,6 +251,7 @@ export function BudgetColumn({ const groupCardWidth = Math.max(desktopBudgetListWidth + 48, 372); const canEditBudgets = canManageBudgets(viewer.role); const canEditExpenses = canManageBudgets(viewer.role); + const canEditDonations = viewer.role === "ORGA" || viewer.role === "FINANCE"; useEffect(() => { setBudgetDrafts( @@ -287,6 +335,32 @@ export function BudgetColumn({ })); } + function getAssignedDonationDraft(expense: DashboardExpense, donation: DashboardExpenseDonation): AssignedDonationDraft { + return assignedDonationDrafts[donation.id] ?? { + title: donation.title, + description: donation.description ?? "", + amount: donation.amount.toFixed(2), + donatedAt: donation.donatedAt.slice(0, 10), + target: "EXPENSE", + workingGroupId: group.id, + expenseId: expense.id + }; + } + + function updateAssignedDonationDraft( + expense: DashboardExpense, + donation: DashboardExpenseDonation, + patch: Partial + ) { + setAssignedDonationDrafts((current) => ({ + ...current, + [donation.id]: { + ...getAssignedDonationDraft(expense, donation), + ...patch + } + })); + } + function resetDraft(budget: DashboardBudget) { setBudgetDrafts((current) => ({ ...current, @@ -830,9 +904,121 @@ export function BudgetColumn({ ) : null} - + { + setEditingAssignedDonationId(donation.id); + setAssignedDonationDrafts((current) => ({ + ...current, + [donation.id]: getAssignedDonationDraft(expense, donation) + })); + }} + /> + {expense.donations.map((donation) => { + if (editingAssignedDonationId !== donation.id) { + return null; + } + + const donationDraft = getAssignedDonationDraft(expense, donation); + + return ( + + + + Spende bearbeiten + + updateAssignedDonationDraft(expense, donation, { title: event.target.value })} + fullWidth + /> + updateAssignedDonationDraft(expense, donation, { amount: event.target.value })} + fullWidth + /> + updateAssignedDonationDraft(expense, donation, { donatedAt: event.target.value })} + InputLabelProps={{ shrink: true }} + fullWidth + /> + + updateAssignedDonationDraft(expense, donation, { description: event.target.value }) + } + fullWidth + multiline + minRows={2} + /> + + + + + + + + ); + })} +