aktueller Stand

This commit is contained in:
root
2025-11-09 15:04:29 +01:00
parent e90f9299c3
commit 685d50d56a
4 changed files with 176 additions and 46 deletions

View File

@@ -25,9 +25,39 @@ function App() {
const [availableCollapsed, setAvailableCollapsed] = useState(true);
const [adminSettings, setAdminSettings] = useState(null);
const [adminSettingsLoading, setAdminSettingsLoading] = useState(false);
const [syncProgress, setSyncProgress] = useState({ active: false, percent: 0, message: '', block: false });
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
const startSyncProgress = useCallback((message, percent, block = false) => {
setSyncProgress({ active: true, percent, message, block });
}, []);
const updateSyncProgress = useCallback((message, percent) => {
setSyncProgress((prev) => {
if (!prev.active) {
return prev;
}
return {
...prev,
message: message || prev.message,
percent: Math.min(100, Math.max(percent, prev.percent))
};
});
}, []);
const finishSyncProgress = useCallback(() => {
setSyncProgress((prev) => {
if (!prev.active) {
return prev;
}
return { ...prev, percent: 100 };
});
setTimeout(() => {
setSyncProgress({ active: false, percent: 0, message: '', block: false });
}, 400);
}, []);
const normalizeAdminSettings = useCallback((raw) => {
if (!raw) {
return null;
@@ -38,6 +68,7 @@ function App() {
randomDelayMaxSeconds: raw.randomDelayMaxSeconds ?? '',
initialDelayMinSeconds: raw.initialDelayMinSeconds ?? '',
initialDelayMaxSeconds: raw.initialDelayMaxSeconds ?? '',
storePickupCheckDelayMs: raw.storePickupCheckDelayMs ?? '',
ignoredSlots: Array.isArray(raw.ignoredSlots)
? raw.ignoredSlots.map((slot) => ({
storeId: slot?.storeId ? String(slot.storeId) : '',
@@ -70,9 +101,15 @@ function App() {
}, [resetSessionState]);
const bootstrapSession = useCallback(
async (token) => {
async (token, { withProgress = false } = {}) => {
if (!token) {
return;
}
setLoading(true);
setError('');
if (withProgress) {
startSyncProgress('Session wird aufgebaut...', 10, true);
}
try {
const response = await fetch('/api/auth/session', {
headers: { Authorization: `Bearer ${token}` }
@@ -86,6 +123,9 @@ function App() {
}
const data = await response.json();
setSession({ token, profile: data.profile, isAdmin: data.isAdmin });
if (withProgress) {
updateSyncProgress('Betriebe werden geprüft...', 45);
}
setStores(Array.isArray(data.stores) ? data.stores : []);
setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null);
@@ -100,14 +140,23 @@ function App() {
throw new Error(`HTTP ${configResponse.status}`);
}
const configData = await configResponse.json();
if (withProgress) {
updateSyncProgress('Konfiguration wird geladen...', 75);
}
setConfig(Array.isArray(configData) ? configData : []);
if (withProgress) {
updateSyncProgress('Synchronisierung abgeschlossen', 95);
}
} catch (err) {
setError(`Session konnte nicht wiederhergestellt werden: ${err.message}`);
} finally {
setLoading(false);
if (withProgress) {
finishSyncProgress();
}
}
},
[handleUnauthorized, normalizeAdminSettings]
[handleUnauthorized, normalizeAdminSettings, startSyncProgress, updateSyncProgress, finishSyncProgress]
);
useEffect(() => {
@@ -200,10 +249,7 @@ function App() {
} catch (storageError) {
console.warn('Konnte Token nicht speichern:', storageError);
}
setSession({ token: data.token, profile: data.profile, isAdmin: data.isAdmin });
setConfig(Array.isArray(data.config) ? data.config : []);
setStores(Array.isArray(data.stores) ? data.stores : []);
setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null);
await bootstrapSession(data.token, { withProgress: true });
setStatus('Anmeldung erfolgreich. Konfiguration geladen.');
setTimeout(() => setStatus(''), 3000);
} catch (err) {
@@ -276,6 +322,24 @@ function App() {
}
};
const refreshStoresAndConfig = useCallback(
async ({ block = false } = {}) => {
if (!session?.token) {
return;
}
startSyncProgress('Betriebe werden geprüft...', 15, block);
try {
await fetchStoresList();
updateSyncProgress('Konfiguration wird aktualisiert...', 70);
await fetchConfig(undefined, { silent: true });
updateSyncProgress('Synchronisierung abgeschlossen', 95);
} finally {
finishSyncProgress();
}
},
[session?.token, fetchStoresList, fetchConfig, startSyncProgress, updateSyncProgress, finishSyncProgress]
);
const saveConfig = async () => {
if (!session?.token) {
return;
@@ -571,6 +635,7 @@ function App() {
randomDelayMaxSeconds: toNumber(adminSettings.randomDelayMaxSeconds),
initialDelayMinSeconds: toNumber(adminSettings.initialDelayMinSeconds),
initialDelayMaxSeconds: toNumber(adminSettings.initialDelayMaxSeconds),
storePickupCheckDelayMs: toNumber(adminSettings.storePickupCheckDelayMs),
ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({
storeId: slot.storeId || '',
description: slot.description || ''
@@ -672,7 +737,7 @@ function App() {
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={fetchStoresList}
onClick={() => refreshStoresAndConfig({ block: false })}
className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
>
Betriebe aktualisieren
@@ -721,10 +786,11 @@ function App() {
statusClass = 'text-gray-500';
} else if (needsRestore) {
statusLabel = 'Ausgeblendet erneut hinzufügen';
statusClass = 'text-amber-600';
}
if (blockedByNoPickups) {
statusLabel = 'Keine Pickups automatisch verborgen';
statusClass = 'text-amber-600';
statusClass = 'text-red-600';
}
return (
<button
@@ -1104,6 +1170,18 @@ function App() {
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Verzögerung Store-Prüfung (ms)</label>
<input
type="number"
min="0"
value={adminSettings.storePickupCheckDelayMs}
onChange={(e) => handleAdminSettingChange('storePickupCheckDelayMs', e.target.value, true)}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="z. B. 400"
/>
<p className="text-xs text-gray-500 mt-1">Hilft Rate-Limits beim Abfragen der Pickups zu vermeiden.</p>
</div>
</div>
<div className="border border-purple-200 rounded-lg bg-white p-4">
@@ -1166,16 +1244,19 @@ function App() {
return (
<Router>
<div className="min-h-screen bg-gray-100 py-6">
<div className="max-w-7xl mx-auto px-4">
<NavigationTabs isAdmin={session?.isAdmin} />
<Routes>
<Route path="/" element={dashboardContent} />
<Route path="/admin" element={adminPageContent} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
<>
<div className="min-h-screen bg-gray-100 py-6">
<div className="max-w-7xl mx-auto px-4">
<NavigationTabs isAdmin={session?.isAdmin} />
<Routes>
<Route path="/" element={dashboardContent} />
<Route path="/admin" element={adminPageContent} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</div>
</div>
</div>
<StoreSyncOverlay state={syncProgress} />
</>
</Router>
);
}
@@ -1222,4 +1303,32 @@ function AdminAccessMessage() {
);
}
function StoreSyncOverlay({ state }) {
if (!state?.active) {
return null;
}
const percent = Math.round(state.percent || 0);
const backgroundColor = state.block ? 'rgba(255,255,255,0.95)' : 'rgba(15,23,42,0.4)';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center px-4" style={{ backgroundColor }}>
<div className="bg-white shadow-2xl rounded-lg p-6 w-full max-w-md">
<p className="text-gray-800 font-semibold mb-3">{state.message || 'Synchronisiere...'}</p>
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
<div
className="h-3 bg-blue-600 transition-all duration-300 ease-out"
style={{ width: `${percent}%` }}
></div>
</div>
<div className="flex items-center justify-between mt-2 text-sm text-gray-500">
<span>{percent}%</span>
<span>Bitte warten...</span>
</div>
<p className="text-xs text-gray-400 mt-2">
Die Verzögerung schützt vor Rate-Limits während die Betriebe geprüft werden.
</p>
</div>
</div>
);
}
export default App;