From 34e5f96db76754e300d7f45e808c5b68bb5274b3 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 13 Apr 2026 21:22:02 +0200 Subject: [PATCH] =?UTF-8?q?Bereits=20an=20AG=20=C3=BCbergeben=20l=C3=A4uft?= =?UTF-8?q?=20jetzt=20so,=20wie=20du=20es=20beschrieben=20hast:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bezahlt setzen zählt in der Budgetanzeige automatisch mit. Zusätzlich gibt es unter Neue Ausgabe eine eigene Insel für zusätzlich bereits übergebenes Geld, falls das nicht über einzelne Ausgaben läuft. In den Budgetkarten wird das als gestrichelte Querlinie plus eigenem Chip dargestellt --- .../migration.sql | 1 + prisma/schema.prisma | 1 + prisma/seed.ts | 11 +- src/app/api/audit-logs/[id]/restore/route.ts | 2 + src/app/api/budgets/[id]/route.ts | 28 ++- src/app/api/budgets/route.ts | 35 ++- src/app/api/export/csv/route.ts | 6 + src/app/api/import/csv/route.ts | 8 +- src/app/page.tsx | 1 + src/components/dashboard/budget-column.tsx | 40 ++++ src/components/dashboard/dashboard-shell.tsx | 199 ++++++++++++++++++ src/lib/audit-snapshots.ts | 5 +- src/lib/dashboard-types.ts | 1 + 13 files changed, 317 insertions(+), 21 deletions(-) create mode 100644 prisma/migrations/202604131955_budget_released_amount/migration.sql diff --git a/prisma/migrations/202604131955_budget_released_amount/migration.sql b/prisma/migrations/202604131955_budget_released_amount/migration.sql new file mode 100644 index 0000000..ded6589 --- /dev/null +++ b/prisma/migrations/202604131955_budget_released_amount/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "budgets" ADD COLUMN "released_amount" DECIMAL(10, 2) NOT NULL DEFAULT 0; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d8b031f..7f45b58 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -88,6 +88,7 @@ model Budget { id String @id @default(cuid()) name String totalBudget Decimal @db.Decimal(10, 2) @map("total_budget") + releasedAmount Decimal @default(0) @db.Decimal(10, 2) @map("released_amount") colorCode String @map("color_code") workingGroupId String @map("working_group_id") periodId String @map("period_id") diff --git a/prisma/seed.ts b/prisma/seed.ts index 6499ed8..3a72a0d 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -60,7 +60,8 @@ async function upsertBudget( periodId: string, name: string, totalBudget: number, - colorCode: string + colorCode: string, + releasedAmount = 0 ) { return prisma.budget.upsert({ where: { @@ -72,11 +73,13 @@ async function upsertBudget( }, update: { totalBudget, + releasedAmount, colorCode }, create: { name, totalBudget, + releasedAmount, colorCode, workingGroupId, periodId @@ -128,9 +131,9 @@ async function main() { const awareness = await upsertWorkingGroup("AG Awareness"); const technik = await upsertWorkingGroup("AG Technik"); - await upsertBudget(deko.id, currentPeriod.id, "Deko Hauptbudget", 0, "#FFB94A"); - await upsertBudget(awareness.id, currentPeriod.id, "Awareness Hauptbudget", 800, "#68A35D"); - const technikBudget = await upsertBudget(technik.id, currentPeriod.id, "Technik Infrastruktur", 1500, "#5677F6"); + 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 upsertUser({ username: "vorstand-a", diff --git a/src/app/api/audit-logs/[id]/restore/route.ts b/src/app/api/audit-logs/[id]/restore/route.ts index 67dacc2..491d5a4 100644 --- a/src/app/api/audit-logs/[id]/restore/route.ts +++ b/src/app/api/audit-logs/[id]/restore/route.ts @@ -159,6 +159,7 @@ export async function POST(_: Request, { params }: Context) { data: { name: asString(previous.name, "Budgetname"), totalBudget: asNumber(previous.totalBudget, "Budgetbetrag"), + releasedAmount: asNumber(previous.releasedAmount ?? 0, "Zus\u00e4tzliche Mittel\u00fcbergabe"), colorCode: asString(previous.colorCode, "Budgetfarbe") } }); @@ -173,6 +174,7 @@ export async function POST(_: Request, { params }: Context) { id: asString(deleted.id, "Budget-ID"), name: asString(deleted.name, "Budgetname"), totalBudget: asNumber(deleted.totalBudget, "Budgetbetrag"), + releasedAmount: asNumber(deleted.releasedAmount ?? 0, "Zus\u00e4tzliche Mittel\u00fcbergabe"), colorCode: asString(deleted.colorCode, "Budgetfarbe"), workingGroupId: asString(deleted.workingGroupId, "AG-ID"), periodId: asString(deleted.periodId, "Zeitraum-ID"), diff --git a/src/app/api/budgets/[id]/route.ts b/src/app/api/budgets/[id]/route.ts index 9bfe55d..fd9d6d7 100644 --- a/src/app/api/budgets/[id]/route.ts +++ b/src/app/api/budgets/[id]/route.ts @@ -8,11 +8,22 @@ import { canManageBudgets } from "@/lib/domain"; import prisma from "@/lib/prisma"; import { getCurrentViewer } from "@/lib/session"; -const updateBudgetSchema = z.object({ - name: z.string().trim().min(2).max(80), - totalBudget: z.coerce.number().min(0), - colorCode: z.string().regex(/^#([0-9a-fA-F]{6})$/) -}); +const updateBudgetSchema = z + .object({ + name: z.string().trim().min(2).max(80), + totalBudget: z.coerce.number().min(0), + releasedAmount: z.coerce.number().min(0).optional(), + colorCode: z.string().regex(/^#([0-9a-fA-F]{6})$/) + }) + .superRefine((value, ctx) => { + if (value.releasedAmount !== undefined && value.releasedAmount > value.totalBudget) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Der bereits an die AG uebergebene Betrag darf das Budget nicht uebersteigen.", + path: ["releasedAmount"] + }); + } + }); type Context = { params: { @@ -43,16 +54,18 @@ export async function PATCH(request: Request, { params }: Context) { const parsed = updateBudgetSchema.safeParse(body); if (!parsed.success) { - return NextResponse.json({ error: "Budgetname, Betrag oder Farbe sind ungueltig." }, { status: 400 }); + return NextResponse.json({ error: "Budgetname, Betrag, Mitteluebergabe oder Farbe sind ungueltig." }, { status: 400 }); } try { const previousBudget = budget; + const nextReleasedAmount = parsed.data.releasedAmount ?? Number(previousBudget.releasedAmount); const updatedBudget = await prisma.budget.update({ where: { id: params.id }, data: { name: parsed.data.name, totalBudget: parsed.data.totalBudget, + releasedAmount: nextReleasedAmount, colorCode: parsed.data.colorCode } }); @@ -66,6 +79,7 @@ export async function PATCH(request: Request, { params }: Context) { summary: `Budget ${updatedBudget.name} wurde aktualisiert.`, metadata: { totalBudget: parsed.data.totalBudget, + releasedAmount: nextReleasedAmount, colorCode: parsed.data.colorCode, rollback: { kind: "budget.update", @@ -99,7 +113,7 @@ export async function DELETE(_: Request, { params }: Context) { } if (!canManageBudgets(viewer.role)) { - return NextResponse.json({ error: "Nur Vorstand oder Finanz-AG dürfen Budgets löschen." }, { status: 403 }); + return NextResponse.json({ error: "Nur Vorstand oder Finanz-AG duerfen Budgets loeschen." }, { status: 403 }); } const budget = await prisma.budget.findUnique({ diff --git a/src/app/api/budgets/route.ts b/src/app/api/budgets/route.ts index c79240e..928ffe7 100644 --- a/src/app/api/budgets/route.ts +++ b/src/app/api/budgets/route.ts @@ -7,13 +7,24 @@ import { canManageBudgets } from "@/lib/domain"; import prisma from "@/lib/prisma"; import { getCurrentViewer } from "@/lib/session"; -const budgetSchema = z.object({ - workingGroupId: z.string().trim().min(1), - periodId: z.string().trim().min(1), - name: z.string().trim().min(2).max(80), - totalBudget: z.coerce.number().min(0), - colorCode: z.string().regex(/^#([0-9a-fA-F]{6})$/) -}); +const budgetSchema = z + .object({ + workingGroupId: z.string().trim().min(1), + periodId: z.string().trim().min(1), + name: z.string().trim().min(2).max(80), + totalBudget: z.coerce.number().min(0), + releasedAmount: z.coerce.number().min(0).optional(), + colorCode: z.string().regex(/^#([0-9a-fA-F]{6})$/) + }) + .superRefine((value, ctx) => { + if (value.releasedAmount !== undefined && value.releasedAmount > value.totalBudget) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Der bereits an die AG uebergebene Betrag darf das Budget nicht uebersteigen.", + path: ["releasedAmount"] + }); + } + }); export async function POST(request: Request) { const viewer = await getCurrentViewer(); @@ -30,7 +41,10 @@ export async function POST(request: Request) { const parsed = budgetSchema.safeParse(body); if (!parsed.success) { - return NextResponse.json({ error: "Bitte AG, Budgetname, Betrag und Farbe korrekt angeben." }, { status: 400 }); + return NextResponse.json( + { error: "Bitte AG, Budgetname, Betrag, Mitteluebergabe und Farbe korrekt angeben." }, + { status: 400 } + ); } const workingGroup = await prisma.workingGroup.findUnique({ @@ -63,6 +77,8 @@ export async function POST(request: Request) { } }); + const nextReleasedAmount = parsed.data.releasedAmount ?? Number(existingBudget?.releasedAmount ?? 0); + const budget = existingBudget ? await prisma.budget.update({ where: { @@ -70,6 +86,7 @@ export async function POST(request: Request) { }, data: { totalBudget: parsed.data.totalBudget, + releasedAmount: nextReleasedAmount, colorCode: parsed.data.colorCode } }) @@ -79,6 +96,7 @@ export async function POST(request: Request) { periodId: accountingPeriod.id, name: parsed.data.name, totalBudget: parsed.data.totalBudget, + releasedAmount: nextReleasedAmount, colorCode: parsed.data.colorCode } }); @@ -98,6 +116,7 @@ export async function POST(request: Request) { periodId: accountingPeriod.id, periodName: accountingPeriod.name, totalBudget: parsed.data.totalBudget, + releasedAmount: nextReleasedAmount, rollback: existingBudget ? { kind: "budget.update", diff --git a/src/app/api/export/csv/route.ts b/src/app/api/export/csv/route.ts index f1945bd..4615a57 100644 --- a/src/app/api/export/csv/route.ts +++ b/src/app/api/export/csv/route.ts @@ -33,6 +33,7 @@ const CSV_HEADERS = [ "description", "amount", "totalBudget", + "releasedAmount", "colorCode", "approvalStatus", "approvalType", @@ -198,6 +199,7 @@ export async function GET() { description: "", amount: "", totalBudget: "", + releasedAmount: "", colorCode: "", approvalStatus: "", approvalType: "", @@ -250,6 +252,7 @@ export async function GET() { description: "", amount: "", totalBudget: "", + releasedAmount: "", colorCode: "", approvalStatus: "", approvalType: "", @@ -302,6 +305,7 @@ export async function GET() { description: "", amount: "", totalBudget: group.budgets.reduce((sum, budget) => sum + Number(budget.totalBudget), 0).toFixed(2), + releasedAmount: group.budgets.reduce((sum, budget) => sum + Number(budget.releasedAmount), 0).toFixed(2), colorCode: "", approvalStatus: "", approvalType: "", @@ -353,6 +357,7 @@ export async function GET() { description: "", amount: "", totalBudget: Number(budget.totalBudget).toFixed(2), + releasedAmount: Number(budget.releasedAmount).toFixed(2), colorCode: budget.colorCode, approvalStatus: "", approvalType: "", @@ -510,6 +515,7 @@ export async function GET() { description: "", amount: "", totalBudget: "", + releasedAmount: "", colorCode: "", approvalStatus: "", approvalType: "", diff --git a/src/app/api/import/csv/route.ts b/src/app/api/import/csv/route.ts index 16dcfac..f7a047c 100644 --- a/src/app/api/import/csv/route.ts +++ b/src/app/api/import/csv/route.ts @@ -167,9 +167,14 @@ export async function POST(request: Request) { for (const row of budgetRows) { const totalBudget = toNumber(row.totalBudget); + const releasedAmount = toNumber(row.releasedAmount) ?? 0; if (totalBudget === null) { - throw new Error(`Budget ${row.budgetName || row.id} enthält keinen gültigen Betrag.`); + throw new Error(`Budget ${row.budgetName || row.id} enth\u00e4lt keinen g\u00fcltigen Betrag.`); + } + + if (releasedAmount > totalBudget) { + throw new Error(`Budget ${row.budgetName || row.id} enth\u00e4lt eine zu hohe zus\u00e4tzliche Mittel\u00fcbergabe.`); } await tx.budget.create({ @@ -177,6 +182,7 @@ export async function POST(request: Request) { id: row.id, name: row.budgetName, totalBudget, + releasedAmount, colorCode: row.colorCode, workingGroupId: row.workingGroupId, periodId: row.periodId, diff --git a/src/app/page.tsx b/src/app/page.tsx index 4e764dd..27c8c1c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -150,6 +150,7 @@ export default async function DashboardPage() { id: budget.id, name: budget.name, totalBudget: Number(budget.totalBudget), + releasedAmount: Number(budget.releasedAmount), colorCode: budget.colorCode, periodId: budget.periodId, expenses: budget.expenses.map((expense) => { diff --git a/src/components/dashboard/budget-column.tsx b/src/components/dashboard/budget-column.tsx index b845466..963a42f 100644 --- a/src/components/dashboard/budget-column.tsx +++ b/src/components/dashboard/budget-column.tsx @@ -128,6 +128,10 @@ function getPendingSpend(expenses: DashboardExpense[]) { return expenses.reduce((sum, expense) => sum + (expense.approvalStatus === "PENDING" ? expense.periodAmount : 0), 0); } +function getPaidSpend(expenses: DashboardExpense[]) { + return expenses.reduce((sum, expense) => sum + (expense.paidAt ? expense.periodAmount : 0), 0); +} + export function BudgetColumn({ group, viewer, @@ -182,6 +186,10 @@ export function BudgetColumn({ () => group.budgets.reduce((sum, budget) => sum + getPendingSpend(budget.expenses), 0), [group.budgets] ); + const releasedSpend = useMemo( + () => group.budgets.reduce((sum, budget) => sum + budget.releasedAmount + getPaidSpend(budget.expenses), 0), + [group.budgets] + ); const totalCommitted = approvedSpend + pendingSpend; const remainingBudget = group.totalBudget - totalCommitted; @@ -340,6 +348,12 @@ export function BudgetColumn({ color={remainingBudget < 0 ? "error" : "default"} sx={{ ...wrappingChipSx, width: "fit-content" }} /> + group.totalBudget ? "error" : "default"} + variant="outlined" + sx={{ ...wrappingChipSx, width: "fit-content" }} + /> {group.budgets.length === 0 ? ( @@ -372,11 +386,15 @@ export function BudgetColumn({ const isEditing = editingBudgetId === budget.id; const budgetApproved = getApprovedSpend(budget.expenses); const budgetPending = getPendingSpend(budget.expenses); + const budgetReleasedByPayments = getPaidSpend(budget.expenses); + const budgetReleasedTotal = budget.releasedAmount + budgetReleasedByPayments; const budgetCommitted = budgetApproved + budgetPending; const budgetRemaining = budget.totalBudget - budgetCommitted; const approvedPercent = budget.totalBudget > 0 ? Math.min((budgetApproved / budget.totalBudget) * 100, 100) : 0; const cumulativePercent = budget.totalBudget > 0 ? Math.min((budgetCommitted / budget.totalBudget) * 100, 100) : 0; + const releasedPercent = + budget.totalBudget > 0 ? Math.min((budgetReleasedTotal / budget.totalBudget) * 100, 100) : 0; return ( + {budgetReleasedTotal > 0 ? ( + + ) : null} @@ -487,6 +518,15 @@ export function BudgetColumn({ color={budgetRemaining < 0 ? "error" : "default"} sx={{ ...wrappingChipSx, width: "fit-content" }} /> + budget.totalBudget ? "error" : "default"} + variant="outlined" + sx={{ ...wrappingChipSx, width: "fit-content" }} + /> + + {`Davon zusätzlich ohne Einzelposten erfasst: ${formatCurrency(budget.releasedAmount)}. "Bezahlt setzen" zählt automatisch mit.`} + {`Unter ${formatCurrency(approvalThreshold)} werden sofort freigegeben. Größere Ausgaben bleiben blass, bis alle drei Signaturen vorliegen.`} diff --git a/src/components/dashboard/dashboard-shell.tsx b/src/components/dashboard/dashboard-shell.tsx index 87de43c..44f286f 100644 --- a/src/components/dashboard/dashboard-shell.tsx +++ b/src/components/dashboard/dashboard-shell.tsx @@ -77,6 +77,12 @@ type BudgetFormState = { colorCode: string; }; +type BudgetReleaseFormState = { + workingGroupId: string; + budgetId: string; + releasedAmount: string; +}; + type WorkingGroupFormState = { name: string; }; @@ -243,6 +249,11 @@ export function DashboardShell({ totalBudget: "1200", colorCode: "#FFB94A" }); + const [budgetReleaseForm, setBudgetReleaseForm] = useState({ + workingGroupId: visibleGroups[0]?.id ?? "", + budgetId: visibleGroups[0]?.budgets[0]?.id ?? "", + releasedAmount: (visibleGroups[0]?.budgets[0]?.releasedAmount ?? 0).toFixed(2) + }); const [workingGroupForm, setWorkingGroupForm] = useState({ name: "" }); @@ -310,6 +321,52 @@ export function DashboardShell({ } }, [budgetForm.workingGroupId, visibleGroups]); + useEffect(() => { + if (visibleGroups.length === 0) { + setBudgetReleaseForm({ + workingGroupId: "", + budgetId: "", + releasedAmount: "0.00" + }); + return; + } + + const selectedGroup = visibleGroups.find((group) => group.id === budgetReleaseForm.workingGroupId) ?? visibleGroups[0]; + const selectedBudget = selectedGroup?.budgets.find((budget) => budget.id === budgetReleaseForm.budgetId) ?? selectedGroup?.budgets[0]; + + if ( + budgetReleaseForm.workingGroupId !== (selectedGroup?.id ?? "") || + budgetReleaseForm.budgetId !== (selectedBudget?.id ?? "") + ) { + setBudgetReleaseForm({ + workingGroupId: selectedGroup?.id ?? "", + budgetId: selectedBudget?.id ?? "", + releasedAmount: (selectedBudget?.releasedAmount ?? 0).toFixed(2) + }); + } + }, [budgetReleaseForm.budgetId, budgetReleaseForm.workingGroupId, visibleGroups]); + + useEffect(() => { + if (!budgetReleaseForm.budgetId) { + return; + } + + const selectedGroup = visibleGroups.find((group) => group.id === budgetReleaseForm.workingGroupId) ?? visibleGroups[0]; + const selectedBudget = selectedGroup?.budgets.find((budget) => budget.id === budgetReleaseForm.budgetId); + + if (!selectedBudget) { + return; + } + + const nextReleasedAmount = selectedBudget.releasedAmount.toFixed(2); + if (budgetReleaseForm.releasedAmount !== nextReleasedAmount) { + setBudgetReleaseForm((current) => ({ + ...current, + releasedAmount: nextReleasedAmount + })); + } + }, [budgetReleaseForm.budgetId, budgetReleaseForm.workingGroupId, visibleGroups]); + useEffect(() => { if (!message || message.type !== "success") { return; @@ -375,6 +432,18 @@ export function DashboardShell({ const mobileSelectedGroup = visibleGroups.find((group) => group.id === selectedMobileGroupId) ?? visibleGroups[0]; const selectedBudgetWorkingGroup = visibleGroups.find((group) => group.id === budgetForm.workingGroupId) ?? null; + const selectedBudgetReleaseGroup = + visibleGroups.find((group) => group.id === budgetReleaseForm.workingGroupId) ?? visibleGroups[0] ?? null; + const selectedBudgetReleaseOptions = selectedBudgetReleaseGroup?.budgets ?? []; + const selectedBudgetReleaseBudget = + selectedBudgetReleaseOptions.find((budget) => budget.id === budgetReleaseForm.budgetId) ?? + selectedBudgetReleaseOptions[0] ?? + null; + const selectedBudgetReleasePaidAmount = + selectedBudgetReleaseBudget?.expenses.reduce( + (sum, expense) => sum + (expense.paidAt ? expense.periodAmount : 0), + 0 + ) ?? 0; const selectedPeriodForManagement = accountingPeriods.find((period) => period.id === selectedCurrentPeriodId) ?? currentPeriod ?? null; @@ -645,6 +714,44 @@ export function DashboardShell({ }, "Budget wurde aktualisiert."); } + async function handleSaveBudgetRelease(event: FormEvent) { + event.preventDefault(); + + if (!selectedBudgetReleaseBudget) { + setMessage({ type: "error", text: "Bitte zuerst ein Budget ausw\u00e4hlen." }); + return; + } + + const nextReleasedAmount = Number(budgetReleaseForm.releasedAmount.replace(",", ".")); + + if (!Number.isFinite(nextReleasedAmount) || nextReleasedAmount < 0) { + setMessage({ type: "error", text: "Bitte einen g\u00fcltigen zus\u00e4tzlichen \u00dcbergabebetrag eingeben." }); + return; + } + + await runAction(async () => { + await parseResponse( + await fetch(`/api/budgets/${selectedBudgetReleaseBudget.id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + name: selectedBudgetReleaseBudget.name, + totalBudget: selectedBudgetReleaseBudget.totalBudget, + releasedAmount: nextReleasedAmount, + colorCode: selectedBudgetReleaseBudget.colorCode + }) + }) + ); + + setBudgetReleaseForm((current) => ({ + ...current, + releasedAmount: nextReleasedAmount.toFixed(2) + })); + }, `Zus\u00e4tzliche Mittel\u00fcbergabe f\u00fcr ${selectedBudgetReleaseBudget.name} wurde gespeichert.`); + } + async function handleDeleteBudget(budgetId: string) { await runAction(async () => { await parseResponse( @@ -1274,6 +1381,98 @@ export function DashboardShell({ ) : null} + {canManagePeriods && (isCompactLayout || desktopSection === "overview") ? ( + + + + + + {"Bereits an AG übergeben"} + + + {"\"Bezahlt setzen\" z\u00e4hlt automatisch mit. Hier erg\u00e4nzt du nur zus\u00e4tzlich bereits \u00fcbergebenes Geld, das nicht \u00fcber eine einzelne Ausgabe l\u00e4uft."} + + + + + + + setBudgetReleaseForm((current) => ({ + ...current, + workingGroupId: event.target.value, + budgetId: "" + })) + } + required + fullWidth + disabled={visibleGroups.length === 0} + helperText={visibleGroups.length === 0 ? "Lege zuerst eine AG an." : "Wähle die AG mit dem betroffenen Budget."} + > + {visibleGroups.map((group) => ( + + {group.name} + + ))} + + + setBudgetReleaseForm((current) => ({ + ...current, + budgetId: event.target.value + })) + } + required + fullWidth + disabled={selectedBudgetReleaseOptions.length === 0} + helperText={ + selectedBudgetReleaseOptions.length === 0 + ? "In dieser AG gibt es noch kein Budget." + : "Die gestrichelte Linie im Budget zeigt die gesamte Mittelübergabe inklusive bezahlter Posten." + } + > + {selectedBudgetReleaseOptions.map((budget) => ( + + {budget.name} + + ))} + + + setBudgetReleaseForm((current) => ({ + ...current, + releasedAmount: event.target.value + })) + } + required + fullWidth + disabled={!selectedBudgetReleaseBudget} + helperText={ + selectedBudgetReleaseBudget + ? `Automatisch \u00fcber Bezahlt: ${currencyFormatter.format(selectedBudgetReleasePaidAmount)} | Zus\u00e4tzlich erfasst: ${currencyFormatter.format(selectedBudgetReleaseBudget.releasedAmount)}` + : "Wähle zuerst ein Budget aus." + } + /> + + + + + + + ) : null} + {canManageBudgets(viewer.role) && (isCompactLayout || desktopSection === "budgetGroups") ? ( diff --git a/src/lib/audit-snapshots.ts b/src/lib/audit-snapshots.ts index d2e6bc2..9fa8e65 100644 --- a/src/lib/audit-snapshots.ts +++ b/src/lib/audit-snapshots.ts @@ -27,11 +27,14 @@ export function snapshotAppSettings(settings: Pick) { +export function snapshotBudget( + budget: Pick +) { return { id: budget.id, name: budget.name, totalBudget: Number(budget.totalBudget), + releasedAmount: Number(budget.releasedAmount), colorCode: budget.colorCode, workingGroupId: budget.workingGroupId, periodId: budget.periodId, diff --git a/src/lib/dashboard-types.ts b/src/lib/dashboard-types.ts index 213c411..b9c7965 100644 --- a/src/lib/dashboard-types.ts +++ b/src/lib/dashboard-types.ts @@ -62,6 +62,7 @@ export type DashboardBudget = { id: string; name: string; totalBudget: number; + releasedAmount: number; colorCode: string; periodId: string; expenses: DashboardExpense[];