From 5341a2e4ba0316f92e56469ec0440eb30f9128c7 Mon Sep 17 00:00:00 2001 From: Meik Date: Mon, 10 Nov 2025 13:30:25 +0100 Subject: [PATCH] refactoring --- src/App.js | 406 ++++++--------------------- src/hooks/useConfigManager.js | 100 +++++++ src/hooks/useDirtyNavigationGuard.js | 69 +++++ src/hooks/useStoreSync.js | 204 ++++++++++++++ 4 files changed, 454 insertions(+), 325 deletions(-) create mode 100644 src/hooks/useConfigManager.js create mode 100644 src/hooks/useDirtyNavigationGuard.js create mode 100644 src/hooks/useStoreSync.js 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;