diff --git a/src/App.js b/src/App.js index f1f5870..ba638e5 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,7 @@ import useConfigManager from './hooks/useConfigManager'; import useStoreSync from './hooks/useStoreSync'; import useDirtyNavigationGuard from './hooks/useDirtyNavigationGuard'; import useSessionManager from './hooks/useSessionManager'; +import useAdminSettings from './hooks/useAdminSettings'; import NavigationTabs from './components/NavigationTabs'; import LoginView from './components/LoginView'; import DashboardView from './components/DashboardView'; @@ -28,8 +29,6 @@ function App() { const [status, setStatus] = useState(''); const [error, setError] = useState(''); const [availableCollapsed, setAvailableCollapsed] = useState(true); - const [adminSettings, setAdminSettings] = useState(null); - const [adminSettingsLoading, setAdminSettingsLoading] = useState(false); const [initializing, setInitializing] = useState(false); const [activeRangePicker, setActiveRangePicker] = useState(null); const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null }); @@ -86,39 +85,6 @@ function App() { nudgeSyncProgress } = useSyncProgress(); - const normalizeAdminSettings = useCallback((raw) => { - if (!raw) { - return null; - } - return { - scheduleCron: raw.scheduleCron || '', - randomDelayMinSeconds: raw.randomDelayMinSeconds ?? '', - 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) : '', - description: slot?.description || '' - })) - : [], - notifications: { - ntfy: { - enabled: !!raw.notifications?.ntfy?.enabled, - serverUrl: raw.notifications?.ntfy?.serverUrl || 'https://ntfy.sh', - topicPrefix: raw.notifications?.ntfy?.topicPrefix || '', - username: raw.notifications?.ntfy?.username || '', - password: raw.notifications?.ntfy?.password || '' - }, - telegram: { - enabled: !!raw.notifications?.telegram?.enabled, - botToken: raw.notifications?.telegram?.botToken || '' - } - } - }; - }, []); - const { session, authorizedFetch, @@ -129,7 +95,6 @@ function App() { getStoredToken } = useSessionManager({ normalizeConfigEntries, - normalizeAdminSettings, onUnauthorized: notifyUnauthorized, setError, setLoading @@ -169,6 +134,24 @@ function App() { finishSyncProgress }); + const { + adminSettings, + adminSettingsLoading, + setAdminSettingsSnapshot, + clearAdminSettings, + handleAdminSettingChange, + handleAdminNotificationChange, + handleIgnoredSlotChange, + addIgnoredSlot, + removeIgnoredSlot, + saveAdminSettings + } = useAdminSettings({ + session, + authorizedFetch, + setStatus, + setError + }); + const { requestNavigation, dialogState: dirtyDialogState, @@ -187,11 +170,11 @@ function App() { return {}; } setStores(Array.isArray(result.stores) ? result.stores : []); - setAdminSettings(result.adminSettings ?? null); + setAdminSettingsSnapshot(result.adminSettings ?? null); setConfig(Array.isArray(result.config) ? result.config : []); return result; }, - [setStores, setAdminSettings, setConfig] + [setStores, setAdminSettingsSnapshot, setConfig] ); const resetSessionState = useCallback(() => { @@ -199,8 +182,7 @@ function App() { setStores([]); setStatus(''); setError(''); - setAdminSettings(null); - setAdminSettingsLoading(false); + clearAdminSettings(); setAvailableCollapsed(true); setInitializing(false); }, [ @@ -208,8 +190,7 @@ function App() { setStores, setStatus, setError, - setAdminSettings, - setAdminSettingsLoading, + clearAdminSettings, setAvailableCollapsed, setInitializing ]); @@ -237,42 +218,6 @@ function App() { sessionToken: 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); @@ -637,140 +582,6 @@ function App() { setFocusedStoreId(storeId); }; - 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 handleAdminNotificationChange = (channel, field, value) => { - setAdminSettings((prev) => { - if (!prev) { - return prev; - } - return { - ...prev, - notifications: { - ...(prev.notifications || {}), - [channel]: { - ...(prev.notifications?.[channel] || {}), - [field]: value - } - } - }; - }); - }; - - 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), - storePickupCheckDelayMs: toNumber(adminSettings.storePickupCheckDelayMs), - ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({ - storeId: slot.storeId || '', - description: slot.description || '' - })), - notifications: { - ntfy: { - enabled: !!adminSettings.notifications?.ntfy?.enabled, - serverUrl: adminSettings.notifications?.ntfy?.serverUrl || '', - topicPrefix: adminSettings.notifications?.ntfy?.topicPrefix || '', - username: adminSettings.notifications?.ntfy?.username || '', - password: adminSettings.notifications?.ntfy?.password || '' - }, - telegram: { - enabled: !!adminSettings.notifications?.telegram?.enabled, - botToken: adminSettings.notifications?.telegram?.botToken || '' - } - } - }; - - 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 ( <> diff --git a/src/hooks/useAdminSettings.js b/src/hooks/useAdminSettings.js new file mode 100644 index 0000000..5f47f7b --- /dev/null +++ b/src/hooks/useAdminSettings.js @@ -0,0 +1,189 @@ +import { useCallback, useEffect, useState } from 'react'; +import { normalizeAdminSettings, serializeAdminSettings } from '../utils/adminSettings'; + +const useAdminSettings = ({ session, authorizedFetch, setStatus, setError }) => { + const [adminSettings, setAdminSettings] = useState(null); + const [adminSettingsLoading, setAdminSettingsLoading] = useState(false); + + const clearAdminSettings = useCallback(() => { + setAdminSettings(null); + setAdminSettingsLoading(false); + }, []); + + const setAdminSettingsSnapshot = useCallback((snapshot) => { + setAdminSettings(snapshot ? normalizeAdminSettings(snapshot) : null); + }, []); + + const loadAdminSettings = useCallback(async () => { + if (!session?.token || !session.isAdmin) { + clearAdminSettings(); + return; + } + setAdminSettingsLoading(true); + try { + const response = await authorizedFetch('/api/admin/settings'); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const data = await response.json(); + setAdminSettings(normalizeAdminSettings(data)); + } catch (err) { + setError(`Admin-Einstellungen konnten nicht geladen werden: ${err.message}`); + } finally { + setAdminSettingsLoading(false); + } + }, [session?.token, session?.isAdmin, authorizedFetch, setError, clearAdminSettings]); + + useEffect(() => { + if (!session?.token || !session.isAdmin) { + clearAdminSettings(); + 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, setError, clearAdminSettings]); + + const handleAdminSettingChange = useCallback((field, value, isNumber = false) => { + setAdminSettings((prev) => { + if (!prev) { + return prev; + } + let nextValue = value; + if (isNumber) { + nextValue = value === '' ? '' : Number(value); + } + return { + ...prev, + [field]: nextValue + }; + }); + }, []); + + const handleAdminNotificationChange = useCallback((channel, field, value) => { + setAdminSettings((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + notifications: { + ...(prev.notifications || {}), + [channel]: { + ...(prev.notifications?.[channel] || {}), + [field]: value + } + } + }; + }); + }, []); + + const handleIgnoredSlotChange = useCallback((index, field, value) => { + setAdminSettings((prev) => { + if (!prev) { + return prev; + } + const slots = [...(prev.ignoredSlots || [])]; + slots[index] = { + ...slots[index], + [field]: value + }; + return { + ...prev, + ignoredSlots: slots + }; + }); + }, []); + + const addIgnoredSlot = useCallback(() => { + setAdminSettings((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + ignoredSlots: [...(prev.ignoredSlots || []), { storeId: '', description: '' }] + }; + }); + }, []); + + const removeIgnoredSlot = useCallback((index) => { + setAdminSettings((prev) => { + if (!prev) { + return prev; + } + const slots = [...(prev.ignoredSlots || [])]; + slots.splice(index, 1); + return { + ...prev, + ignoredSlots: slots + }; + }); + }, []); + + const saveAdminSettings = useCallback(async () => { + if (!session?.token || !session.isAdmin || !adminSettings) { + return; + } + setStatus('Admin-Einstellungen werden gespeichert...'); + setError(''); + + try { + const payload = serializeAdminSettings(adminSettings); + 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}`); + } + }, [session?.token, session?.isAdmin, adminSettings, authorizedFetch, setStatus, setError]); + + return { + adminSettings, + adminSettingsLoading, + setAdminSettingsSnapshot, + clearAdminSettings, + handleAdminSettingChange, + handleAdminNotificationChange, + handleIgnoredSlotChange, + addIgnoredSlot, + removeIgnoredSlot, + saveAdminSettings, + loadAdminSettings + }; +}; + +export default useAdminSettings; diff --git a/src/hooks/useSessionManager.js b/src/hooks/useSessionManager.js index 78237f1..e602907 100644 --- a/src/hooks/useSessionManager.js +++ b/src/hooks/useSessionManager.js @@ -1,14 +1,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +import { normalizeAdminSettings } from '../utils/adminSettings'; const TOKEN_STORAGE_KEY = 'pickupConfigToken'; -const useSessionManager = ({ - normalizeConfigEntries, - normalizeAdminSettings, - onUnauthorized, - setError, - setLoading -}) => { +const useSessionManager = ({ normalizeConfigEntries, onUnauthorized, setError, setLoading }) => { const [session, setSession] = useState(null); const unauthorizedRef = useRef(onUnauthorized || (() => {})); @@ -127,7 +122,7 @@ const useSessionManager = ({ setLoading(false); } }, - [normalizeAdminSettings, normalizeConfigEntries, handleUnauthorized, setError, setLoading] + [normalizeConfigEntries, handleUnauthorized, setError, setLoading] ); const performLogout = useCallback(async () => { diff --git a/src/utils/adminSettings.js b/src/utils/adminSettings.js new file mode 100644 index 0000000..3f93399 --- /dev/null +++ b/src/utils/adminSettings.js @@ -0,0 +1,71 @@ +export const normalizeAdminSettings = (raw) => { + if (!raw) { + return null; + } + return { + scheduleCron: raw.scheduleCron || '', + randomDelayMinSeconds: raw.randomDelayMinSeconds ?? '', + 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) : '', + description: slot?.description || '' + })) + : [], + notifications: { + ntfy: { + enabled: !!raw.notifications?.ntfy?.enabled, + serverUrl: raw.notifications?.ntfy?.serverUrl || 'https://ntfy.sh', + topicPrefix: raw.notifications?.ntfy?.topicPrefix || '', + username: raw.notifications?.ntfy?.username || '', + password: raw.notifications?.ntfy?.password || '' + }, + telegram: { + enabled: !!raw.notifications?.telegram?.enabled, + botToken: raw.notifications?.telegram?.botToken || '' + } + } + }; +}; + +const toNumberOrUndefined = (value) => { + if (value === '' || value === null || value === undefined) { + return undefined; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +}; + +export const serializeAdminSettings = (adminSettings) => { + if (!adminSettings) { + return null; + } + return { + scheduleCron: adminSettings.scheduleCron, + randomDelayMinSeconds: toNumberOrUndefined(adminSettings.randomDelayMinSeconds), + randomDelayMaxSeconds: toNumberOrUndefined(adminSettings.randomDelayMaxSeconds), + initialDelayMinSeconds: toNumberOrUndefined(adminSettings.initialDelayMinSeconds), + initialDelayMaxSeconds: toNumberOrUndefined(adminSettings.initialDelayMaxSeconds), + storePickupCheckDelayMs: toNumberOrUndefined(adminSettings.storePickupCheckDelayMs), + ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({ + storeId: slot.storeId || '', + description: slot.description || '' + })), + notifications: { + ntfy: { + enabled: !!adminSettings.notifications?.ntfy?.enabled, + serverUrl: adminSettings.notifications?.ntfy?.serverUrl || '', + topicPrefix: adminSettings.notifications?.ntfy?.topicPrefix || '', + username: adminSettings.notifications?.ntfy?.username || '', + password: adminSettings.notifications?.ntfy?.password || '' + }, + telegram: { + enabled: !!adminSettings.notifications?.telegram?.enabled, + botToken: adminSettings.notifications?.telegram?.botToken || '' + } + } + }; +};