Stichtag Timeline fuer Ausgaben und Finanzuebersicht
All checks were successful
CI / Build and Deploy (push) Successful in 2m58s
All checks were successful
CI / Build and Deploy (push) Successful in 2m58s
This commit is contained in:
@@ -51,6 +51,13 @@ import {
|
|||||||
requiresManualApproval
|
requiresManualApproval
|
||||||
} from "@/lib/domain";
|
} from "@/lib/domain";
|
||||||
|
|
||||||
|
type CutoffSelectionOption = {
|
||||||
|
value: string;
|
||||||
|
cutoffId: string;
|
||||||
|
cutoffPhase: "PRE" | "POST";
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
type BudgetColumnProps = {
|
type BudgetColumnProps = {
|
||||||
group: DashboardWorkingGroup;
|
group: DashboardWorkingGroup;
|
||||||
workingGroups: DashboardWorkingGroup[];
|
workingGroups: DashboardWorkingGroup[];
|
||||||
@@ -141,6 +148,42 @@ function createDraft(budget: DashboardBudget): BudgetDraft {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createCutoffSelectionValue(cutoffId: string, cutoffPhase: "PRE" | "POST") {
|
||||||
|
return `${cutoffId}:${cutoffPhase}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCutoffSelectionValue(value: string) {
|
||||||
|
const [cutoffId, cutoffPhase] = value.split(":");
|
||||||
|
|
||||||
|
return {
|
||||||
|
cutoffId: cutoffId ?? "",
|
||||||
|
cutoffPhase: cutoffPhase === "POST" ? "POST" : "PRE"
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCutoffSelectionOptions(cutoffs: DashboardPeriodCutoff[]): CutoffSelectionOption[] {
|
||||||
|
if (cutoffs.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CutoffSelectionOption[] = cutoffs.map((cutoff) => ({
|
||||||
|
value: createCutoffSelectionValue(cutoff.id, "PRE"),
|
||||||
|
cutoffId: cutoff.id,
|
||||||
|
cutoffPhase: "PRE" as const,
|
||||||
|
label: `Pre ${cutoff.name}`
|
||||||
|
}));
|
||||||
|
const lastCutoff = cutoffs[cutoffs.length - 1];
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
value: createCutoffSelectionValue(lastCutoff.id, "POST"),
|
||||||
|
cutoffId: lastCutoff.id,
|
||||||
|
cutoffPhase: "POST" as const,
|
||||||
|
label: `Post ${lastCutoff.name}`
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
function StatusChips({
|
function StatusChips({
|
||||||
expense,
|
expense,
|
||||||
canEditDonations,
|
canEditDonations,
|
||||||
@@ -1078,8 +1121,12 @@ export function BudgetColumn({
|
|||||||
const editGroup =
|
const editGroup =
|
||||||
workingGroups.find((entry) => entry.id === draft.agId) ?? workingGroups[0] ?? group;
|
workingGroups.find((entry) => entry.id === draft.agId) ?? workingGroups[0] ?? group;
|
||||||
const editBudgets = editGroup.budgets;
|
const editBudgets = editGroup.budgets;
|
||||||
const selectedCutoff =
|
const cutoffOptions = getCutoffSelectionOptions(cutoffs);
|
||||||
cutoffs.find((cutoff) => cutoff.id === draft.cutoffId) ?? cutoffs[0] ?? null;
|
const selectedCutoffValue = createCutoffSelectionValue(draft.cutoffId, draft.cutoffPhase);
|
||||||
|
const selectedCutoffOption =
|
||||||
|
cutoffOptions.find((option) => option.value === selectedCutoffValue) ??
|
||||||
|
cutoffOptions[0] ??
|
||||||
|
null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
@@ -1143,38 +1190,24 @@ export function BudgetColumn({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<Stack direction={{ xs: "column", sm: "row" }} gap={1}>
|
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
label="Stichtag"
|
label="Stichtag-Zuordnung"
|
||||||
size="small"
|
size="small"
|
||||||
value={draft.cutoffId}
|
value={selectedCutoffOption?.value ?? ""}
|
||||||
onChange={(event) => updateExpenseDraft(expense, { cutoffId: event.target.value })}
|
onChange={(event) => {
|
||||||
|
const next = parseCutoffSelectionValue(event.target.value);
|
||||||
|
updateExpenseDraft(expense, next);
|
||||||
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={cutoffs.length === 0}
|
disabled={cutoffOptions.length === 0}
|
||||||
>
|
>
|
||||||
{cutoffs.map((cutoff) => (
|
{cutoffOptions.map((option) => (
|
||||||
<MenuItem key={cutoff.id} value={cutoff.id}>
|
<MenuItem key={option.value} value={option.value}>
|
||||||
{cutoff.name}
|
{option.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
label="Zuordnung"
|
|
||||||
size="small"
|
|
||||||
value={draft.cutoffPhase}
|
|
||||||
onChange={(event) =>
|
|
||||||
updateExpenseDraft(expense, {
|
|
||||||
cutoffPhase: event.target.value as "PRE" | "POST"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<MenuItem value="PRE">{`Pre ${selectedCutoff?.name ?? "Open Air"}`}</MenuItem>
|
|
||||||
<MenuItem value="POST">{`Post ${selectedCutoff?.name ?? "Open Air"}`}</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
</Stack>
|
|
||||||
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1182,7 +1215,11 @@ export function BudgetColumn({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={busy || !draft.budgetId}
|
disabled={busy || !draft.budgetId}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await onUpdateExpense(expense.id, draft);
|
await onUpdateExpense(expense.id, {
|
||||||
|
...draft,
|
||||||
|
cutoffId: selectedCutoffOption?.cutoffId ?? draft.cutoffId,
|
||||||
|
cutoffPhase: selectedCutoffOption?.cutoffPhase ?? draft.cutoffPhase
|
||||||
|
});
|
||||||
setEditingExpenseId(null);
|
setEditingExpenseId(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import type {
|
|||||||
DashboardAuditLog,
|
DashboardAuditLog,
|
||||||
DashboardDonation,
|
DashboardDonation,
|
||||||
DashboardManagedUser,
|
DashboardManagedUser,
|
||||||
|
DashboardPeriodCutoff,
|
||||||
DashboardSettings,
|
DashboardSettings,
|
||||||
DashboardViewer,
|
DashboardViewer,
|
||||||
DashboardWorkingGroup
|
DashboardWorkingGroup
|
||||||
@@ -223,6 +224,12 @@ type MobileAction =
|
|||||||
type FinanceViewMode = "monthly" | "yearly" | "cutoff";
|
type FinanceViewMode = "monthly" | "yearly" | "cutoff";
|
||||||
type FinancePresentation = "charts" | "table";
|
type FinancePresentation = "charts" | "table";
|
||||||
type DesktopSection = "overview" | "finance" | "budgetGroups" | "periods" | "users" | "logs";
|
type DesktopSection = "overview" | "finance" | "budgetGroups" | "periods" | "users" | "logs";
|
||||||
|
type CutoffSelectionOption = {
|
||||||
|
value: string;
|
||||||
|
cutoffId: string;
|
||||||
|
cutoffPhase: "PRE" | "POST";
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
const currencyFormatter = new Intl.NumberFormat("de-DE", {
|
const currencyFormatter = new Intl.NumberFormat("de-DE", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "EUR"
|
currency: "EUR"
|
||||||
@@ -241,6 +248,68 @@ function toDateInputValue(value: string) {
|
|||||||
return value.slice(0, 10);
|
return value.slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createCutoffSelectionValue(cutoffId: string, cutoffPhase: "PRE" | "POST") {
|
||||||
|
return `${cutoffId}:${cutoffPhase}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCutoffSelectionValue(value: string) {
|
||||||
|
const [cutoffId, cutoffPhase] = value.split(":");
|
||||||
|
|
||||||
|
return {
|
||||||
|
cutoffId: cutoffId ?? "",
|
||||||
|
cutoffPhase: cutoffPhase === "POST" ? "POST" : "PRE"
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCutoffSelectionOptions(cutoffs: DashboardPeriodCutoff[]): CutoffSelectionOption[] {
|
||||||
|
if (cutoffs.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CutoffSelectionOption[] = cutoffs.map((cutoff) => ({
|
||||||
|
value: createCutoffSelectionValue(cutoff.id, "PRE"),
|
||||||
|
cutoffId: cutoff.id,
|
||||||
|
cutoffPhase: "PRE" as const,
|
||||||
|
label: `Pre ${cutoff.name}`
|
||||||
|
}));
|
||||||
|
const lastCutoff = cutoffs[cutoffs.length - 1];
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
value: createCutoffSelectionValue(lastCutoff.id, "POST"),
|
||||||
|
cutoffId: lastCutoff.id,
|
||||||
|
cutoffPhase: "POST",
|
||||||
|
label: `Post ${lastCutoff.name}`
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGeneralDonationCutoffSelectionValue(cutoffs: DashboardPeriodCutoff[], donatedAt: string) {
|
||||||
|
const options = getCutoffSelectionOptions(cutoffs);
|
||||||
|
|
||||||
|
if (options.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const donationDate = new Date(donatedAt);
|
||||||
|
donationDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
for (const cutoff of cutoffs) {
|
||||||
|
if (!cutoff.date) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cutoffDate = new Date(cutoff.date);
|
||||||
|
cutoffDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (donationDate <= cutoffDate) {
|
||||||
|
return createCutoffSelectionValue(cutoff.id, "PRE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createCutoffSelectionValue(cutoffs[cutoffs.length - 1].id, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
function formatPeriodRange(startsAt: string, endsAt: string) {
|
function formatPeriodRange(startsAt: string, endsAt: string) {
|
||||||
const formatter = new Intl.DateTimeFormat("de-DE", { dateStyle: "medium" });
|
const formatter = new Intl.DateTimeFormat("de-DE", { dateStyle: "medium" });
|
||||||
return `${formatter.format(new Date(startsAt))} bis ${formatter.format(new Date(endsAt))}`;
|
return `${formatter.format(new Date(startsAt))} bis ${formatter.format(new Date(endsAt))}`;
|
||||||
@@ -809,8 +878,26 @@ export function DashboardShell({
|
|||||||
const managementCutoffs = selectedPeriodForManagement?.cutoffs ?? [];
|
const managementCutoffs = selectedPeriodForManagement?.cutoffs ?? [];
|
||||||
const currentCutoffs = currentPeriod?.cutoffs ?? [];
|
const currentCutoffs = currentPeriod?.cutoffs ?? [];
|
||||||
const primaryCurrentCutoff = currentCutoffs[0] ?? null;
|
const primaryCurrentCutoff = currentCutoffs[0] ?? null;
|
||||||
const selectedExpenseCutoff =
|
const expenseCutoffOptions = useMemo(() => getCutoffSelectionOptions(currentCutoffs), [currentCutoffs]);
|
||||||
currentCutoffs.find((cutoff) => cutoff.id === expenseForm.cutoffId) ?? primaryCurrentCutoff;
|
const selectedExpenseCutoffValue = createCutoffSelectionValue(expenseForm.cutoffId, expenseForm.cutoffPhase);
|
||||||
|
const selectedExpenseCutoffOption =
|
||||||
|
expenseCutoffOptions.find((option) => option.value === selectedExpenseCutoffValue) ?? expenseCutoffOptions[0] ?? null;
|
||||||
|
useEffect(() => {
|
||||||
|
if (expenseCutoffOptions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseCutoffOptions.some((option) => option.value === selectedExpenseCutoffValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = expenseCutoffOptions[0];
|
||||||
|
setExpenseForm((current) => ({
|
||||||
|
...current,
|
||||||
|
cutoffId: fallback.cutoffId,
|
||||||
|
cutoffPhase: fallback.cutoffPhase
|
||||||
|
}));
|
||||||
|
}, [expenseCutoffOptions, selectedExpenseCutoffValue]);
|
||||||
|
|
||||||
const allExpenses = useMemo(
|
const allExpenses = useMemo(
|
||||||
() => visibleGroups.flatMap((group) => group.budgets.flatMap((budget) => budget.expenses)),
|
() => visibleGroups.flatMap((group) => group.budgets.flatMap((budget) => budget.expenses)),
|
||||||
@@ -938,19 +1025,35 @@ export function DashboardShell({
|
|||||||
() => donations.reduce((sum, donation) => sum + (donation.expenseId ? donation.amount : 0), 0),
|
() => donations.reduce((sum, donation) => sum + (donation.expenseId ? donation.amount : 0), 0),
|
||||||
[donations]
|
[donations]
|
||||||
);
|
);
|
||||||
const preCutoffExpenses = useMemo(
|
const plannedUntilCutoffs = useMemo(
|
||||||
() =>
|
() => {
|
||||||
allExpenses.reduce(
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
return currentCutoffs
|
||||||
|
.filter((cutoff) => {
|
||||||
|
if (!cutoff.date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cutoffDate = new Date(cutoff.date);
|
||||||
|
cutoffDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
return cutoffDate >= today;
|
||||||
|
})
|
||||||
|
.map((cutoff) => ({
|
||||||
|
cutoff,
|
||||||
|
amount: allExpenses.reduce(
|
||||||
(sum, expense) =>
|
(sum, expense) =>
|
||||||
sum +
|
sum +
|
||||||
(expense.cutoffPhase === "PRE" &&
|
(expense.cutoffPhase === "PRE" && !expense.paidAt && expense.cutoffId === cutoff.id
|
||||||
!expense.paidAt &&
|
|
||||||
(!primaryCurrentCutoff?.id || expense.cutoffId === primaryCurrentCutoff.id)
|
|
||||||
? expense.netPeriodAmount
|
? expense.netPeriodAmount
|
||||||
: 0),
|
: 0),
|
||||||
0
|
0
|
||||||
),
|
)
|
||||||
[allExpenses, primaryCurrentCutoff]
|
}));
|
||||||
|
},
|
||||||
|
[allExpenses, currentCutoffs]
|
||||||
);
|
);
|
||||||
const paidTotal = useMemo(
|
const paidTotal = useMemo(
|
||||||
() => allExpenses.reduce((sum, expense) => sum + (expense.paidAt ? expense.netPeriodAmount : 0), 0),
|
() => allExpenses.reduce((sum, expense) => sum + (expense.paidAt ? expense.netPeriodAmount : 0), 0),
|
||||||
@@ -2309,44 +2412,28 @@ export function DashboardShell({
|
|||||||
helperText={"Ab diesem Datum werden Monatsraten innerhalb des aktuellen Zeitraums automatisch berechnet."}
|
helperText={"Ab diesem Datum werden Monatsraten innerhalb des aktuellen Zeitraums automatisch berechnet."}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Stack direction={{ xs: "column", sm: "row" }} gap={1.2}>
|
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
label="Stichtag"
|
label="Stichtag-Zuordnung"
|
||||||
value={expenseForm.cutoffId}
|
value={selectedExpenseCutoffOption?.value ?? ""}
|
||||||
onChange={(event) =>
|
onChange={(event) => {
|
||||||
|
const next = parseCutoffSelectionValue(event.target.value);
|
||||||
setExpenseForm((current) => ({
|
setExpenseForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
cutoffId: event.target.value
|
cutoffId: next.cutoffId,
|
||||||
}))
|
cutoffPhase: next.cutoffPhase
|
||||||
}
|
}));
|
||||||
|
}}
|
||||||
required
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={currentCutoffs.length === 0}
|
disabled={expenseCutoffOptions.length === 0}
|
||||||
>
|
>
|
||||||
{currentCutoffs.map((cutoff) => (
|
{expenseCutoffOptions.map((option) => (
|
||||||
<MenuItem key={cutoff.id} value={cutoff.id}>
|
<MenuItem key={option.value} value={option.value}>
|
||||||
{cutoff.name}
|
{option.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
label="Zuordnung"
|
|
||||||
value={expenseForm.cutoffPhase}
|
|
||||||
onChange={(event) =>
|
|
||||||
setExpenseForm((current) => ({
|
|
||||||
...current,
|
|
||||||
cutoffPhase: event.target.value as ExpenseFormState["cutoffPhase"]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<MenuItem value="PRE">{`Pre ${selectedExpenseCutoff?.name ?? "Open Air"}`}</MenuItem>
|
|
||||||
<MenuItem value="POST">{`Post ${selectedExpenseCutoff?.name ?? "Open Air"}`}</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
</Stack>
|
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
label="Arbeitsgruppe"
|
label="Arbeitsgruppe"
|
||||||
@@ -3604,43 +3691,39 @@ export function DashboardShell({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (financeViewMode === "cutoff") {
|
if (financeViewMode === "cutoff") {
|
||||||
const pre = allExpenses.filter(
|
const expenseById = new Map(allExpenses.map((expense) => [expense.id, expense]));
|
||||||
(expense) =>
|
const rows = expenseCutoffOptions.map((option) => {
|
||||||
expense.cutoffPhase === "PRE" && (!primaryCurrentCutoff?.id || expense.cutoffId === primaryCurrentCutoff.id)
|
const expenses = allExpenses.filter(
|
||||||
|
(expense) => expense.cutoffId === option.cutoffId && expense.cutoffPhase === option.cutoffPhase
|
||||||
);
|
);
|
||||||
const post = allExpenses.filter(
|
const donationsForSection = donations.filter((donation) => {
|
||||||
(expense) =>
|
if (donation.expenseId) {
|
||||||
expense.cutoffPhase === "POST" && (!primaryCurrentCutoff?.id || expense.cutoffId === primaryCurrentCutoff.id)
|
const expense = expenseById.get(donation.expenseId);
|
||||||
|
return (
|
||||||
|
expense?.cutoffId === option.cutoffId &&
|
||||||
|
expense.cutoffPhase === option.cutoffPhase
|
||||||
);
|
);
|
||||||
const cutoffDate = primaryCurrentCutoff?.date ? new Date(primaryCurrentCutoff.date) : null;
|
|
||||||
const preGeneralDonations = donations
|
|
||||||
.filter((donation) => !donation.expenseId && (!cutoffDate || new Date(donation.donatedAt) <= cutoffDate))
|
|
||||||
.reduce((sum, donation) => sum + donation.amount, 0);
|
|
||||||
const postGeneralDonations = donations
|
|
||||||
.filter((donation) => !donation.expenseId && cutoffDate && new Date(donation.donatedAt) > cutoffDate)
|
|
||||||
.reduce((sum, donation) => sum + donation.amount, 0);
|
|
||||||
const preAssignedDonations = donations
|
|
||||||
.filter((donation) => donation.expenseId && pre.some((expense) => expense.id === donation.expenseId))
|
|
||||||
.reduce((sum, donation) => sum + donation.amount, 0);
|
|
||||||
const postAssignedDonations = donations
|
|
||||||
.filter((donation) => donation.expenseId && post.some((expense) => expense.id === donation.expenseId))
|
|
||||||
.reduce((sum, donation) => sum + donation.amount, 0);
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: `Pre ${primaryCurrentCutoff?.name ?? "Open Air"}`,
|
|
||||||
planned: pre.reduce((sum, expense) => sum + (expense.approvalStatus === "PENDING" ? expense.netPeriodAmount : 0), 0),
|
|
||||||
approved: pre.reduce((sum, expense) => sum + (expense.approvalStatus === "APPROVED" ? expense.netPeriodAmount : 0), 0),
|
|
||||||
paid: pre.reduce((sum, expense) => sum + (expense.paidAt ? expense.netPeriodAmount : 0), 0),
|
|
||||||
donations: preAssignedDonations + preGeneralDonations
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `Post ${primaryCurrentCutoff?.name ?? "Open Air"}`,
|
|
||||||
planned: post.reduce((sum, expense) => sum + (expense.approvalStatus === "PENDING" ? expense.netPeriodAmount : 0), 0),
|
|
||||||
approved: post.reduce((sum, expense) => sum + (expense.approvalStatus === "APPROVED" ? expense.netPeriodAmount : 0), 0),
|
|
||||||
paid: post.reduce((sum, expense) => sum + (expense.paidAt ? expense.netPeriodAmount : 0), 0),
|
|
||||||
donations: postAssignedDonations + postGeneralDonations
|
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
return getGeneralDonationCutoffSelectionValue(currentCutoffs, donation.donatedAt) === option.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: option.label,
|
||||||
|
planned: expenses.reduce(
|
||||||
|
(sum, expense) => sum + (expense.approvalStatus === "PENDING" ? expense.netPeriodAmount : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
approved: expenses.reduce(
|
||||||
|
(sum, expense) => sum + (expense.approvalStatus === "APPROVED" ? expense.netPeriodAmount : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
paid: expenses.reduce((sum, expense) => sum + (expense.paidAt ? expense.netPeriodAmount : 0), 0),
|
||||||
|
donations: donationsForSection.reduce((sum, donation) => sum + donation.amount, 0)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows.length > 0 ? rows : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -3901,10 +3984,13 @@ export function DashboardShell({
|
|||||||
label={`Budgets sichtbar: ${currencyFormatter.format(totals.budget)}`}
|
label={`Budgets sichtbar: ${currencyFormatter.format(totals.budget)}`}
|
||||||
sx={{ bgcolor: alpha("#FFFFFF", 0.12), color: "white" }}
|
sx={{ bgcolor: alpha("#FFFFFF", 0.12), color: "white" }}
|
||||||
/>
|
/>
|
||||||
|
{plannedUntilCutoffs.map(({ cutoff, amount }) => (
|
||||||
<Chip
|
<Chip
|
||||||
label={`Geplant bis ${primaryCurrentCutoff?.name ?? "Open Air"}: ${currencyFormatter.format(preCutoffExpenses)}`}
|
key={cutoff.id}
|
||||||
|
label={`Geplant bis ${cutoff.name}: ${currencyFormatter.format(amount)}`}
|
||||||
sx={{ bgcolor: alpha("#FFFFFF", 0.12), color: "white" }}
|
sx={{ bgcolor: alpha("#FFFFFF", 0.12), color: "white" }}
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
<Chip
|
<Chip
|
||||||
label={`Abos monatlich: ${currencyFormatter.format(totals.subscriptions)}`}
|
label={`Abos monatlich: ${currencyFormatter.format(totals.subscriptions)}`}
|
||||||
sx={{ bgcolor: alpha("#FFFFFF", 0.12), color: "white" }}
|
sx={{ bgcolor: alpha("#FFFFFF", 0.12), color: "white" }}
|
||||||
|
|||||||
Reference in New Issue
Block a user