AG Scroll Settings Budget Push und Rechnungsdokumente umsetzen
All checks were successful
CI / Build and Deploy (push) Successful in 2m20s
All checks were successful
CI / Build and Deploy (push) Successful in 2m20s
This commit is contained in:
@@ -32,7 +32,6 @@ 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 {
|
||||
APPROVAL_FLOW,
|
||||
approvalLabel,
|
||||
canDeleteExpense,
|
||||
canDocumentExpense,
|
||||
@@ -48,6 +47,7 @@ type BudgetColumnProps = {
|
||||
viewer: DashboardViewer;
|
||||
busy: boolean;
|
||||
approvalThreshold: number;
|
||||
requiredApprovalTypes: ("CHAIR_A" | "CHAIR_B" | "FINANCE")[];
|
||||
onApprove: (expenseId: string, approvalType: "CHAIR_A" | "CHAIR_B" | "FINANCE") => Promise<void>;
|
||||
onMarkPaid: (expenseId: string) => Promise<void>;
|
||||
onDocument: (expenseId: string, proofUrl?: string) => Promise<void>;
|
||||
@@ -140,6 +140,7 @@ export function BudgetColumn({
|
||||
viewer,
|
||||
busy,
|
||||
approvalThreshold,
|
||||
requiredApprovalTypes,
|
||||
onApprove,
|
||||
onMarkPaid,
|
||||
onDocument,
|
||||
@@ -156,8 +157,7 @@ export function BudgetColumn({
|
||||
const [editingBudgetId, setEditingBudgetId] = useState<string | null>(null);
|
||||
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
||||
const [groupDraftName, setGroupDraftName] = useState(group.name);
|
||||
const [proofFileDrafts, setProofFileDrafts] = useState<Record<string, File | null>>({});
|
||||
const [invoiceDateDrafts, setInvoiceDateDrafts] = useState<Record<string, string>>({});
|
||||
const [proofFileDrafts, setProofFileDrafts] = useState<Record<string, { file: File; invoiceDate: string }[]>>({});
|
||||
const [expandedRecurringExpenses, setExpandedRecurringExpenses] = useState<Record<string, boolean>>({});
|
||||
|
||||
const budgetCardWidth = 352;
|
||||
@@ -223,6 +223,34 @@ export function BudgetColumn({
|
||||
}));
|
||||
}
|
||||
|
||||
function addProofFiles(expenseId: string, files: FileList | null) {
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextFiles = Array.from(files).map((file) => ({ file, invoiceDate: "" }));
|
||||
setProofFileDrafts((current) => ({
|
||||
...current,
|
||||
[expenseId]: [...(current[expenseId] ?? []), ...nextFiles]
|
||||
}));
|
||||
}
|
||||
|
||||
function updateProofInvoiceDate(expenseId: string, index: number, invoiceDate: string) {
|
||||
setProofFileDrafts((current) => ({
|
||||
...current,
|
||||
[expenseId]: (current[expenseId] ?? []).map((entry, entryIndex) =>
|
||||
entryIndex === index ? { ...entry, invoiceDate } : entry
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
function removeProofDraft(expenseId: string, index: number) {
|
||||
setProofFileDrafts((current) => ({
|
||||
...current,
|
||||
[expenseId]: (current[expenseId] ?? []).filter((_, entryIndex) => entryIndex !== index)
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
@@ -231,7 +259,8 @@ export function BudgetColumn({
|
||||
maxWidth: "none",
|
||||
flexShrink: 0,
|
||||
backgroundColor: alpha(theme.palette.background.paper, isDark ? 0.94 : 0.98),
|
||||
backgroundImage: "none"
|
||||
backgroundImage: "none",
|
||||
touchAction: "pan-y"
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
@@ -384,20 +413,16 @@ export function BudgetColumn({
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: 2,
|
||||
overflowX: { xs: "auto", md: "visible" },
|
||||
overflowY: "hidden",
|
||||
pb: { xs: 1.5, md: 0 },
|
||||
alignItems: "stretch",
|
||||
scrollSnapType: { xs: "x proximity", md: "none" },
|
||||
scrollbarGutter: { xs: "stable both-edges", md: "auto" },
|
||||
overscrollBehaviorX: "contain",
|
||||
width: { md: desktopBudgetListWidth },
|
||||
minWidth: { md: desktopBudgetListWidth }
|
||||
}}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: 2,
|
||||
overflow: "visible",
|
||||
pb: 0,
|
||||
alignItems: "stretch",
|
||||
width: { md: desktopBudgetListWidth },
|
||||
minWidth: { md: desktopBudgetListWidth }
|
||||
}}
|
||||
>
|
||||
{group.budgets.map((budget) => {
|
||||
const draft = getDraft(budget);
|
||||
@@ -644,10 +669,10 @@ export function BudgetColumn({
|
||||
) : null}
|
||||
|
||||
{budget.expenses.map((expense) => {
|
||||
const doneApprovalTypes = expense.approvals.map((approval) => approval.approvalType);
|
||||
const availableApprovals = requiresManualApproval(expense.amount, approvalThreshold)
|
||||
? getAvailableApprovalTypes(viewer.approvalPermissions, doneApprovalTypes)
|
||||
: [];
|
||||
const doneApprovalTypes = expense.approvals.map((approval) => approval.approvalType);
|
||||
const availableApprovals = requiresManualApproval(expense.amount, approvalThreshold)
|
||||
? getAvailableApprovalTypes(viewer.approvalPermissions, doneApprovalTypes, requiredApprovalTypes)
|
||||
: [];
|
||||
const isRecurringSeries = expense.recurrence === "MONTHLY";
|
||||
const isRecurringExpanded = expandedRecurringExpenses[expense.id] ?? false;
|
||||
const canUploadProof = expense.creator.id === viewer.id || canDocumentExpense(viewer.role);
|
||||
@@ -655,15 +680,16 @@ export function BudgetColumn({
|
||||
return (
|
||||
<Box
|
||||
key={expense.id}
|
||||
sx={{
|
||||
p: 2.25,
|
||||
borderRadius: "18px",
|
||||
border: `1px solid ${alpha(budget.colorCode, 0.18)}`,
|
||||
backgroundColor:
|
||||
expense.approvalStatus === "APPROVED"
|
||||
? alpha(budget.colorCode, isDark ? 0.16 : 0.08)
|
||||
: alpha(budget.colorCode, isDark ? 0.1 : 0.04)
|
||||
}}
|
||||
sx={{
|
||||
p: 2.25,
|
||||
borderRadius: "18px",
|
||||
border: `1px solid ${alpha(budget.colorCode, 0.18)}`,
|
||||
backgroundColor:
|
||||
expense.approvalStatus === "APPROVED"
|
||||
? alpha(budget.colorCode, isDark ? 0.16 : 0.08)
|
||||
: alpha(budget.colorCode, isDark ? 0.1 : 0.04),
|
||||
touchAction: "pan-y"
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1.4}>
|
||||
<Stack spacing={1}>
|
||||
@@ -753,7 +779,7 @@ export function BudgetColumn({
|
||||
|
||||
{requiresManualApproval(expense.amount, approvalThreshold) ? (
|
||||
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
||||
{APPROVAL_FLOW.map((approvalType) => {
|
||||
{requiredApprovalTypes.map((approvalType) => {
|
||||
const matchingApproval = expense.approvals.find(
|
||||
(approval) => approval.approvalType === approvalType
|
||||
);
|
||||
@@ -776,25 +802,27 @@ export function BudgetColumn({
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
{expense.proofUrl ? (
|
||||
<Stack spacing={0.4}>
|
||||
<Link
|
||||
href={expense.proofUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
underline="hover"
|
||||
variant="body2"
|
||||
sx={{ overflowWrap: "anywhere" }}
|
||||
>
|
||||
{"Rechnungsdokument \u00f6ffnen"}
|
||||
</Link>
|
||||
{expense.invoiceDate ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Rechnung vom {dateFormatter.format(new Date(expense.invoiceDate))}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Stack>
|
||||
) : null}
|
||||
{expense.documents.length > 0 ? (
|
||||
<Stack spacing={0.5}>
|
||||
{expense.documents.map((document, documentIndex) => (
|
||||
<Box key={document.id}>
|
||||
<Link
|
||||
href={document.proofUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
underline="hover"
|
||||
variant="body2"
|
||||
sx={{ overflowWrap: "anywhere" }}
|
||||
>
|
||||
{`Rechnung ${documentIndex + 1}: ${document.storedFileName}`}
|
||||
</Link>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: "block" }}>
|
||||
Rechnung vom {dateFormatter.format(new Date(document.invoiceDate))}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
||||
{availableApprovals.map((approvalType) => (
|
||||
@@ -869,28 +897,10 @@ export function BudgetColumn({
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
{!expense.paidAt &&
|
||||
expense.approvalStatus === "APPROVED" &&
|
||||
!expense.proofUrl &&
|
||||
canUploadProof ? (
|
||||
<Stack spacing={1} sx={{ width: "100%", maxWidth: 420 }}>
|
||||
<TextField
|
||||
label="Rechnungsdatum"
|
||||
type="date"
|
||||
value={invoiceDateDrafts[expense.id] ?? ""}
|
||||
onChange={(event) =>
|
||||
setInvoiceDateDrafts((current) => ({
|
||||
...current,
|
||||
[expense.id]: event.target.value
|
||||
}))
|
||||
}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
size="small"
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Button
|
||||
{expense.approvalStatus === "APPROVED" && canUploadProof ? (
|
||||
<Stack spacing={1} sx={{ width: "100%", maxWidth: 420 }}>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Button
|
||||
component="label"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -900,18 +910,14 @@ export function BudgetColumn({
|
||||
sx={{ minWidth: 0 }}
|
||||
>
|
||||
Datei
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
accept="image/*,application/pdf"
|
||||
onChange={(event) =>
|
||||
setProofFileDrafts((current) => ({
|
||||
...current,
|
||||
[expense.id]: event.target.files?.[0] ?? null
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*,application/pdf"
|
||||
onChange={(event) => addProofFiles(expense.id, event.target.files)}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
component="label"
|
||||
size="small"
|
||||
@@ -924,28 +930,42 @@ export function BudgetColumn({
|
||||
Kamera
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
onChange={(event) =>
|
||||
setProofFileDrafts((current) => ({
|
||||
...current,
|
||||
[expense.id]: event.target.files?.[0] ?? null
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
{proofFileDrafts[expense.id]?.name ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: "block", overflowWrap: "anywhere" }}
|
||||
>
|
||||
{proofFileDrafts[expense.id]?.name}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Button
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
onChange={(event) => addProofFiles(expense.id, event.target.files)}
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
{(proofFileDrafts[expense.id] ?? []).map((entry, entryIndex) => (
|
||||
<Stack key={`${entry.file.name}-${entryIndex}`} spacing={0.7}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ overflowWrap: "anywhere" }}>
|
||||
{entry.file.name}
|
||||
</Typography>
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={1}>
|
||||
<TextField
|
||||
label="Rechnungsdatum"
|
||||
type="date"
|
||||
value={entry.invoiceDate}
|
||||
onChange={(event) => updateProofInvoiceDate(expense.id, entryIndex, event.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
size="small"
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
onClick={() => removeProofDraft(expense.id, entryIndex)}
|
||||
>
|
||||
Entfernen
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
<Button
|
||||
size="medium"
|
||||
variant="contained"
|
||||
color="success"
|
||||
@@ -960,18 +980,24 @@ export function BudgetColumn({
|
||||
textAlign: "center"
|
||||
}}
|
||||
onClick={async () => {
|
||||
const proofFile = proofFileDrafts[expense.id];
|
||||
const invoiceDate = invoiceDateDrafts[expense.id] ?? "";
|
||||
const proofDrafts = proofFileDrafts[expense.id] ?? [];
|
||||
|
||||
if (!proofFile || !invoiceDate) {
|
||||
return;
|
||||
}
|
||||
if (proofDrafts.length === 0 || proofDrafts.some((entry) => !entry.invoiceDate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await onUploadProof(expense.id, proofFile, invoiceDate);
|
||||
}}
|
||||
>
|
||||
Rechnung abgeben und bezahlt setzen
|
||||
</Button>
|
||||
for (const entry of proofDrafts) {
|
||||
await onUploadProof(expense.id, entry.file, entry.invoiceDate);
|
||||
}
|
||||
|
||||
setProofFileDrafts((current) => ({
|
||||
...current,
|
||||
[expense.id]: []
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{expense.paidAt ? "Rechnung nachreichen" : "Rechnung abgeben und bezahlt setzen"}
|
||||
</Button>
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user