Desktop ist wieder auf Horizontal-Scroll zurückgebaut, mobil bleibt die Dropdown-Auswahl. Dabei habe ich die Scroll-Container stabilisiert, damit die AG- und Budgetkarten sauber scrollen statt seitlich zu „wackeln“, in dashboard-shell.tsx und budget-column.tsx.
Die Abo-Logik ist jetzt deutlich sauberer: beim Anlegen gibt es ein Startdatum, der Server leitet daraus Monatsraten für den gewählten Zeitraum ab, Budgets rechnen mit dem periodischen Gesamtbetrag, und Abo-Ausgaben erscheinen als aufklappbare Gruppe statt als aufgeblähte Liste. Das steckt vor allem in page.tsx, recurring-expenses.ts, route.ts, dashboard-types.ts und der Migration migration.sql. Backup/Import und Audit-Restore kennen das neue Feld ebenfalls.
This commit is contained in:
@@ -66,6 +66,7 @@ type ExpenseFormState = {
|
||||
agId: string;
|
||||
budgetId: string;
|
||||
recurrence: "NONE" | "MONTHLY";
|
||||
recurrenceStartAt: string;
|
||||
proofUrl: string;
|
||||
};
|
||||
|
||||
@@ -233,6 +234,7 @@ export function DashboardShell({
|
||||
agId: defaultEditableGroup?.id ?? "",
|
||||
budgetId: defaultBudget?.id ?? "",
|
||||
recurrence: "NONE",
|
||||
recurrenceStartAt: toDateInputValue(currentPeriod?.startsAt ?? new Date().toISOString()),
|
||||
proofUrl: ""
|
||||
});
|
||||
const [budgetForm, setBudgetForm] = useState<BudgetFormState>({
|
||||
@@ -411,7 +413,7 @@ export function DashboardShell({
|
||||
(groupSum, budget) =>
|
||||
groupSum +
|
||||
budget.expenses.reduce(
|
||||
(sum, expense) => sum + (expense.approvalStatus === "APPROVED" ? expense.amount : 0),
|
||||
(sum, expense) => sum + (expense.approvalStatus === "APPROVED" ? expense.periodAmount : 0),
|
||||
0
|
||||
),
|
||||
0
|
||||
@@ -420,7 +422,7 @@ export function DashboardShell({
|
||||
(groupSum, budget) =>
|
||||
groupSum +
|
||||
budget.expenses.reduce(
|
||||
(sum, expense) => sum + (expense.approvalStatus === "PENDING" ? expense.amount : 0),
|
||||
(sum, expense) => sum + (expense.approvalStatus === "PENDING" ? expense.periodAmount : 0),
|
||||
0
|
||||
),
|
||||
0
|
||||
@@ -485,7 +487,12 @@ export function DashboardShell({
|
||||
}
|
||||
|
||||
if (!expenseForm.budgetId) {
|
||||
setMessage({ type: "error", text: "Bitte zuerst ein Budget f\u00fcr diese AG anlegen oder ausw\u00e4hlen." });
|
||||
setMessage({ type: "error", text: "Bitte zuerst ein Budget für diese AG anlegen oder auswählen." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (expenseForm.recurrence === "MONTHLY" && !expenseForm.recurrenceStartAt) {
|
||||
setMessage({ type: "error", text: "Bitte ein Startdatum für das monatliche Abo angeben." });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -503,6 +510,7 @@ export function DashboardShell({
|
||||
agId: expenseForm.agId,
|
||||
budgetId: expenseForm.budgetId,
|
||||
recurrence: expenseForm.recurrence,
|
||||
recurrenceStartAt: expenseForm.recurrence === "MONTHLY" ? expenseForm.recurrenceStartAt : "",
|
||||
proofUrl: expenseForm.proofUrl
|
||||
})
|
||||
})
|
||||
@@ -518,6 +526,7 @@ export function DashboardShell({
|
||||
agId: resetGroup,
|
||||
budgetId: resetBudget,
|
||||
recurrence: "NONE",
|
||||
recurrenceStartAt: toDateInputValue(currentPeriod?.startsAt ?? new Date().toISOString()),
|
||||
proofUrl: ""
|
||||
});
|
||||
}, "Ausgabe wurde gespeichert.");
|
||||
@@ -1186,11 +1195,24 @@ export function DashboardShell({
|
||||
}))
|
||||
}
|
||||
fullWidth
|
||||
helperText={"Monatliche Abos erscheinen oben gesammelt im \u00dcberblick."}
|
||||
helperText={"Monatliche Abos werden im Zeitraum automatisch Monat für Monat fortgeschrieben."}
|
||||
>
|
||||
<MenuItem value="NONE">Einmalig</MenuItem>
|
||||
<MenuItem value="MONTHLY">Monatliches Abo</MenuItem>
|
||||
</TextField>
|
||||
{expenseForm.recurrence === "MONTHLY" ? (
|
||||
<TextField
|
||||
label="Abo-Startdatum"
|
||||
type="date"
|
||||
value={expenseForm.recurrenceStartAt}
|
||||
onChange={(event) =>
|
||||
setExpenseForm((current) => ({ ...current, recurrenceStartAt: event.target.value }))
|
||||
}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
fullWidth
|
||||
helperText={"Ab diesem Datum werden Monatsraten innerhalb des aktuellen Zeitraums automatisch berechnet."}
|
||||
/>
|
||||
) : null}
|
||||
<TextField
|
||||
select
|
||||
label="Arbeitsgruppe"
|
||||
@@ -1907,7 +1929,7 @@ export function DashboardShell({
|
||||
|
||||
const overviewContent = (
|
||||
<Stack spacing={2.5}>
|
||||
{visibleGroups.length > 1 ? (
|
||||
{isCompactLayout && visibleGroups.length > 1 ? (
|
||||
<Card>
|
||||
<CardContent sx={{ p: 2.5 }}>
|
||||
<Stack spacing={1.5}>
|
||||
@@ -1916,7 +1938,7 @@ export function DashboardShell({
|
||||
AG auswählen
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Wähle die AG, die gerade in der Übersicht angezeigt werden soll.
|
||||
Mobil zeigen wir jeweils eine AG auf einmal, damit die Budgetkarten sauber lesbar bleiben.
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
@@ -1937,23 +1959,36 @@ export function DashboardShell({
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Stack direction="column" gap={2}>
|
||||
{(mobileSelectedGroup ? [mobileSelectedGroup] : []).map((group) => (
|
||||
<BudgetColumn
|
||||
key={group.id}
|
||||
group={group}
|
||||
viewer={viewer}
|
||||
busy={busy}
|
||||
approvalThreshold={approvalThreshold}
|
||||
onApprove={handleApprove}
|
||||
onMarkPaid={handleMarkPaid}
|
||||
onDocument={handleDocument}
|
||||
onSaveWorkingGroup={handleSaveWorkingGroup}
|
||||
onDeleteWorkingGroup={handleDeleteWorkingGroup}
|
||||
onSaveBudget={handleSaveBudget}
|
||||
onDeleteBudget={handleDeleteBudget}
|
||||
onDeleteExpense={handleDeleteExpense}
|
||||
/>
|
||||
<Stack
|
||||
direction={isCompactLayout ? "column" : "row"}
|
||||
gap={2}
|
||||
sx={{
|
||||
overflowX: isCompactLayout ? "visible" : "auto",
|
||||
overflowY: "hidden",
|
||||
pb: isCompactLayout ? 0 : 2,
|
||||
alignItems: "stretch",
|
||||
scrollSnapType: isCompactLayout ? "none" : "x proximity",
|
||||
scrollbarGutter: isCompactLayout ? "auto" : "stable both-edges",
|
||||
overscrollBehaviorX: "contain"
|
||||
}}
|
||||
>
|
||||
{(isCompactLayout ? (mobileSelectedGroup ? [mobileSelectedGroup] : []) : visibleGroups).map((group) => (
|
||||
<Box key={group.id} sx={{ flex: "0 0 auto", scrollSnapAlign: "start" }}>
|
||||
<BudgetColumn
|
||||
group={group}
|
||||
viewer={viewer}
|
||||
busy={busy}
|
||||
approvalThreshold={approvalThreshold}
|
||||
onApprove={handleApprove}
|
||||
onMarkPaid={handleMarkPaid}
|
||||
onDocument={handleDocument}
|
||||
onSaveWorkingGroup={handleSaveWorkingGroup}
|
||||
onDeleteWorkingGroup={handleDeleteWorkingGroup}
|
||||
onSaveBudget={handleSaveBudget}
|
||||
onDeleteBudget={handleDeleteBudget}
|
||||
onDeleteExpense={handleDeleteExpense}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user