Rechnungsdatum und Belegupload ueberarbeiten
This commit is contained in:
@@ -50,7 +50,7 @@ type BudgetColumnProps = {
|
||||
onApprove: (expenseId: string, approvalType: "CHAIR_A" | "CHAIR_B" | "FINANCE") => Promise<void>;
|
||||
onMarkPaid: (expenseId: string) => Promise<void>;
|
||||
onDocument: (expenseId: string, proofUrl?: string) => Promise<void>;
|
||||
onUploadProof: (expenseId: string, file: File) => Promise<string>;
|
||||
onUploadProof: (expenseId: string, file: File, invoiceDate: string) => Promise<string>;
|
||||
onSaveWorkingGroup: (groupId: string, name: string) => Promise<void>;
|
||||
onDeleteWorkingGroup: (groupId: string, groupName: string) => Promise<void>;
|
||||
onSaveBudget: (budgetId: string, name: string, totalBudget: string, colorCode: string) => Promise<void>;
|
||||
@@ -155,8 +155,8 @@ export function BudgetColumn({
|
||||
const [editingBudgetId, setEditingBudgetId] = useState<string | null>(null);
|
||||
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
||||
const [groupDraftName, setGroupDraftName] = useState(group.name);
|
||||
const [proofUrlDrafts, setProofUrlDrafts] = useState<Record<string, string>>({});
|
||||
const [proofFileDrafts, setProofFileDrafts] = useState<Record<string, File | null>>({});
|
||||
const [invoiceDateDrafts, setInvoiceDateDrafts] = useState<Record<string, string>>({});
|
||||
const [expandedRecurringExpenses, setExpandedRecurringExpenses] = useState<Record<string, boolean>>({});
|
||||
|
||||
const budgetCardWidth = 352;
|
||||
@@ -756,16 +756,23 @@ export function BudgetColumn({
|
||||
) : null}
|
||||
|
||||
{expense.proofUrl ? (
|
||||
<Link
|
||||
href={expense.proofUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
underline="hover"
|
||||
variant="body2"
|
||||
sx={{ overflowWrap: "anywhere" }}
|
||||
>
|
||||
{"Beleg \u00f6ffnen"}
|
||||
</Link>
|
||||
<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}
|
||||
|
||||
<Stack direction="row" gap={1} useFlexGap flexWrap="wrap">
|
||||
@@ -791,7 +798,12 @@ export function BudgetColumn({
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{!expense.paidAt && expense.approvalStatus === "APPROVED" && canMarkPaid(viewer.role) ? (
|
||||
{!expense.paidAt &&
|
||||
expense.approvalStatus === "APPROVED" &&
|
||||
expense.proofUrl &&
|
||||
expense.invoiceDate &&
|
||||
expense.documentedAt &&
|
||||
canMarkPaid(viewer.role) ? (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -829,8 +841,25 @@ export function BudgetColumn({
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
{expense.paidAt && !expense.documentedAt && canDocumentExpense(viewer.role) ? (
|
||||
<Stack direction={{ xs: "column", sm: "row" }} gap={1}>
|
||||
{!expense.paidAt &&
|
||||
expense.approvalStatus === "APPROVED" &&
|
||||
!expense.proofUrl &&
|
||||
canDocumentExpense(viewer.role) ? (
|
||||
<Stack direction={{ xs: "column", sm: "row" }} gap={1} alignItems={{ sm: "center" }}>
|
||||
<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
|
||||
/>
|
||||
<TextField
|
||||
label="Beleg"
|
||||
value={proofFileDrafts[expense.id]?.name ?? expense.proofUrl ?? ""}
|
||||
@@ -874,14 +903,17 @@ export function BudgetColumn({
|
||||
disabled={busy}
|
||||
onClick={async () => {
|
||||
const proofFile = proofFileDrafts[expense.id];
|
||||
const proofUrl = proofFile
|
||||
? await onUploadProof(expense.id, proofFile)
|
||||
: proofUrlDrafts[expense.id] ?? expense.proofUrl ?? undefined;
|
||||
const invoiceDate = invoiceDateDrafts[expense.id] ?? "";
|
||||
|
||||
await onDocument(expense.id, proofUrl);
|
||||
if (!proofFile || !invoiceDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
await onUploadProof(expense.id, proofFile, invoiceDate);
|
||||
await onMarkPaid(expense.id);
|
||||
}}
|
||||
>
|
||||
Dokumentieren
|
||||
Rechnung abgeben und bezahlt setzen
|
||||
</Button>
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
@@ -325,7 +325,6 @@ export function DashboardShell({
|
||||
const [approvalThresholdDraft, setApprovalThresholdDraft] = useState(approvalThreshold.toFixed(2));
|
||||
const [periodForm, setPeriodForm] = useState<PeriodFormState>(getSuggestedPeriodDraft(currentPeriod));
|
||||
const [periodEditForm, setPeriodEditForm] = useState<PeriodEditFormState>(getPeriodEditDraft(currentPeriod));
|
||||
const [expenseProofFile, setExpenseProofFile] = useState<File | null>(null);
|
||||
const [pushStatus, setPushStatus] = useState<"idle" | "enabled" | "blocked" | "unsupported">("idle");
|
||||
useEffect(() => {
|
||||
if (visibleGroups.length === 0) {
|
||||
@@ -626,7 +625,7 @@ export function DashboardShell({
|
||||
}
|
||||
|
||||
await runAction(async () => {
|
||||
const result = (await parseResponse(
|
||||
await parseResponse(
|
||||
await fetch("/api/expenses", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -642,19 +641,7 @@ export function DashboardShell({
|
||||
recurrenceStartAt: expenseForm.recurrence === "MONTHLY" ? expenseForm.recurrenceStartAt : ""
|
||||
})
|
||||
})
|
||||
)) as { expense?: { id: string } };
|
||||
|
||||
if (expenseProofFile && result.expense?.id) {
|
||||
const formData = new FormData();
|
||||
formData.set("file", expenseProofFile);
|
||||
|
||||
await parseResponse(
|
||||
await fetch(`/api/expenses/${result.expense.id}/proof`, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const resetGroup = defaultEditableGroup?.id ?? "";
|
||||
const resetBudget = defaultEditableGroup?.budgets[0]?.id ?? "";
|
||||
@@ -668,7 +655,6 @@ export function DashboardShell({
|
||||
recurrence: "NONE",
|
||||
recurrenceStartAt: toDateInputValue(currentPeriod?.startsAt ?? new Date().toISOString())
|
||||
});
|
||||
setExpenseProofFile(null);
|
||||
}, "Ausgabe wurde gespeichert.");
|
||||
}
|
||||
|
||||
@@ -767,18 +753,30 @@ export function DashboardShell({
|
||||
}, "Ausgabe wurde dokumentiert.");
|
||||
}
|
||||
|
||||
async function handleUploadProof(expenseId: string, file: File) {
|
||||
const formData = new FormData();
|
||||
formData.set("file", file);
|
||||
async function handleUploadProof(expenseId: string, file: File, invoiceDate: string) {
|
||||
setBusy(true);
|
||||
setMessage(null);
|
||||
|
||||
const result = (await parseResponse(
|
||||
await fetch(`/api/expenses/${expenseId}/proof`, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
})
|
||||
)) as { proofUrl: string };
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.set("file", file);
|
||||
formData.set("invoiceDate", invoiceDate);
|
||||
|
||||
return result.proofUrl;
|
||||
const result = (await parseResponse(
|
||||
await fetch(`/api/expenses/${expenseId}/proof`, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
})
|
||||
)) as { proofUrl: string };
|
||||
|
||||
return result.proofUrl;
|
||||
} catch (error) {
|
||||
const text = error instanceof Error ? error.message : "Beleg konnte nicht hochgeladen werden.";
|
||||
setMessage({ type: "error", text });
|
||||
throw error;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveBudget(budgetId: string, name: string, totalBudget: string, colorCode: string) {
|
||||
@@ -1485,17 +1483,6 @@ export function DashboardShell({
|
||||
Neue Ausgabe
|
||||
</Typography>
|
||||
</Box>
|
||||
{viewer.approvalPermissions.length > 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant={pushStatus === "enabled" ? "contained" : "outlined"}
|
||||
startIcon={<NotificationsActiveRoundedIcon />}
|
||||
disabled={busy || pushStatus === "unsupported"}
|
||||
onClick={handleEnablePushNotifications}
|
||||
>
|
||||
{pushStatus === "enabled" ? "Web Push aktiv" : "Freigabe-Push aktivieren"}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Box component="form" onSubmit={handleCreateExpense}>
|
||||
<Stack spacing={2}>
|
||||
@@ -1594,34 +1581,6 @@ export function DashboardShell({
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
label="Beleg"
|
||||
value={expenseProofFile?.name ?? ""}
|
||||
fullWidth
|
||||
InputProps={{ readOnly: true }}
|
||||
helperText="Optional: Bild oder PDF auswählen. Auf Mobilgeräten kann die Kamera angeboten werden."
|
||||
/>
|
||||
<Stack direction={{ xs: "column", sm: "row" }} gap={1} useFlexGap flexWrap="wrap">
|
||||
<Button component="label" variant="outlined" startIcon={<UploadFileRoundedIcon />} disabled={busy}>
|
||||
Beleg auswählen
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
accept="image/*,application/pdf"
|
||||
onChange={(event) => setExpenseProofFile(event.target.files?.[0] ?? null)}
|
||||
/>
|
||||
</Button>
|
||||
<Button component="label" variant="outlined" disabled={busy}>
|
||||
Kamera öffnen
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
onChange={(event) => setExpenseProofFile(event.target.files?.[0] ?? null)}
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
@@ -2534,6 +2493,19 @@ export function DashboardShell({
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction={{ xs: "column", sm: "row" }} gap={1.2} alignItems={{ sm: "center" }}>
|
||||
{viewer.approvalPermissions.length > 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
variant={pushStatus === "enabled" ? "contained" : "outlined"}
|
||||
startIcon={<NotificationsActiveRoundedIcon />}
|
||||
disabled={busy || pushStatus === "unsupported"}
|
||||
sx={{ borderColor: alpha("#FFFFFF", 0.28), color: "white" }}
|
||||
onClick={handleEnablePushNotifications}
|
||||
>
|
||||
{pushStatus === "enabled" ? "Web Push aktiv" : "Freigabe-Push"}
|
||||
</Button>
|
||||
) : null}
|
||||
<Chip
|
||||
label={`${viewer.username} - ${roleLabel(viewer.role)}`}
|
||||
sx={{ bgcolor: alpha("#FFFFFF", 0.14), color: "white", fontWeight: 700, maxWidth: "100%" }}
|
||||
|
||||
Reference in New Issue
Block a user