Monatsauswahl und Spendenbearbeitung ergaenzen
All checks were successful
CI / Build and Deploy (push) Successful in 3m26s
All checks were successful
CI / Build and Deploy (push) Successful in 3m26s
This commit is contained in:
@@ -178,12 +178,17 @@ export default async function DashboardPage() {
|
|||||||
const periodCutoffById = new Map(periodCutoffs.map((period) => [period.id, period]));
|
const periodCutoffById = new Map(periodCutoffs.map((period) => [period.id, period]));
|
||||||
const expenseCutoffById = new Map(expenseCutoffs.map((expense) => [expense.id, expense.cutoff_phase]));
|
const expenseCutoffById = new Map(expenseCutoffs.map((expense) => [expense.id, expense.cutoff_phase]));
|
||||||
const donationsByExpenseId = new Map<string, number>();
|
const donationsByExpenseId = new Map<string, number>();
|
||||||
|
const donationRowsByExpenseId = new Map<string, typeof donationRows>();
|
||||||
for (const donation of donationRows) {
|
for (const donation of donationRows) {
|
||||||
if (donation.expense_id) {
|
if (donation.expense_id) {
|
||||||
donationsByExpenseId.set(
|
donationsByExpenseId.set(
|
||||||
donation.expense_id,
|
donation.expense_id,
|
||||||
(donationsByExpenseId.get(donation.expense_id) ?? 0) + Number(donation.amount)
|
(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,
|
recurrenceStartAt,
|
||||||
cutoffPhase: expenseCutoffById.get(expense.id) ?? "PRE",
|
cutoffPhase: expenseCutoffById.get(expense.id) ?? "PRE",
|
||||||
donationAmount: donationsByExpenseId.get(expense.id) ?? 0,
|
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(
|
netPeriodAmount: Math.max(
|
||||||
0,
|
0,
|
||||||
getExpensePeriodAmount(amount, expense.recurrence, occurrences.length) -
|
getExpensePeriodAmount(amount, expense.recurrence, occurrences.length) -
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ import { alpha, useTheme } from "@mui/material/styles";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { ColorPickerField } from "@/components/dashboard/color-picker-field";
|
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 {
|
import {
|
||||||
approvalLabel,
|
approvalLabel,
|
||||||
canDeleteExpense,
|
canDeleteExpense,
|
||||||
@@ -72,6 +78,19 @@ type BudgetColumnProps = {
|
|||||||
cutoffPhase: "PRE" | "POST";
|
cutoffPhase: "PRE" | "POST";
|
||||||
}
|
}
|
||||||
) => Promise<void>;
|
) => 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 = {
|
type BudgetDraft = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -79,6 +98,16 @@ type BudgetDraft = {
|
|||||||
colorCode: string;
|
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", {
|
const currencyFormatter = new Intl.NumberFormat("de-DE", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "EUR"
|
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 (
|
return (
|
||||||
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
||||||
<Chip
|
<Chip
|
||||||
@@ -125,18 +162,24 @@ function StatusChips({ expense }: { expense: DashboardExpense }) {
|
|||||||
{expense.documentedAt ? (
|
{expense.documentedAt ? (
|
||||||
<Chip label="Dokumentiert" color="success" size="small" icon={<TaskAltRoundedIcon />} sx={wrappingChipSx} />
|
<Chip label="Dokumentiert" color="success" size="small" icon={<TaskAltRoundedIcon />} sx={wrappingChipSx} />
|
||||||
) : null}
|
) : null}
|
||||||
{expense.donationAmount > 0 ? (
|
{expense.donations.map((donation) => (
|
||||||
<Chip
|
<Chip
|
||||||
label={`Spende: ${formatCurrency(expense.donationAmount)}`}
|
key={donation.id}
|
||||||
|
label={`Spende: ${formatCurrency(donation.amount)}`}
|
||||||
size="small"
|
size="small"
|
||||||
|
onDelete={canEditDonations ? () => onEditDonation(donation) : undefined}
|
||||||
|
deleteIcon={canEditDonations ? <EditRoundedIcon /> : undefined}
|
||||||
sx={{
|
sx={{
|
||||||
...wrappingChipSx,
|
...wrappingChipSx,
|
||||||
bgcolor: "#F6C343",
|
bgcolor: "#F6C343",
|
||||||
color: "#1F1600",
|
color: "#1F1600",
|
||||||
fontWeight: 700
|
fontWeight: 700,
|
||||||
|
"& .MuiChip-deleteIcon": {
|
||||||
|
color: "#1F1600"
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
))}
|
||||||
{expense.recurrence === "MONTHLY" ? (
|
{expense.recurrence === "MONTHLY" ? (
|
||||||
<Chip
|
<Chip
|
||||||
label={recurrenceLabel(expense.recurrence)}
|
label={recurrenceLabel(expense.recurrence)}
|
||||||
@@ -179,7 +222,9 @@ export function BudgetColumn({
|
|||||||
onSaveBudget,
|
onSaveBudget,
|
||||||
onDeleteBudget,
|
onDeleteBudget,
|
||||||
onDeleteExpense,
|
onDeleteExpense,
|
||||||
onUpdateExpense
|
onUpdateExpense,
|
||||||
|
onUpdateDonation,
|
||||||
|
onDeleteDonation
|
||||||
}: BudgetColumnProps) {
|
}: BudgetColumnProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
@@ -193,9 +238,11 @@ export function BudgetColumn({
|
|||||||
const [expandedRecurringExpenses, setExpandedRecurringExpenses] = useState<Record<string, boolean>>({});
|
const [expandedRecurringExpenses, setExpandedRecurringExpenses] = useState<Record<string, boolean>>({});
|
||||||
const [expandedExpenseDetails, setExpandedExpenseDetails] = useState<Record<string, boolean>>({});
|
const [expandedExpenseDetails, setExpandedExpenseDetails] = useState<Record<string, boolean>>({});
|
||||||
const [editingExpenseId, setEditingExpenseId] = useState<string | null>(null);
|
const [editingExpenseId, setEditingExpenseId] = useState<string | null>(null);
|
||||||
|
const [editingAssignedDonationId, setEditingAssignedDonationId] = useState<string | null>(null);
|
||||||
const [expenseDrafts, setExpenseDrafts] = useState<
|
const [expenseDrafts, setExpenseDrafts] = useState<
|
||||||
Record<string, { title: string; description: string; amount: string; agId: string; budgetId: string; cutoffPhase: "PRE" | "POST" }>
|
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 budgetCardWidth = 286;
|
||||||
const desktopBudgetGap = 12;
|
const desktopBudgetGap = 12;
|
||||||
@@ -204,6 +251,7 @@ export function BudgetColumn({
|
|||||||
const groupCardWidth = Math.max(desktopBudgetListWidth + 48, 372);
|
const groupCardWidth = Math.max(desktopBudgetListWidth + 48, 372);
|
||||||
const canEditBudgets = canManageBudgets(viewer.role);
|
const canEditBudgets = canManageBudgets(viewer.role);
|
||||||
const canEditExpenses = canManageBudgets(viewer.role);
|
const canEditExpenses = canManageBudgets(viewer.role);
|
||||||
|
const canEditDonations = viewer.role === "ORGA" || viewer.role === "FINANCE";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBudgetDrafts(
|
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) {
|
function resetDraft(budget: DashboardBudget) {
|
||||||
setBudgetDrafts((current) => ({
|
setBudgetDrafts((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -830,9 +904,121 @@ export function BudgetColumn({
|
|||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
<StatusChips expense={expense} />
|
<StatusChips
|
||||||
|
expense={expense}
|
||||||
|
canEditDonations={canEditDonations}
|
||||||
|
onEditDonation={(donation) => {
|
||||||
|
setEditingAssignedDonationId(donation.id);
|
||||||
|
setAssignedDonationDrafts((current) => ({
|
||||||
|
...current,
|
||||||
|
[donation.id]: getAssignedDonationDraft(expense, donation)
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Stack>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -429,6 +429,7 @@ export function DashboardShell({
|
|||||||
const [desktopSection, setDesktopSection] = useState<DesktopSection>("overview");
|
const [desktopSection, setDesktopSection] = useState<DesktopSection>("overview");
|
||||||
const [financeViewMode, setFinanceViewMode] = useState<FinanceViewMode>("monthly");
|
const [financeViewMode, setFinanceViewMode] = useState<FinanceViewMode>("monthly");
|
||||||
const [financePresentation, setFinancePresentation] = useState<FinancePresentation>("charts");
|
const [financePresentation, setFinancePresentation] = useState<FinancePresentation>("charts");
|
||||||
|
const [selectedFinanceMonth, setSelectedFinanceMonth] = useState("ALL");
|
||||||
const [selectedCurrentPeriodId, setSelectedCurrentPeriodId] = useState(currentPeriodId);
|
const [selectedCurrentPeriodId, setSelectedCurrentPeriodId] = useState(currentPeriodId);
|
||||||
const [selectedMobileGroupId, setSelectedMobileGroupId] = useState(visibleGroups[0]?.id ?? "");
|
const [selectedMobileGroupId, setSelectedMobileGroupId] = useState(visibleGroups[0]?.id ?? "");
|
||||||
const [focusedBudgetId, setFocusedBudgetId] = useState<string | null>(null);
|
const [focusedBudgetId, setFocusedBudgetId] = useState<string | null>(null);
|
||||||
@@ -3259,6 +3260,8 @@ export function DashboardShell({
|
|||||||
onDeleteBudget={handleDeleteBudget}
|
onDeleteBudget={handleDeleteBudget}
|
||||||
onDeleteExpense={handleDeleteExpense}
|
onDeleteExpense={handleDeleteExpense}
|
||||||
onUpdateExpense={handleUpdateExpense}
|
onUpdateExpense={handleUpdateExpense}
|
||||||
|
onUpdateDonation={handleUpdateDonation}
|
||||||
|
onDeleteDonation={handleDeleteDonation}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
@@ -3309,6 +3312,8 @@ export function DashboardShell({
|
|||||||
onDeleteBudget={handleDeleteBudget}
|
onDeleteBudget={handleDeleteBudget}
|
||||||
onDeleteExpense={handleDeleteExpense}
|
onDeleteExpense={handleDeleteExpense}
|
||||||
onUpdateExpense={handleUpdateExpense}
|
onUpdateExpense={handleUpdateExpense}
|
||||||
|
onUpdateDonation={handleUpdateDonation}
|
||||||
|
onDeleteDonation={handleDeleteDonation}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
@@ -3350,7 +3355,10 @@ export function DashboardShell({
|
|||||||
row.donations += donation.amount;
|
row.donations += donation.amount;
|
||||||
rows.set(key, row);
|
rows.set(key, row);
|
||||||
}
|
}
|
||||||
return [...rows.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([, row]) => row);
|
return [...rows.entries()]
|
||||||
|
.sort(([left], [right]) => left.localeCompare(right))
|
||||||
|
.filter(([key]) => selectedFinanceMonth === "ALL" || key === selectedFinanceMonth)
|
||||||
|
.map(([, row]) => row);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (financeViewMode === "cutoff") {
|
if (financeViewMode === "cutoff") {
|
||||||
@@ -3397,6 +3405,20 @@ export function DashboardShell({
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
})();
|
})();
|
||||||
|
const financeMonthOptions = (() => {
|
||||||
|
const rows = new Map<string, string>();
|
||||||
|
for (const expense of allExpenses) {
|
||||||
|
const date = new Date(expense.createdAt);
|
||||||
|
const key = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
|
||||||
|
rows.set(key, new Intl.DateTimeFormat("de-DE", { month: "long", year: "numeric" }).format(date));
|
||||||
|
}
|
||||||
|
for (const donation of donations) {
|
||||||
|
const date = new Date(donation.donatedAt);
|
||||||
|
const key = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
|
||||||
|
rows.set(key, new Intl.DateTimeFormat("de-DE", { month: "long", year: "numeric" }).format(date));
|
||||||
|
}
|
||||||
|
return [...rows.entries()].sort(([left], [right]) => left.localeCompare(right));
|
||||||
|
})();
|
||||||
|
|
||||||
const financeOverviewContent = (
|
const financeOverviewContent = (
|
||||||
<Stack spacing={2.5}>
|
<Stack spacing={2.5}>
|
||||||
@@ -3425,6 +3447,22 @@ export function DashboardShell({
|
|||||||
<MenuItem value="charts">Grafisch</MenuItem>
|
<MenuItem value="charts">Grafisch</MenuItem>
|
||||||
<MenuItem value="table">Tabellarisch</MenuItem>
|
<MenuItem value="table">Tabellarisch</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
|
{financeViewMode === "monthly" ? (
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Monat"
|
||||||
|
value={selectedFinanceMonth}
|
||||||
|
onChange={(event) => setSelectedFinanceMonth(event.target.value)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<MenuItem value="ALL">Alle Monate</MenuItem>
|
||||||
|
{financeMonthOptions.map(([monthKey, monthLabel]) => (
|
||||||
|
<MenuItem key={monthKey} value={monthKey}>
|
||||||
|
{monthLabel}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
||||||
<Chip label={`Budget: ${currencyFormatter.format(totals.budget)}`} />
|
<Chip label={`Budget: ${currencyFormatter.format(totals.budget)}`} />
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ export type DashboardExpenseDocument = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DashboardExpenseDonation = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
amount: number;
|
||||||
|
donatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type DashboardExpense = {
|
export type DashboardExpense = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -73,6 +81,7 @@ export type DashboardExpense = {
|
|||||||
recurrenceStartAt: string | null;
|
recurrenceStartAt: string | null;
|
||||||
cutoffPhase: CutoffPhaseValue;
|
cutoffPhase: CutoffPhaseValue;
|
||||||
donationAmount: number;
|
donationAmount: number;
|
||||||
|
donations: DashboardExpenseDonation[];
|
||||||
netPeriodAmount: number;
|
netPeriodAmount: number;
|
||||||
paidAt: string | null;
|
paidAt: string | null;
|
||||||
documentedAt: string | null;
|
documentedAt: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user