This commit is contained in:
@@ -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<void>;
|
||||
onUpdateDonation: (
|
||||
donationId: string,
|
||||
draft: {
|
||||
title: string;
|
||||
description: string;
|
||||
amount: string;
|
||||
donatedAt: string;
|
||||
target: "GENERAL" | "EXPENSE";
|
||||
workingGroupId: string;
|
||||
expenseId: string;
|
||||
}
|
||||
) => Promise<void>;
|
||||
onDeleteDonation: (donationId: string, title: string) => Promise<void>;
|
||||
};
|
||||
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 (
|
||||
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
||||
<Chip
|
||||
@@ -125,18 +162,24 @@ function StatusChips({ expense }: { expense: DashboardExpense }) {
|
||||
{expense.documentedAt ? (
|
||||
<Chip label="Dokumentiert" color="success" size="small" icon={<TaskAltRoundedIcon />} sx={wrappingChipSx} />
|
||||
) : null}
|
||||
{expense.donationAmount > 0 ? (
|
||||
{expense.donations.map((donation) => (
|
||||
<Chip
|
||||
label={`Spende: ${formatCurrency(expense.donationAmount)}`}
|
||||
key={donation.id}
|
||||
label={`Spende: ${formatCurrency(donation.amount)}`}
|
||||
size="small"
|
||||
onDelete={canEditDonations ? () => onEditDonation(donation) : undefined}
|
||||
deleteIcon={canEditDonations ? <EditRoundedIcon /> : undefined}
|
||||
sx={{
|
||||
...wrappingChipSx,
|
||||
bgcolor: "#F6C343",
|
||||
color: "#1F1600",
|
||||
fontWeight: 700
|
||||
fontWeight: 700,
|
||||
"& .MuiChip-deleteIcon": {
|
||||
color: "#1F1600"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
))}
|
||||
{expense.recurrence === "MONTHLY" ? (
|
||||
<Chip
|
||||
label={recurrenceLabel(expense.recurrence)}
|
||||
@@ -179,7 +222,9 @@ export function BudgetColumn({
|
||||
onSaveBudget,
|
||||
onDeleteBudget,
|
||||
onDeleteExpense,
|
||||
onUpdateExpense
|
||||
onUpdateExpense,
|
||||
onUpdateDonation,
|
||||
onDeleteDonation
|
||||
}: BudgetColumnProps) {
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
@@ -193,9 +238,11 @@ export function BudgetColumn({
|
||||
const [expandedRecurringExpenses, setExpandedRecurringExpenses] = useState<Record<string, boolean>>({});
|
||||
const [expandedExpenseDetails, setExpandedExpenseDetails] = useState<Record<string, boolean>>({});
|
||||
const [editingExpenseId, setEditingExpenseId] = useState<string | null>(null);
|
||||
const [editingAssignedDonationId, setEditingAssignedDonationId] = useState<string | null>(null);
|
||||
const [expenseDrafts, setExpenseDrafts] = useState<
|
||||
Record<string, { title: string; description: string; amount: string; agId: string; budgetId: string; cutoffPhase: "PRE" | "POST" }>
|
||||
>({});
|
||||
const [assignedDonationDrafts, setAssignedDonationDrafts] = useState<Record<string, AssignedDonationDraft>>({});
|
||||
|
||||
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<AssignedDonationDraft>
|
||||
) {
|
||||
setAssignedDonationDrafts((current) => ({
|
||||
...current,
|
||||
[donation.id]: {
|
||||
...getAssignedDonationDraft(expense, donation),
|
||||
...patch
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function resetDraft(budget: DashboardBudget) {
|
||||
setBudgetDrafts((current) => ({
|
||||
...current,
|
||||
@@ -830,9 +904,121 @@ export function BudgetColumn({
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
<StatusChips expense={expense} />
|
||||
<StatusChips
|
||||
expense={expense}
|
||||
canEditDonations={canEditDonations}
|
||||
onEditDonation={(donation) => {
|
||||
setEditingAssignedDonationId(donation.id);
|
||||
setAssignedDonationDrafts((current) => ({
|
||||
...current,
|
||||
[donation.id]: getAssignedDonationDraft(expense, donation)
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{expense.donations.map((donation) => {
|
||||
if (editingAssignedDonationId !== donation.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const donationDraft = getAssignedDonationDraft(expense, donation);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={donation.id}
|
||||
sx={{
|
||||
p: 1.2,
|
||||
borderRadius: "14px",
|
||||
bgcolor: alpha("#F6C343", 0.16),
|
||||
border: `1px solid ${alpha("#F6C343", 0.36)}`
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>
|
||||
Spende bearbeiten
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Titel"
|
||||
size="small"
|
||||
value={donationDraft.title}
|
||||
onChange={(event) => updateAssignedDonationDraft(expense, donation, { title: event.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Betrag in EUR"
|
||||
type="number"
|
||||
size="small"
|
||||
inputProps={{ min: 0.01, step: 0.01 }}
|
||||
value={donationDraft.amount}
|
||||
onChange={(event) => updateAssignedDonationDraft(expense, donation, { amount: event.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Spendendatum"
|
||||
type="date"
|
||||
size="small"
|
||||
value={donationDraft.donatedAt}
|
||||
onChange={(event) => updateAssignedDonationDraft(expense, donation, { donatedAt: event.target.value })}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Beschreibung"
|
||||
size="small"
|
||||
value={donationDraft.description}
|
||||
onChange={(event) =>
|
||||
updateAssignedDonationDraft(expense, donation, { description: event.target.value })
|
||||
}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
/>
|
||||
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
variant="contained"
|
||||
disabled={busy}
|
||||
onClick={async () => {
|
||||
await onUpdateDonation(donation.id, donationDraft);
|
||||
setEditingAssignedDonationId(null);
|
||||
}}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
disabled={busy}
|
||||
onClick={async () => {
|
||||
if (!window.confirm(`Spende "${donation.title}" wirklich löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await onDeleteDonation(donation.id, donation.title);
|
||||
setEditingAssignedDonationId(null);
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
variant="text"
|
||||
disabled={busy}
|
||||
onClick={() => setEditingAssignedDonationId(null)}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
|
||||
Reference in New Issue
Block a user