From 96eabafea84964d228a791691b326aebf000e946 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 9 Nov 2025 18:00:35 +0100 Subject: [PATCH] feat: streamline dashboard actions by removing inline add-entry form and manual refresh --- src/App.js | 209 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 179 insertions(+), 30 deletions(-) diff --git a/src/App.js b/src/App.js index efc5a5d..0ab24fd 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { BrowserRouter as Router, Routes, Route, Navigate, Link, useLocation } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Navigate, Link, useLocation, useNavigate } from 'react-router-dom'; import './App.css'; const TOKEN_STORAGE_KEY = 'pickupConfigToken'; @@ -23,6 +23,11 @@ function App() { etaSeconds: null }); const [initializing, setInitializing] = useState(false); + const [isDirty, setIsDirty] = useState(false); + const [dirtyDialogOpen, setDirtyDialogOpen] = useState(false); + const [dirtyDialogMessage, setDirtyDialogMessage] = useState(''); + const [pendingNavigation, setPendingNavigation] = useState(null); + const [dirtyDialogSaving, setDirtyDialogSaving] = useState(false); const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']; const delay = useCallback((ms) => new Promise((resolve) => setTimeout(resolve, ms)), []); @@ -281,7 +286,7 @@ function App() { } }; - const handleLogout = async () => { + const performLogout = useCallback(async () => { if (!session?.token) { handleUnauthorized(); return; @@ -293,6 +298,10 @@ function App() { } finally { handleUnauthorized(); } + }, [session?.token, authorizedFetch, handleUnauthorized]); + + const handleLogout = () => { + requestNavigation('dich abzumelden', performLogout); }; const fetchConfig = useCallback(async (tokenOverride, { silent = false } = {}) => { @@ -427,6 +436,7 @@ function App() { await fetchStoresList(effectiveToken); await fetchConfig(effectiveToken, { silent: true }); + setIsDirty(false); setStatus('Betriebe aktualisiert.'); setTimeout(() => setStatus(''), 3000); } catch (err) { @@ -451,11 +461,88 @@ function App() { ] ); + const saveConfig = useCallback(async () => { + if (!session?.token) { + return false; + } + setStatus('Speichere...'); + setError(''); + try { + const response = await authorizedFetch('/api/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const result = await response.json(); + if (result.success) { + setStatus('Konfiguration erfolgreich gespeichert!'); + setTimeout(() => setStatus(''), 3000); + setIsDirty(false); + return true; + } + throw new Error(result.error || 'Unbekannter Fehler beim Speichern'); + } catch (err) { + setError(`Fehler beim Speichern: ${err.message}`); + setStatus(''); + return false; + } finally { + setLoading(false); + } + }, [session?.token, authorizedFetch, config]); + const refreshStoresAndConfig = useCallback( ({ block = false } = {}) => syncStoresWithProgress({ block, reason: 'manual', startJob: true }), [syncStoresWithProgress] ); + const requestNavigation = useCallback( + (message, action) => { + if (!isDirty) { + action(); + return; + } + setDirtyDialogMessage(message || 'Änderungen wurden noch nicht gespeichert.'); + setPendingNavigation(() => action); + setDirtyDialogOpen(true); + }, + [isDirty] + ); + + const handleDirtySave = useCallback(async () => { + if (!pendingNavigation) { + setDirtyDialogOpen(false); + return; + } + setDirtyDialogSaving(true); + const success = await saveConfig(); + setDirtyDialogSaving(false); + if (!success) { + return; + } + const action = pendingNavigation; + setPendingNavigation(null); + setDirtyDialogOpen(false); + action(); + }, [pendingNavigation, saveConfig]); + + const handleDirtyDiscard = useCallback(() => { + const action = pendingNavigation; + setPendingNavigation(null); + setDirtyDialogOpen(false); + setIsDirty(false); + if (action) { + action(); + } + }, [pendingNavigation]); + + const handleDirtyCancel = useCallback(() => { + setDirtyDialogOpen(false); + setPendingNavigation(null); + }, []); + useEffect(() => { let ticker; let cancelled = false; @@ -513,33 +600,20 @@ function App() { syncStoresWithProgress ]); - const saveConfig = async () => { - if (!session?.token) { - return; - } - setStatus('Speichere...'); - setError(''); - try { - const response = await authorizedFetch('/api/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(config) - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + useEffect(() => { + const handleBeforeUnload = (event) => { + if (!isDirty) { + return; } - const result = await response.json(); - if (result.success) { - setStatus('Konfiguration erfolgreich gespeichert!'); - setTimeout(() => setStatus(''), 3000); - } else { - throw new Error(result.error || 'Unbekannter Fehler beim Speichern'); - } - } catch (err) { - setError(`Fehler beim Speichern: ${err.message}`); - setStatus(''); - } - }; + event.preventDefault(); + event.returnValue = ''; + return ''; + }; + window.addEventListener('beforeunload', handleBeforeUnload); + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [isDirty]); const persistConfigUpdate = async (updater, successMessage) => { if (!session?.token) { @@ -571,12 +645,14 @@ function App() { const message = successMessage || 'Konfiguration gespeichert.'; setStatus(message); setTimeout(() => setStatus(''), 3000); + setIsDirty(false); } catch (err) { setError(`Fehler beim Speichern: ${err.message}`); } }; const deleteEntry = (entryId) => { + setIsDirty(true); if (window.confirm('Soll dieser Eintrag dauerhaft gelöscht werden?')) { const updatedConfig = config.filter((item) => item.id !== entryId); setConfig(updatedConfig); @@ -589,6 +665,7 @@ function App() { if (!window.confirm('Soll dieser Betrieb ausgeblendet werden?')) { return; } + setIsDirty(true); await persistConfigUpdate( (prev) => prev.map((item) => (item.id === entryId ? { ...item, hidden: true } : item)), 'Betrieb ausgeblendet.' @@ -596,6 +673,7 @@ function App() { }; const handleToggleActive = (entryId) => { + setIsDirty(true); setConfig((prev) => prev.map((item) => item.id === entryId ? { ...item, active: !item.active } : item @@ -604,6 +682,7 @@ function App() { }; const handleToggleProfileCheck = (entryId) => { + setIsDirty(true); setConfig((prev) => prev.map((item) => item.id === entryId ? { ...item, checkProfileId: !item.checkProfileId } : item @@ -612,6 +691,7 @@ function App() { }; const handleToggleOnlyNotify = (entryId) => { + setIsDirty(true); setConfig((prev) => prev.map((item) => item.id === entryId ? { ...item, onlyNotify: !item.onlyNotify } : item @@ -620,6 +700,7 @@ function App() { }; const handleWeekdayChange = (entryId, value) => { + setIsDirty(true); setConfig((prev) => prev.map((item) => { if (item.id !== entryId) { @@ -635,6 +716,7 @@ function App() { }; const handleDateChange = (entryId, value) => { + setIsDirty(true); setConfig((prev) => prev.map((item) => { if (item.id !== entryId) { @@ -671,6 +753,7 @@ function App() { return; } const message = existing ? 'Betrieb wieder eingeblendet.' : 'Betrieb zur Liste hinzugefügt.'; + setIsDirty(true); await persistConfigUpdate( (prev) => { const already = prev.find((item) => item.id === storeId); @@ -996,6 +1079,15 @@ function App() { target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800 flex items-center" + onClick={(event) => { + if (!isDirty) { + return; + } + event.preventDefault(); + requestNavigation('das Foodsharing-Dashboard zu öffnen', () => + window.open('https://foodsharing.de/?page=dashboard', '_blank', 'noopener,noreferrer') + ); + }} > @@ -1300,7 +1392,7 @@ function App() { <>
- + @@ -1309,13 +1401,22 @@ function App() {
+ ); } -function NavigationTabs({ isAdmin }) { +function NavigationTabs({ isAdmin, onProtectedNavigate }) { const location = useLocation(); + const navigate = useNavigate(); if (!isAdmin) { return null; } @@ -1324,6 +1425,18 @@ function NavigationTabs({ isAdmin }) { { to: '/admin', label: 'Admin' } ]; + const handleClick = (event, tab) => { + event.preventDefault(); + if (location.pathname === tab.to) { + return; + } + if (onProtectedNavigate) { + onProtectedNavigate(`zur Seite "${tab.label}" wechseln`, () => navigate(tab.to)); + } else { + navigate(tab.to); + } + }; + return (
{tabs.map((tab) => { @@ -1332,6 +1445,7 @@ function NavigationTabs({ isAdmin }) { handleClick(event, tab)} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${ isActive ? 'bg-blue-600 text-white shadow' : 'bg-white text-blue-600 border border-blue-200 hover:bg-blue-50' }`} @@ -1359,6 +1473,41 @@ function AdminAccessMessage() { ); } +function DirtyNavigationDialog({ open, message, onSave, onDiscard, onCancel, saving }) { + if (!open) { + return null; + } + return ( +
+
+

Änderungen nicht gespeichert

+

{message || 'Es gibt ungespeicherte Änderungen. Wie möchtest du fortfahren?'}

+
+ + + +
+
+
+ ); +} + function formatEta(seconds) { if (seconds == null || seconds === Infinity) { return null;