UI Push Deep Links und Drive Diagnose verbessern
All checks were successful
CI / Build and Deploy (push) Successful in 2m30s
All checks were successful
CI / Build and Deploy (push) Successful in 2m30s
This commit is contained in:
@@ -37,7 +37,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import type { FormEvent } from "react";
|
||||
import { startTransition, useEffect, useMemo, useState } from "react";
|
||||
|
||||
@@ -134,6 +134,15 @@ type DashboardMessage = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
type DriveDiagnosticResult = {
|
||||
ok?: boolean;
|
||||
error?: string;
|
||||
code?: string;
|
||||
details?: string[];
|
||||
folderId?: string;
|
||||
serviceAccountEmail?: string;
|
||||
};
|
||||
|
||||
function sortApprovalPermissions(value: ApprovalPermissionValue[]) {
|
||||
return APPROVAL_FLOW.filter((approvalType) => value.includes(approvalType));
|
||||
}
|
||||
@@ -243,7 +252,11 @@ async function parseResponse(response: Response) {
|
||||
const payload = await response.json().catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.error ?? "Die Anfrage konnte nicht verarbeitet werden.");
|
||||
const detailText = Array.isArray(payload?.details) && payload.details.length > 0
|
||||
? `\n\n${payload.details.map((detail: string) => `- ${detail}`).join("\n")}`
|
||||
: "";
|
||||
const codeText = typeof payload?.code === "string" ? `\nCode: ${payload.code}` : "";
|
||||
throw new Error(`${payload?.error ?? "Die Anfrage konnte nicht verarbeitet werden."}${codeText}${detailText}`);
|
||||
}
|
||||
|
||||
return payload;
|
||||
@@ -269,6 +282,7 @@ export function DashboardShell({
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
const isCompactLayout = useMediaQuery(theme.breakpoints.down("lg"));
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const visibleGroups = workingGroups;
|
||||
const editableExpenseGroups =
|
||||
@@ -325,10 +339,11 @@ export function DashboardShell({
|
||||
});
|
||||
const [message, setMessage] = useState<DashboardMessage | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [mobileSection, setMobileSection] = useState<MobileSection>("overview");
|
||||
const [desktopSection, setDesktopSection] = useState<DesktopSection>("overview");
|
||||
const [selectedCurrentPeriodId, setSelectedCurrentPeriodId] = useState(currentPeriodId);
|
||||
const [backupFile, setBackupFile] = useState<File | null>(null);
|
||||
const [mobileSection, setMobileSection] = useState<MobileSection>("overview");
|
||||
const [desktopSection, setDesktopSection] = useState<DesktopSection>("overview");
|
||||
const [selectedCurrentPeriodId, setSelectedCurrentPeriodId] = useState(currentPeriodId);
|
||||
const [selectedMobileGroupId, setSelectedMobileGroupId] = useState(visibleGroups[0]?.id ?? "");
|
||||
const [backupFile, setBackupFile] = useState<File | null>(null);
|
||||
const [editingPasswordUserId, setEditingPasswordUserId] = useState<string | null>(null);
|
||||
const [editingUserId, setEditingUserId] = useState<string | null>(null);
|
||||
const [passwordDrafts, setPasswordDrafts] = useState<Record<string, string>>({});
|
||||
@@ -336,6 +351,7 @@ export function DashboardShell({
|
||||
const [userDrafts, setUserDrafts] = useState<Record<string, ManagedUserDraft>>({});
|
||||
const [approvalThresholdDraft, setApprovalThresholdDraft] = useState(approvalThreshold.toFixed(2));
|
||||
const [isOrgaSettingsOpen, setIsOrgaSettingsOpen] = useState(false);
|
||||
const [driveDiagnosticResult, setDriveDiagnosticResult] = useState<DriveDiagnosticResult | null>(null);
|
||||
const [orgaSettingsDraft, setOrgaSettingsDraft] = useState<OrgaSettingsDraft>({
|
||||
requiredApprovalTypes: settings.requiredApprovalTypes,
|
||||
budgetReleaseNotifyTarget: settings.budgetReleaseNotifyTarget
|
||||
@@ -359,6 +375,30 @@ export function DashboardShell({
|
||||
}
|
||||
}, [desktopSection, desktopSections]);
|
||||
|
||||
useEffect(() => {
|
||||
const directGroupId = searchParams.get("group");
|
||||
const budgetId = searchParams.get("budget");
|
||||
const expenseId = searchParams.get("expense");
|
||||
const targetGroupId =
|
||||
(directGroupId && visibleGroups.some((group) => group.id === directGroupId) ? directGroupId : null) ??
|
||||
visibleGroups.find((group) => budgetId && group.budgets.some((budget) => budget.id === budgetId))?.id ??
|
||||
visibleGroups.find((group) =>
|
||||
expenseId && group.budgets.some((budget) => budget.expenses.some((expense) => expense.id === expenseId))
|
||||
)?.id ??
|
||||
null;
|
||||
|
||||
if (targetGroupId) {
|
||||
setSelectedMobileGroupId(targetGroupId);
|
||||
setMobileSection("overview");
|
||||
setDesktopSection("overview");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visibleGroups.some((group) => group.id === selectedMobileGroupId)) {
|
||||
setSelectedMobileGroupId(visibleGroups[0]?.id ?? "");
|
||||
}
|
||||
}, [searchParams, selectedMobileGroupId, visibleGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleGroups.length === 0) {
|
||||
setBudgetForm((current) => ({
|
||||
@@ -1050,7 +1090,7 @@ export function DashboardShell({
|
||||
if (!Number.isFinite(nextThreshold) || nextThreshold < 0) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Bitte eine gueltige Freigabe-Schwelle eingeben."
|
||||
text: "Bitte eine gültige Freigabe-Schwelle eingeben."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1091,10 +1131,33 @@ export function DashboardShell({
|
||||
}, "Zust\u00e4ndigkeiten und Benachrichtigungen wurden gespeichert.");
|
||||
}
|
||||
|
||||
async function handleRunDriveDiagnostics() {
|
||||
setBusy(true);
|
||||
setDriveDiagnosticResult(null);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const result = (await parseResponse(
|
||||
await fetch("/api/settings/drive-diagnostics", {
|
||||
method: "POST"
|
||||
})
|
||||
)) as DriveDiagnosticResult;
|
||||
|
||||
setDriveDiagnosticResult(result);
|
||||
setMessage({ type: "success", text: "Drive-Verbindung erfolgreich getestet." });
|
||||
} catch (error) {
|
||||
const text = error instanceof Error ? error.message : "Drive-Verbindungstest fehlgeschlagen.";
|
||||
setDriveDiagnosticResult({ ok: false, error: text });
|
||||
setMessage({ type: "error", text });
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEnablePushNotifications() {
|
||||
if (!("serviceWorker" in navigator) || !("PushManager" in window) || !("Notification" in window)) {
|
||||
setPushStatus("unsupported");
|
||||
setMessage({ type: "error", text: "Dieser Browser unterstuetzt Web Push nicht." });
|
||||
setMessage({ type: "error", text: "Dieser Browser unterstützt Web Push nicht." });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1280,7 +1343,7 @@ export function DashboardShell({
|
||||
})
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Keine Freigaberollen verfuegbar
|
||||
Keine Freigaberollen verfügbar
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -2375,8 +2438,26 @@ export function DashboardShell({
|
||||
</Card>
|
||||
) : null;
|
||||
|
||||
const selectedMobileGroup = visibleGroups.find((group) => group.id === selectedMobileGroupId) ?? visibleGroups[0] ?? null;
|
||||
const overviewGroups = isCompactLayout && selectedMobileGroup ? [selectedMobileGroup] : visibleGroups;
|
||||
|
||||
const overviewContent = (
|
||||
<Stack spacing={2.5}>
|
||||
{isCompactLayout && visibleGroups.length > 1 ? (
|
||||
<TextField
|
||||
select
|
||||
label="AG auswählen"
|
||||
value={selectedMobileGroup?.id ?? ""}
|
||||
onChange={(event) => setSelectedMobileGroupId(event.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
{visibleGroups.map((group) => (
|
||||
<MenuItem key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
) : null}
|
||||
{isCompactLayout ? (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -2391,9 +2472,9 @@ export function DashboardShell({
|
||||
touchAction: "pan-x pan-y"
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" gap={2} sx={{ width: "max-content", minWidth: "100%", alignItems: "stretch" }}>
|
||||
{visibleGroups.map((group) => (
|
||||
<Box key={group.id} sx={{ width: "min(88vw, 456px)", flex: "0 0 auto", scrollSnapAlign: "start" }}>
|
||||
<Stack direction="row" gap={2} sx={{ width: "100%", minWidth: "100%", alignItems: "stretch" }}>
|
||||
{overviewGroups.map((group) => (
|
||||
<Box key={group.id} sx={{ width: "100%", flex: "0 0 auto", scrollSnapAlign: "start" }}>
|
||||
<BudgetColumn
|
||||
group={group}
|
||||
viewer={viewer}
|
||||
@@ -2438,7 +2519,7 @@ export function DashboardShell({
|
||||
pr: 0.5
|
||||
}}
|
||||
>
|
||||
{visibleGroups.map((group) => (
|
||||
{overviewGroups.map((group) => (
|
||||
<Box key={group.id} sx={{ flex: "0 0 auto", scrollSnapAlign: "start" }}>
|
||||
<BudgetColumn
|
||||
group={group}
|
||||
@@ -2498,7 +2579,28 @@ export function DashboardShell({
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: { xs: 3, md: 4 } }}>
|
||||
<CardContent sx={{ p: { xs: 3, md: 4 }, position: "relative" }}>
|
||||
{viewer.role === "ORGA" ? (
|
||||
<IconButton
|
||||
aria-label="Zuständigkeiten und Benachrichtigungen"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: { xs: 18, md: 24 },
|
||||
right: { xs: 18, md: 24 },
|
||||
width: 44,
|
||||
height: 44,
|
||||
border: `1px solid ${alpha("#FFFFFF", 0.28)}`,
|
||||
color: "white",
|
||||
bgcolor: alpha("#FFFFFF", 0.08),
|
||||
"&:hover": {
|
||||
bgcolor: alpha("#FFFFFF", 0.16)
|
||||
}
|
||||
}}
|
||||
onClick={() => setIsOrgaSettingsOpen(true)}
|
||||
>
|
||||
<SettingsRoundedIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<Stack spacing={3}>
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
@@ -2506,7 +2608,7 @@ export function DashboardShell({
|
||||
alignItems={{ xs: "flex-start", md: "center" }}
|
||||
gap={2}
|
||||
>
|
||||
<Box sx={{ maxWidth: 760 }}>
|
||||
<Box sx={{ maxWidth: 760, pr: viewer.role === "ORGA" ? { xs: 6, md: 0 } : 0 }}>
|
||||
<Typography variant="overline" sx={{ color: alpha("#FFFFFF", 0.72), letterSpacing: "0.18em" }}>
|
||||
Rave for Peace
|
||||
</Typography>
|
||||
@@ -2534,15 +2636,6 @@ export function DashboardShell({
|
||||
{pushStatus === "enabled" ? "Web Push aktiv" : "Freigabe-Push"}
|
||||
</Button>
|
||||
) : null}
|
||||
{viewer.role === "ORGA" ? (
|
||||
<IconButton
|
||||
aria-label="Zuständigkeiten und Benachrichtigungen"
|
||||
sx={{ border: `1px solid ${alpha("#FFFFFF", 0.28)}`, color: "white" }}
|
||||
onClick={() => setIsOrgaSettingsOpen(true)}
|
||||
>
|
||||
<SettingsRoundedIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<Chip
|
||||
label={`${viewer.username} - ${roleLabel(viewer.role)}`}
|
||||
sx={{ bgcolor: alpha("#FFFFFF", 0.14), color: "white", fontWeight: 700, maxWidth: "100%" }}
|
||||
@@ -2644,6 +2737,40 @@ export function DashboardShell({
|
||||
<MenuItem value="ALL_GROUP_USERS">Alle Nutzer der AG</MenuItem>
|
||||
<MenuItem value="GROUP_MEMBERS_ONLY">Nur AG-Mitglieder</MenuItem>
|
||||
</TextField>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
Google Drive
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Prüft Service Account, Zielordner und Upload-Rechte mit einer temporären Testdatei.
|
||||
</Typography>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
disabled={busy}
|
||||
onClick={handleRunDriveDiagnostics}
|
||||
sx={{ mt: 1.2 }}
|
||||
>
|
||||
Drive-Verbindung testen
|
||||
</Button>
|
||||
{driveDiagnosticResult ? (
|
||||
<Alert
|
||||
severity={driveDiagnosticResult.ok ? "success" : "error"}
|
||||
sx={{ mt: 1.2, whiteSpace: "pre-line" }}
|
||||
>
|
||||
{driveDiagnosticResult.ok
|
||||
? [
|
||||
"Drive-Test erfolgreich.",
|
||||
driveDiagnosticResult.serviceAccountEmail
|
||||
? `Service Account: ${driveDiagnosticResult.serviceAccountEmail}`
|
||||
: null,
|
||||
driveDiagnosticResult.folderId ? `Zielordner: ${driveDiagnosticResult.folderId}` : null,
|
||||
...(driveDiagnosticResult.details ?? [])
|
||||
].filter(Boolean).join("\n")
|
||||
: driveDiagnosticResult.error ?? "Drive-Verbindungstest fehlgeschlagen."}
|
||||
</Alert>
|
||||
) : null}
|
||||
</Box>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -2656,7 +2783,7 @@ export function DashboardShell({
|
||||
|
||||
<Container maxWidth={false} sx={{ maxWidth: 1640 }}>
|
||||
<Stack spacing={3} px={2}>
|
||||
{message ? <Alert severity={message.type}>{message.text}</Alert> : null}
|
||||
{message ? <Alert severity={message.type} sx={{ whiteSpace: "pre-line" }}>{message.text}</Alert> : null}
|
||||
{periodOverviewCard}
|
||||
|
||||
{isCompactLayout ? (
|
||||
|
||||
Reference in New Issue
Block a user