diff --git a/src/App.js b/src/App.js
index 6b298d2..d17e8c1 100644
--- a/src/App.js
+++ b/src/App.js
@@ -7,6 +7,9 @@ import 'react-date-range/dist/theme/default.css';
import { formatDateValue, formatRangeLabel } from './utils/dateUtils';
import useSyncProgress from './hooks/useSyncProgress';
import useNotificationSettings from './hooks/useNotificationSettings';
+import useConfigManager from './hooks/useConfigManager';
+import useStoreSync from './hooks/useStoreSync';
+import useDirtyNavigationGuard from './hooks/useDirtyNavigationGuard';
import NavigationTabs from './components/NavigationTabs';
import LoginView from './components/LoginView';
import DashboardView from './components/DashboardView';
@@ -22,7 +25,6 @@ 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('');
@@ -31,19 +33,10 @@ function App() {
const [adminSettings, setAdminSettings] = useState(null);
const [adminSettingsLoading, setAdminSettingsLoading] = useState(false);
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 [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null });
const [activeRangePicker, setActiveRangePicker] = useState(null);
+ const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null });
const [notificationPanelOpen, setNotificationPanelOpen] = useState(false);
const [focusedStoreId, setFocusedStoreId] = useState(null);
- const configRef = useRef(config);
- useEffect(() => {
- configRef.current = config;
- }, [config]);
const minSelectableDate = useMemo(() => startOfDay(new Date()), []);
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
@@ -83,8 +76,6 @@ function App() {
return normalized;
});
}, []);
- const delay = useCallback((ms) => new Promise((resolve) => setTimeout(resolve, ms)), []);
-
const {
syncProgress,
startSyncProgress,
@@ -126,6 +117,74 @@ function App() {
};
}, []);
+ const handleUnauthorizedRef = useRef(() => {});
+
+ 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) {
+ handleUnauthorizedRef.current();
+ throw new Error('Nicht autorisiert');
+ }
+ return response;
+ },
+ [session?.token]
+ );
+
+ const {
+ config,
+ setConfig,
+ isDirty,
+ setIsDirty,
+ persistConfigUpdate,
+ saveConfig
+ } = useConfigManager({
+ sessionToken: session?.token,
+ authorizedFetch,
+ setStatus,
+ setError,
+ setLoading
+ });
+
+ const {
+ fetchConfig,
+ syncStoresWithProgress,
+ refreshStoresAndConfig
+ } = useStoreSync({
+ sessionToken: session?.token,
+ authorizedFetch,
+ setStatus,
+ setError,
+ setLoading,
+ setStores,
+ setConfig,
+ normalizeConfigEntries,
+ setIsDirty,
+ startSyncProgress,
+ updateSyncProgress,
+ finishSyncProgress
+ });
+
+ const {
+ requestNavigation,
+ dialogState: dirtyDialogState,
+ handleDirtySave,
+ handleDirtyDiscard,
+ handleDirtyCancel
+ } = useDirtyNavigationGuard({
+ isDirty,
+ saveConfig,
+ onDiscard: () => setIsDirty(false)
+ });
+
const resetSessionState = useCallback(() => {
setSession(null);
setConfig([]);
@@ -136,7 +195,7 @@ function App() {
setAdminSettingsLoading(false);
setAvailableCollapsed(true);
setInitializing(false);
- }, []);
+ }, [setConfig]);
const handleUnauthorized = useCallback(() => {
resetSessionState();
@@ -147,6 +206,10 @@ function App() {
}
}, [resetSessionState]);
+ useEffect(() => {
+ handleUnauthorizedRef.current = handleUnauthorized;
+ }, [handleUnauthorized]);
+
const bootstrapSession = useCallback(
async (token, { progress } = {}) => {
if (!token) {
@@ -197,27 +260,7 @@ function App() {
}
return {};
},
- [handleUnauthorized, normalizeAdminSettings, normalizeConfigEntries]
- );
-
- 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]
+ [handleUnauthorized, normalizeAdminSettings, normalizeConfigEntries, setConfig]
);
const {
@@ -345,249 +388,6 @@ function App() {
requestNavigation('dich abzumelden', performLogout);
};
- const fetchConfig = useCallback(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(normalizeConfigEntries(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);
- }
- }, [session?.token, authorizedFetch, normalizeConfigEntries]);
-
- const fetchStoresList = useCallback(async (tokenOverride, { silent = false } = {}) => {
- const tokenToUse = tokenOverride || session?.token;
- if (!tokenToUse) {
- return;
- }
- if (!silent) {
- setStatus('');
- }
- setError('');
- try {
- const response = await authorizedFetch('/api/stores', {}, tokenToUse);
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- const data = await response.json();
- setStores(Array.isArray(data) ? data : []);
- if (!silent) {
- setStatus('Betriebe aktualisiert.');
- setTimeout(() => setStatus(''), 3000);
- }
- } catch (err) {
- setError(`Fehler beim Laden der Betriebe: ${err.message}`);
- }
- }, [session?.token, authorizedFetch]);
-
- const syncStoresWithProgress = useCallback(
- async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false, tokenOverride } = {}) => {
- const effectiveToken = tokenOverride || session?.token;
- if (!effectiveToken) {
- return;
- }
- if (!reuseOverlay) {
- startSyncProgress('Betriebe werden geprüft...', 5, block);
- } else {
- updateSyncProgress('Betriebe werden geprüft...', 35);
- }
- try {
- let jobStarted = false;
- const jobStartedAt = Date.now();
- const triggerRefresh = async () => {
- const response = await authorizedFetch('/api/stores/refresh', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ force: true, reason })
- }, effectiveToken);
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- await response.json();
- jobStarted = true;
- };
-
- if (startJob) {
- await triggerRefresh();
- }
-
- let completed = false;
- while (!completed) {
- const statusResp = await authorizedFetch('/api/stores/refresh/status', {}, effectiveToken);
- if (!statusResp.ok) {
- throw new Error(`HTTP ${statusResp.status}`);
- }
- const statusData = await statusResp.json();
- const job = statusData.job;
- if (job?.status === 'running') {
- const total = job.total || 0;
- const processed = job.processed || 0;
- let percent;
- if (total > 0) {
- const ratio = Math.max(0, Math.min(1, processed / total));
- percent = Math.min(99, 5 + Math.round(ratio * 90));
- }
- let etaSeconds = null;
- if (total > 0 && processed > 0) {
- const elapsedSeconds = Math.max(1, (Date.now() - jobStartedAt) / 1000);
- const rate = processed / elapsedSeconds;
- if (rate > 0) {
- const remaining = Math.max(0, total - processed);
- etaSeconds = Math.round(remaining / rate);
- }
- }
- const message = job.currentStore
- ? `Prüfe ${job.currentStore} (${processed}/${total || '?'})`
- : 'Betriebe werden geprüft...';
- updateSyncProgress(message, percent, { etaSeconds });
- } else if (!job) {
- if (statusData.storesFresh) {
- updateSyncProgress('Betriebe aktuell.', 99, { etaSeconds: null });
- completed = true;
- } else if (!jobStarted) {
- await triggerRefresh();
- await delay(500);
- } else {
- updateSyncProgress('Warte auf Rückmeldung...', undefined, { etaSeconds: null });
- }
- } else if (job.status === 'done') {
- updateSyncProgress('Synchronisierung abgeschlossen', 100, { etaSeconds: null });
- completed = true;
- } else if (job.status === 'error') {
- throw new Error(job.error || 'Unbekannter Fehler beim Prüfen der Betriebe.');
- }
- if (!completed) {
- await delay(1000);
- }
- }
-
- await fetchStoresList(effectiveToken, { silent: reason !== 'manual' });
- await fetchConfig(effectiveToken, { silent: true });
- setIsDirty(false);
- setStatus('Betriebe aktualisiert.');
- setTimeout(() => setStatus(''), 3000);
- } catch (err) {
- setError(`Aktualisieren der Betriebe fehlgeschlagen: ${err.message}`);
- } finally {
- if (!reuseOverlay) {
- finishSyncProgress();
- }
- }
- },
- [
- session?.token,
- authorizedFetch,
- startSyncProgress,
- updateSyncProgress,
- finishSyncProgress,
- delay,
- fetchStoresList,
- fetchConfig,
- setError,
- setStatus
- ]
- );
-
- 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);
- }, []);
-
const askConfirmation = useCallback(
(options = {}) =>
new Promise((resolve) => {
@@ -685,50 +485,6 @@ function App() {
};
}, [isDirty]);
- const persistConfigUpdate = useCallback(
- async (updater, successMessage, { autoCommit = false } = {}) => {
- const baseConfig = configRef.current;
- const nextConfigState = typeof updater === 'function' ? updater(baseConfig) : updater;
- if (!nextConfigState) {
- return;
- }
- setConfig(nextConfigState);
- if (!session?.token) {
- setIsDirty(true);
- return;
- }
- if (!autoCommit) {
- setIsDirty(true);
- }
- 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);
- setIsDirty(false);
- } catch (err) {
- setError(`Fehler beim Speichern: ${err.message}`);
- if (autoCommit) {
- setIsDirty(true);
- }
- }
- },
- [authorizedFetch, session?.token]
- );
-
const deleteEntry = async (entryId) => {
const confirmed = await askConfirmation({
title: 'Eintrag löschen',
@@ -1204,12 +960,12 @@ function App() {
{
+ const [config, setConfig] = useState([]);
+ const [isDirty, setIsDirty] = useState(false);
+ const configRef = useRef(config);
+
+ useEffect(() => {
+ configRef.current = config;
+ }, [config]);
+
+ const persistConfigUpdate = useCallback(
+ async (updater, successMessage, { autoCommit = false } = {}) => {
+ const baseConfig = configRef.current;
+ const nextConfigState = typeof updater === 'function' ? updater(baseConfig) : updater;
+ if (!nextConfigState) {
+ return;
+ }
+ setConfig(nextConfigState);
+ if (!sessionToken) {
+ setIsDirty(true);
+ return;
+ }
+ if (!autoCommit) {
+ setIsDirty(true);
+ }
+ setStatus('Speichere...');
+ setError('');
+ try {
+ const response = await authorizedFetch(CONFIG_ENDPOINT, {
+ 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');
+ }
+ setStatus(successMessage || 'Konfiguration gespeichert.');
+ setTimeout(() => setStatus(''), 3000);
+ setIsDirty(false);
+ } catch (error) {
+ setError(`Fehler beim Speichern: ${error.message}`);
+ if (autoCommit) {
+ setIsDirty(true);
+ }
+ }
+ },
+ [authorizedFetch, sessionToken, setError, setStatus]
+ );
+
+ const saveConfig = useCallback(async () => {
+ if (!sessionToken) {
+ return false;
+ }
+ setStatus('Speichere...');
+ setError('');
+ setLoading(true);
+ try {
+ const response = await authorizedFetch(CONFIG_ENDPOINT, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(configRef.current)
+ });
+ 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');
+ }
+ setStatus('Konfiguration erfolgreich gespeichert!');
+ setTimeout(() => setStatus(''), 3000);
+ setIsDirty(false);
+ return true;
+ } catch (error) {
+ setError(`Fehler beim Speichern: ${error.message}`);
+ setStatus('');
+ return false;
+ } finally {
+ setLoading(false);
+ }
+ }, [authorizedFetch, sessionToken, setError, setLoading, setStatus]);
+
+ return {
+ config,
+ setConfig,
+ isDirty,
+ setIsDirty,
+ persistConfigUpdate,
+ saveConfig
+ };
+};
+
+export default useConfigManager;
diff --git a/src/hooks/useDirtyNavigationGuard.js b/src/hooks/useDirtyNavigationGuard.js
new file mode 100644
index 0000000..3f15ac3
--- /dev/null
+++ b/src/hooks/useDirtyNavigationGuard.js
@@ -0,0 +1,69 @@
+import { useCallback, useState } from 'react';
+
+const DEFAULT_MESSAGE = 'Änderungen wurden noch nicht gespeichert.';
+
+const useDirtyNavigationGuard = ({ isDirty, saveConfig, onDiscard }) => {
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [dialogMessage, setDialogMessage] = useState(DEFAULT_MESSAGE);
+ const [dialogSaving, setDialogSaving] = useState(false);
+ const [pendingNavigation, setPendingNavigation] = useState(null);
+
+ const requestNavigation = useCallback(
+ (message, action = () => {}) => {
+ if (!isDirty) {
+ action();
+ return;
+ }
+ setDialogMessage(message || DEFAULT_MESSAGE);
+ setPendingNavigation(() => action);
+ setDialogOpen(true);
+ },
+ [isDirty]
+ );
+
+ const handleDirtySave = useCallback(async () => {
+ if (!pendingNavigation) {
+ setDialogOpen(false);
+ return;
+ }
+ setDialogSaving(true);
+ const success = await saveConfig();
+ setDialogSaving(false);
+ if (!success) {
+ return;
+ }
+ const action = pendingNavigation;
+ setPendingNavigation(null);
+ setDialogOpen(false);
+ action();
+ }, [pendingNavigation, saveConfig]);
+
+ const handleDirtyDiscard = useCallback(() => {
+ const action = pendingNavigation;
+ setPendingNavigation(null);
+ setDialogOpen(false);
+ onDiscard?.();
+ if (action) {
+ action();
+ }
+ }, [pendingNavigation, onDiscard]);
+
+ const handleDirtyCancel = useCallback(() => {
+ setDialogOpen(false);
+ setPendingNavigation(null);
+ }, []);
+
+ return {
+ requestNavigation,
+ dialogState: {
+ open: dialogOpen,
+ message: dialogMessage,
+ saving: dialogSaving
+ },
+ handleDirtySave,
+ handleDirtyDiscard,
+ handleDirtyCancel
+ };
+};
+
+export default useDirtyNavigationGuard;
diff --git a/src/hooks/useStoreSync.js b/src/hooks/useStoreSync.js
new file mode 100644
index 0000000..33f5d1e
--- /dev/null
+++ b/src/hooks/useStoreSync.js
@@ -0,0 +1,204 @@
+import { useCallback } from 'react';
+
+const useStoreSync = ({
+ sessionToken,
+ authorizedFetch,
+ setStatus,
+ setError,
+ setLoading,
+ setStores,
+ setConfig,
+ normalizeConfigEntries,
+ setIsDirty,
+ startSyncProgress,
+ updateSyncProgress,
+ finishSyncProgress
+}) => {
+ const delay = useCallback((ms) => new Promise((resolve) => setTimeout(resolve, ms)), []);
+
+ const fetchConfig = useCallback(
+ async (tokenOverride, { silent = false } = {}) => {
+ const tokenToUse = tokenOverride || sessionToken;
+ 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(normalizeConfigEntries(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);
+ }
+ },
+ [sessionToken, authorizedFetch, setStatus, setLoading, setError, setConfig, normalizeConfigEntries]
+ );
+
+ const fetchStoresList = useCallback(
+ async (tokenOverride, { silent = false } = {}) => {
+ const tokenToUse = tokenOverride || sessionToken;
+ if (!tokenToUse) {
+ return;
+ }
+ if (!silent) {
+ setStatus('');
+ }
+ setError('');
+ try {
+ const response = await authorizedFetch('/api/stores', {}, tokenToUse);
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const data = await response.json();
+ setStores(Array.isArray(data) ? data : []);
+ if (!silent) {
+ setStatus('Betriebe aktualisiert.');
+ setTimeout(() => setStatus(''), 3000);
+ }
+ } catch (err) {
+ setError(`Fehler beim Laden der Betriebe: ${err.message}`);
+ }
+ },
+ [sessionToken, authorizedFetch, setStatus, setError, setStores]
+ );
+
+ const syncStoresWithProgress = useCallback(
+ async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false, tokenOverride } = {}) => {
+ const effectiveToken = tokenOverride || sessionToken;
+ if (!effectiveToken) {
+ return;
+ }
+ if (!reuseOverlay) {
+ startSyncProgress('Betriebe werden geprüft...', 5, block);
+ } else {
+ updateSyncProgress('Betriebe werden geprüft...', 35);
+ }
+ try {
+ let jobStarted = false;
+ const jobStartedAt = Date.now();
+ const triggerRefresh = async () => {
+ const response = await authorizedFetch(
+ '/api/stores/refresh',
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ force: true, reason })
+ },
+ effectiveToken
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ await response.json();
+ jobStarted = true;
+ };
+
+ if (startJob) {
+ await triggerRefresh();
+ }
+
+ let completed = false;
+ while (!completed) {
+ const statusResp = await authorizedFetch('/api/stores/refresh/status', {}, effectiveToken);
+ if (!statusResp.ok) {
+ throw new Error(`HTTP ${statusResp.status}`);
+ }
+ const statusData = await statusResp.json();
+ const job = statusData.job;
+ if (job?.status === 'running') {
+ const total = job.total || 0;
+ const processed = job.processed || 0;
+ let percent;
+ if (total > 0) {
+ const ratio = Math.max(0, Math.min(1, processed / total));
+ percent = Math.min(99, 5 + Math.round(ratio * 90));
+ }
+ let etaSeconds = null;
+ if (total > 0 && processed > 0) {
+ const elapsedSeconds = Math.max(1, (Date.now() - jobStartedAt) / 1000);
+ const rate = processed / elapsedSeconds;
+ if (rate > 0) {
+ const remaining = Math.max(0, total - processed);
+ etaSeconds = Math.round(remaining / rate);
+ }
+ }
+ const message = job.currentStore
+ ? `Prüfe ${job.currentStore} (${processed}/${total || '?'})`
+ : 'Betriebe werden geprüft...';
+ updateSyncProgress(message, percent, { etaSeconds });
+ } else if (!job) {
+ if (statusData.storesFresh) {
+ updateSyncProgress('Betriebe aktuell.', 99, { etaSeconds: null });
+ completed = true;
+ } else if (!jobStarted) {
+ await triggerRefresh();
+ await delay(500);
+ } else {
+ updateSyncProgress('Warte auf Rückmeldung...', undefined, { etaSeconds: null });
+ }
+ } else if (job.status === 'done') {
+ updateSyncProgress('Synchronisierung abgeschlossen', 100, { etaSeconds: null });
+ completed = true;
+ } else if (job.status === 'error') {
+ throw new Error(job.error || 'Unbekannter Fehler beim Prüfen der Betriebe.');
+ }
+ if (!completed) {
+ await delay(1000);
+ }
+ }
+
+ await fetchStoresList(effectiveToken, { silent: reason !== 'manual' });
+ await fetchConfig(effectiveToken, { silent: true });
+ setIsDirty(false);
+ setStatus('Betriebe aktualisiert.');
+ setTimeout(() => setStatus(''), 3000);
+ } catch (err) {
+ setError(`Aktualisieren der Betriebe fehlgeschlagen: ${err.message}`);
+ } finally {
+ if (!reuseOverlay) {
+ finishSyncProgress();
+ }
+ }
+ },
+ [
+ sessionToken,
+ authorizedFetch,
+ startSyncProgress,
+ updateSyncProgress,
+ finishSyncProgress,
+ delay,
+ fetchStoresList,
+ fetchConfig,
+ setIsDirty,
+ setStatus,
+ setError
+ ]
+ );
+
+ const refreshStoresAndConfig = useCallback(
+ ({ block = false } = {}) => syncStoresWithProgress({ block, reason: 'manual', startJob: true }),
+ [syncStoresWithProgress]
+ );
+
+ return {
+ fetchConfig,
+ fetchStoresList,
+ syncStoresWithProgress,
+ refreshStoresAndConfig
+ };
+};
+
+export default useStoreSync;