import React, { useState, useEffect, useCallback, useMemo } from 'react'; import './App.css'; const emptyEntry = { id: '', label: '', active: false, checkProfileId: true, onlyNotify: false }; const TOKEN_STORAGE_KEY = 'pickupConfigToken'; function App() { const [session, setSession] = useState(null); const [credentials, setCredentials] = useState({ email: '', password: '' }); const [config, setConfig] = useState([]); const [stores, setStores] = useState([]); const [loading, setLoading] = useState(false); const [status, setStatus] = useState(''); const [error, setError] = useState(''); const [newEntry, setNewEntry] = useState(emptyEntry); const [showNewEntryForm, setShowNewEntryForm] = useState(false); const [availableCollapsed, setAvailableCollapsed] = useState(true); const [adminSettings, setAdminSettings] = useState(null); const [adminSettingsLoading, setAdminSettingsLoading] = useState(false); const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']; const normalizeAdminSettings = useCallback((raw) => { if (!raw) { return null; } return { scheduleCron: raw.scheduleCron || '', randomDelayMinSeconds: raw.randomDelayMinSeconds ?? '', randomDelayMaxSeconds: raw.randomDelayMaxSeconds ?? '', initialDelayMinSeconds: raw.initialDelayMinSeconds ?? '', initialDelayMaxSeconds: raw.initialDelayMaxSeconds ?? '', ignoredSlots: Array.isArray(raw.ignoredSlots) ? raw.ignoredSlots.map((slot) => ({ storeId: slot?.storeId ? String(slot.storeId) : '', description: slot?.description || '' })) : [] }; }, []); const resetSessionState = useCallback(() => { setSession(null); setConfig([]); setStores([]); setStatus(''); setError(''); setShowNewEntryForm(false); setNewEntry(emptyEntry); setAdminSettings(null); setAdminSettingsLoading(false); setAvailableCollapsed(true); }, []); const handleUnauthorized = useCallback(() => { resetSessionState(); try { localStorage.removeItem(TOKEN_STORAGE_KEY); } catch (storageError) { console.warn('Konnte Token nicht aus dem Speicher entfernen:', storageError); } }, [resetSessionState]); const bootstrapSession = useCallback( async (token) => { setLoading(true); setError(''); try { const response = await fetch('/api/auth/session', { headers: { Authorization: `Bearer ${token}` } }); if (response.status === 401) { handleUnauthorized(); return; } if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); setSession({ token, profile: data.profile, isAdmin: data.isAdmin }); setStores(Array.isArray(data.stores) ? data.stores : []); setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null); const configResponse = await fetch('/api/config', { headers: { Authorization: `Bearer ${token}` } }); if (configResponse.status === 401) { handleUnauthorized(); return; } if (!configResponse.ok) { throw new Error(`HTTP ${configResponse.status}`); } const configData = await configResponse.json(); setConfig(Array.isArray(configData) ? configData : []); } catch (err) { setError(`Session konnte nicht wiederhergestellt werden: ${err.message}`); } finally { setLoading(false); } }, [handleUnauthorized, normalizeAdminSettings] ); useEffect(() => { try { const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY); if (storedToken) { bootstrapSession(storedToken); } } catch (err) { console.warn('Konnte gespeicherten Token nicht lesen:', err); } }, [bootstrapSession]); const authorizedFetch = useCallback( async (url, options = {}, tokenOverride) => { const activeToken = tokenOverride || session?.token; if (!activeToken) { throw new Error('Keine aktive Session'); } const headers = { Authorization: `Bearer ${activeToken}`, ...(options.headers || {}) }; const response = await fetch(url, { ...options, headers }); if (response.status === 401) { handleUnauthorized(); throw new Error('Nicht autorisiert'); } return response; }, [handleUnauthorized, session?.token] ); useEffect(() => { if (!session?.token || !session.isAdmin) { setAdminSettings(null); setAdminSettingsLoading(false); return; } let cancelled = false; setAdminSettingsLoading(true); (async () => { try { const response = await authorizedFetch('/api/admin/settings'); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); if (!cancelled) { setAdminSettings(normalizeAdminSettings(data)); } } catch (err) { if (!cancelled) { setError(`Admin-Einstellungen konnten nicht geladen werden: ${err.message}`); } } finally { if (!cancelled) { setAdminSettingsLoading(false); } } })(); return () => { cancelled = true; }; }, [session?.token, session?.isAdmin, authorizedFetch, normalizeAdminSettings]); const handleLogin = async (event) => { event.preventDefault(); setLoading(true); setError(''); setStatus(''); try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); try { localStorage.setItem(TOKEN_STORAGE_KEY, data.token); } 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); setStatus('Anmeldung erfolgreich. Konfiguration geladen.'); setTimeout(() => setStatus(''), 3000); } catch (err) { setError(`Login fehlgeschlagen: ${err.message}`); } finally { setLoading(false); } }; const handleLogout = async () => { if (!session?.token) { handleUnauthorized(); return; } try { await authorizedFetch('/api/auth/logout', { method: 'POST' }); } catch (err) { console.warn('Logout fehlgeschlagen:', err); } finally { handleUnauthorized(); } }; const fetchConfig = async (tokenOverride, { silent = false } = {}) => { const tokenToUse = tokenOverride || session?.token; if (!tokenToUse) { return; } if (!silent) { setStatus(''); } setLoading(true); setError(''); try { const response = await authorizedFetch('/api/config', {}, tokenToUse); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); setConfig(Array.isArray(data) ? data : []); if (!silent) { setStatus('Konfiguration aktualisiert.'); setTimeout(() => setStatus(''), 3000); } } catch (err) { setError(`Fehler beim Laden der Konfiguration: ${err.message}`); } finally { setLoading(false); } }; const fetchStoresList = async () => { if (!session?.token) { return; } setStatus(''); setError(''); try { const response = await authorizedFetch('/api/stores'); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); setStores(Array.isArray(data) ? data : []); await fetchConfig(undefined, { silent: true }); setStatus('Betriebe aktualisiert.'); setTimeout(() => setStatus(''), 3000); } catch (err) { setError(`Fehler beim Laden der Betriebe: ${err.message}`); } }; 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}`); } 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(''); } }; const persistConfigUpdate = async (updater, successMessage) => { if (!session?.token) { return; } let nextConfigState; setConfig((prev) => { nextConfigState = typeof updater === 'function' ? updater(prev) : updater; return nextConfigState; }); if (!nextConfigState) { return; } setStatus('Speichere...'); setError(''); try { const response = await authorizedFetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(nextConfigState) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Unbekannter Fehler beim Speichern'); } const message = successMessage || 'Konfiguration gespeichert.'; setStatus(message); setTimeout(() => setStatus(''), 3000); } catch (err) { setError(`Fehler beim Speichern: ${err.message}`); } }; const addEntry = () => { if (!newEntry.id || !newEntry.label) { setError('ID und Bezeichnung müssen ausgefüllt werden!'); return; } const normalized = { ...newEntry, id: String(newEntry.id), hidden: false }; const updatedConfig = [...config.filter((item) => item.id !== normalized.id), normalized]; setConfig(updatedConfig); setNewEntry(emptyEntry); setShowNewEntryForm(false); setStatus('Neuer Eintrag hinzugefügt!'); setTimeout(() => setStatus(''), 3000); }; const deleteEntry = (entryId) => { if (window.confirm('Soll dieser Eintrag dauerhaft gelöscht werden?')) { const updatedConfig = config.filter((item) => item.id !== entryId); setConfig(updatedConfig); setStatus('Eintrag gelöscht!'); setTimeout(() => setStatus(''), 3000); } }; const hideEntry = async (entryId) => { if (!window.confirm('Soll dieser Betrieb ausgeblendet werden?')) { return; } await persistConfigUpdate( (prev) => prev.map((item) => (item.id === entryId ? { ...item, hidden: true } : item)), 'Betrieb ausgeblendet.' ); }; const handleToggleActive = (entryId) => { setConfig((prev) => prev.map((item) => item.id === entryId ? { ...item, active: !item.active } : item ) ); }; const handleToggleProfileCheck = (entryId) => { setConfig((prev) => prev.map((item) => item.id === entryId ? { ...item, checkProfileId: !item.checkProfileId } : item ) ); }; const handleToggleOnlyNotify = (entryId) => { setConfig((prev) => prev.map((item) => item.id === entryId ? { ...item, onlyNotify: !item.onlyNotify } : item ) ); }; const handleWeekdayChange = (entryId, value) => { setConfig((prev) => prev.map((item) => { if (item.id !== entryId) { return item; } const updated = { ...item, desiredWeekday: value || null }; if (value && updated.desiredDate) { delete updated.desiredDate; } return updated; }) ); }; const handleDateChange = (entryId, value) => { setConfig((prev) => prev.map((item) => { if (item.id !== entryId) { return item; } const updated = { ...item, desiredDate: value || null }; if (value && updated.desiredWeekday) { delete updated.desiredWeekday; } return updated; }) ); }; const handleNewEntryChange = (event) => { const { name, value, type, checked } = event.target; setNewEntry({ ...newEntry, [name]: type === 'checkbox' ? checked : value }); }; const configMap = useMemo(() => { const map = new Map(); config.forEach((item) => { if (item?.id) { map.set(String(item.id), item); } }); return map; }, [config]); const visibleConfig = useMemo(() => config.filter((item) => !item.hidden), [config]); const handleStoreSelection = async (store) => { const storeId = String(store.id); const existing = configMap.get(storeId); if (existing && !existing.hidden) { return; } if (!window.confirm(`Soll der Betrieb "${store.name}" zur Liste hinzugefügt werden?`)) { return; } const message = existing ? 'Betrieb wieder eingeblendet.' : 'Betrieb zur Liste hinzugefügt.'; await persistConfigUpdate( (prev) => { const already = prev.find((item) => item.id === storeId); if (already) { return prev.map((item) => item.id === storeId ? { ...item, hidden: false, label: item.label || store.name || `Store ${storeId}` } : item ); } return [ ...prev, { id: storeId, label: store.name || `Store ${storeId}`, active: false, checkProfileId: true, onlyNotify: false, hidden: false } ]; }, message ); }; const handleAdminSettingChange = (field, value, isNumber = false) => { setAdminSettings((prev) => { if (!prev) { return prev; } let nextValue = value; if (isNumber) { nextValue = value === '' ? '' : Number(value); } return { ...prev, [field]: nextValue }; }); }; const handleIgnoredSlotChange = (index, field, value) => { setAdminSettings((prev) => { if (!prev) { return prev; } const slots = [...(prev.ignoredSlots || [])]; slots[index] = { ...slots[index], [field]: field === 'storeId' ? value : value }; return { ...prev, ignoredSlots: slots }; }); }; const addIgnoredSlot = () => { setAdminSettings((prev) => { if (!prev) { return prev; } return { ...prev, ignoredSlots: [...(prev.ignoredSlots || []), { storeId: '', description: '' }] }; }); }; const removeIgnoredSlot = (index) => { setAdminSettings((prev) => { if (!prev) { return prev; } const slots = [...(prev.ignoredSlots || [])]; slots.splice(index, 1); return { ...prev, ignoredSlots: slots }; }); }; const saveAdminSettings = async () => { if (!session?.token || !session.isAdmin || !adminSettings) { return; } setStatus('Admin-Einstellungen werden gespeichert...'); setError(''); const toNumber = (value) => { if (value === '' || value === null || value === undefined) { return undefined; } const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; }; try { const payload = { scheduleCron: adminSettings.scheduleCron, randomDelayMinSeconds: toNumber(adminSettings.randomDelayMinSeconds), randomDelayMaxSeconds: toNumber(adminSettings.randomDelayMaxSeconds), initialDelayMinSeconds: toNumber(adminSettings.initialDelayMinSeconds), initialDelayMaxSeconds: toNumber(adminSettings.initialDelayMaxSeconds), ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({ storeId: slot.storeId || '', description: slot.description || '' })) }; const response = await authorizedFetch('/api/admin/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); setAdminSettings(normalizeAdminSettings(data)); setStatus('Admin-Einstellungen gespeichert.'); setTimeout(() => setStatus(''), 3000); } catch (err) { setError(`Speichern der Admin-Einstellungen fehlgeschlagen: ${err.message}`); } }; if (!session?.token) { return (

Pickup Config Login

Die Anwendung authentifiziert sich direkt bei foodsharing.de. Bitte verwende deine persönlichen Login-Daten.

{error && (
{error}
)}
setCredentials({ ...credentials, email: e.target.value })} className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" required />
setCredentials({ ...credentials, password: e.target.value })} className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" required />
); } if (loading) { return (

Lade Daten...

); } return (

Foodsharing Pickup Manager

Angemeldet

{session.profile.name}

Profil-ID: {session.profile.id}

{!availableCollapsed && (
{stores.length === 0 && (
Noch keine Betriebe geladen. Aktualisiere nach dem Login.
)} {stores.map((store) => { const storeId = String(store.id); const entry = configMap.get(storeId); const isVisible = entry && !entry.hidden; const needsRestore = entry && entry.hidden; let statusLabel = 'Hinzufügen'; if (isVisible) { statusLabel = 'Bereits in Konfiguration'; } else if (needsRestore) { statusLabel = 'Ausgeblendet – erneut hinzufügen'; } return ( ); })}
)}
Foodsharing Dashboard
{error && (
{error}
)} {status && (
{status}
)}
{visibleConfig.length === 0 && ( )} {visibleConfig.map((item, index) => ( ))}
Aktiv Geschäft Profil prüfen Nur benachrichtigen Wochentag Spezifisches Datum Aktionen
Keine sichtbaren Einträge. Nutze „Verfügbare Betriebe“, um Betriebe hinzuzufügen oder ausgeblendete Einträge zurückzuholen.
handleToggleActive(item.id)} className="h-5 w-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500" />
{item.label}
ID: {item.id}
handleToggleProfileCheck(item.id)} className="h-5 w-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500" /> handleToggleOnlyNotify(item.id)} className="h-5 w-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500" /> handleDateChange(item.id, e.target.value)} className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" disabled={item.desiredWeekday} />
{session?.isAdmin && (

Admin-Einstellungen

{adminSettingsLoading &&

Lade Admin-Einstellungen...

} {!adminSettingsLoading && !adminSettings && (

Keine Admin-Einstellungen verfügbar.

)} {adminSettings && ( <>
handleAdminSettingChange('scheduleCron', e.target.value)} className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
handleAdminSettingChange('initialDelayMinSeconds', 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="Min" /> handleAdminSettingChange('initialDelayMaxSeconds', 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="Max" />
handleAdminSettingChange('randomDelayMinSeconds', 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="Min" /> handleAdminSettingChange('randomDelayMaxSeconds', 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="Max" />

Ignorierte Slots

{(!adminSettings.ignoredSlots || adminSettings.ignoredSlots.length === 0) && (

Keine Regeln definiert.

)} {adminSettings.ignoredSlots?.map((slot, index) => (
handleIgnoredSlotChange(index, 'storeId', e.target.value)} placeholder="Store-ID" className="md:col-span-2 border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" /> handleIgnoredSlotChange(index, 'description', e.target.value)} placeholder="Beschreibung (optional)" className="md:col-span-2 border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
))}
)}
)} {showNewEntryForm ? (

Neuen Eintrag hinzufügen

) : ( )}
); } export default App;