Compare commits

...

3 Commits

Author SHA1 Message Date
jan
0d72cfb144 Mobile Budgetauswahl ergaenzen
All checks were successful
CI / Build and Deploy (push) Successful in 2m22s
2026-05-06 18:16:41 +02:00
jan
0bdb6f553b TypeScript baseUrl Deprecation beheben
All checks were successful
CI / Build and Deploy (push) Successful in 2m20s
2026-05-06 10:54:48 +02:00
jan
f525279f2b Shared Drive Uploads unterstuetzen 2026-05-06 10:43:49 +02:00
3 changed files with 44 additions and 14 deletions

View File

@@ -22,9 +22,11 @@ import {
Divider, Divider,
IconButton, IconButton,
Link, Link,
MenuItem,
Stack, Stack,
TextField, TextField,
Typography Typography,
useMediaQuery
} from "@mui/material"; } from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles"; import { alpha, useTheme } from "@mui/material/styles";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@@ -153,10 +155,12 @@ export function BudgetColumn({
}: BudgetColumnProps) { }: BudgetColumnProps) {
const theme = useTheme(); const theme = useTheme();
const isDark = theme.palette.mode === "dark"; const isDark = theme.palette.mode === "dark";
const isCompactLayout = useMediaQuery(theme.breakpoints.down("lg"));
const [budgetDrafts, setBudgetDrafts] = useState<Record<string, BudgetDraft>>({}); const [budgetDrafts, setBudgetDrafts] = useState<Record<string, BudgetDraft>>({});
const [editingBudgetId, setEditingBudgetId] = useState<string | null>(null); const [editingBudgetId, setEditingBudgetId] = useState<string | null>(null);
const [isEditingGroup, setIsEditingGroup] = useState(false); const [isEditingGroup, setIsEditingGroup] = useState(false);
const [groupDraftName, setGroupDraftName] = useState(group.name); const [groupDraftName, setGroupDraftName] = useState(group.name);
const [selectedBudgetId, setSelectedBudgetId] = useState(group.budgets[0]?.id ?? "");
const [proofFileDrafts, setProofFileDrafts] = useState<Record<string, { file: File; invoiceDate: string }[]>>({}); const [proofFileDrafts, setProofFileDrafts] = useState<Record<string, { file: File; invoiceDate: string }[]>>({});
const [expandedRecurringExpenses, setExpandedRecurringExpenses] = useState<Record<string, boolean>>({}); const [expandedRecurringExpenses, setExpandedRecurringExpenses] = useState<Record<string, boolean>>({});
@@ -183,6 +187,12 @@ export function BudgetColumn({
setGroupDraftName(group.name); setGroupDraftName(group.name);
}, [group.name]); }, [group.name]);
useEffect(() => {
if (!group.budgets.some((budget) => budget.id === selectedBudgetId)) {
setSelectedBudgetId(group.budgets[0]?.id ?? "");
}
}, [group.budgets, selectedBudgetId]);
const approvedSpend = useMemo( const approvedSpend = useMemo(
() => group.budgets.reduce((sum, budget) => sum + getApprovedSpend(budget.expenses), 0), () => group.budgets.reduce((sum, budget) => sum + getApprovedSpend(budget.expenses), 0),
[group.budgets] [group.budgets]
@@ -251,6 +261,9 @@ export function BudgetColumn({
})); }));
} }
const selectedBudget = group.budgets.find((budget) => budget.id === selectedBudgetId) ?? group.budgets[0] ?? null;
const visibleBudgets = isCompactLayout && selectedBudget ? [selectedBudget] : group.budgets;
return ( return (
<Card <Card
sx={{ sx={{
@@ -413,6 +426,22 @@ export function BudgetColumn({
</Box> </Box>
) : null} ) : null}
{isCompactLayout && group.budgets.length > 1 ? (
<TextField
select
label="Budget auswählen"
value={selectedBudget?.id ?? ""}
onChange={(event) => setSelectedBudgetId(event.target.value)}
fullWidth
>
{group.budgets.map((budget) => (
<MenuItem key={budget.id} value={budget.id}>
{budget.name}
</MenuItem>
))}
</TextField>
) : null}
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@@ -420,11 +449,11 @@ export function BudgetColumn({
overflow: "visible", overflow: "visible",
pb: 0, pb: 0,
alignItems: "stretch", alignItems: "stretch",
width: { md: desktopBudgetListWidth }, width: isCompactLayout ? "100%" : desktopBudgetListWidth,
minWidth: { md: desktopBudgetListWidth } minWidth: isCompactLayout ? 0 : desktopBudgetListWidth
}} }}
> >
{group.budgets.map((budget) => { {visibleBudgets.map((budget) => {
const draft = getDraft(budget); const draft = getDraft(budget);
const isEditing = editingBudgetId === budget.id; const isEditing = editingBudgetId === budget.id;
const budgetApproved = getApprovedSpend(budget.expenses); const budgetApproved = getApprovedSpend(budget.expenses);
@@ -444,9 +473,9 @@ export function BudgetColumn({
<Box <Box
key={budget.id} key={budget.id}
sx={{ sx={{
minWidth: { xs: 280, sm: 312, md: budgetCardWidth }, minWidth: isCompactLayout ? 0 : budgetCardWidth,
width: { xs: "84vw", sm: 312, md: budgetCardWidth }, width: isCompactLayout ? "100%" : budgetCardWidth,
maxWidth: { xs: 360, md: budgetCardWidth }, maxWidth: isCompactLayout ? "100%" : budgetCardWidth,
flex: "0 0 auto", flex: "0 0 auto",
scrollSnapAlign: "start" scrollSnapAlign: "start"
}} }}

View File

@@ -224,7 +224,8 @@ export async function uploadExpenseProofToDrive(input: {
mimeType: input.mimeType, mimeType: input.mimeType,
body: Readable.from(input.buffer) body: Readable.from(input.buffer)
}, },
fields: "id, webViewLink" fields: "id, name, webViewLink, webContentLink",
supportsAllDrives: true
}).catch((error: unknown) => { }).catch((error: unknown) => {
throw mapDriveError(error, "DRIVE_UPLOAD_FAILED", "Google Drive konnte den Rechnungsbeleg nicht hochladen.", [ throw mapDriveError(error, "DRIVE_UPLOAD_FAILED", "Google Drive konnte den Rechnungsbeleg nicht hochladen.", [
`Zielordner: ${config.folderId}`, `Zielordner: ${config.folderId}`,
@@ -243,7 +244,8 @@ export async function uploadExpenseProofToDrive(input: {
requestBody: { requestBody: {
type: "anyone", type: "anyone",
role: "reader" role: "reader"
} },
supportsAllDrives: true
}).catch((error: unknown) => { }).catch((error: unknown) => {
throw mapDriveError(error, "DRIVE_PERMISSION_FAILED", "Google Drive konnte den Freigabe-Link nicht erstellen.", [ throw mapDriveError(error, "DRIVE_PERMISSION_FAILED", "Google Drive konnte den Freigabe-Link nicht erstellen.", [
`Die Datei wurde vermutlich bereits erstellt. Drive-Datei-ID: ${response.data.id}` `Die Datei wurde vermutlich bereits erstellt. Drive-Datei-ID: ${response.data.id}`
@@ -273,7 +275,8 @@ export async function runDriveDiagnostics() {
mimeType: "text/plain", mimeType: "text/plain",
body: Readable.from(Buffer.from("RFP Finanzen Drive API Test\n", "utf8")) body: Readable.from(Buffer.from("RFP Finanzen Drive API Test\n", "utf8"))
}, },
fields: "id, webViewLink" fields: "id, name, webViewLink, webContentLink",
supportsAllDrives: true
}); });
createdFileId = response.data.id ?? null; createdFileId = response.data.id ?? null;
@@ -282,7 +285,7 @@ export async function runDriveDiagnostics() {
throw new DriveIntegrationError("Google Drive hat für die Testdatei keine Datei-ID zurückgegeben.", "DRIVE_FILE_ID_MISSING"); throw new DriveIntegrationError("Google Drive hat für die Testdatei keine Datei-ID zurückgegeben.", "DRIVE_FILE_ID_MISSING");
} }
await drive.files.delete({ fileId: createdFileId }); await drive.files.delete({ fileId: createdFileId, supportsAllDrives: true });
return { return {
ok: true, ok: true,
@@ -296,7 +299,7 @@ export async function runDriveDiagnostics() {
}; };
} catch (error) { } catch (error) {
if (createdFileId) { if (createdFileId) {
await drive.files.delete({ fileId: createdFileId }).catch((cleanupError: unknown) => { await drive.files.delete({ fileId: createdFileId, supportsAllDrives: true }).catch((cleanupError: unknown) => {
throw new DriveIntegrationError( throw new DriveIntegrationError(
"Drive-Test ist fehlgeschlagen und die temporäre Testdatei konnte nicht gelöscht werden.", "Drive-Test ist fehlgeschlagen und die temporäre Testdatei konnte nicht gelöscht werden.",
"DRIVE_DIAGNOSTIC_CLEANUP_FAILED", "DRIVE_DIAGNOSTIC_CLEANUP_FAILED",

View File

@@ -13,7 +13,6 @@
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"baseUrl": ".",
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"
@@ -26,4 +25,3 @@
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }