refactoring
This commit is contained in:
100
src/hooks/useConfigManager.js
Normal file
100
src/hooks/useConfigManager.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const CONFIG_ENDPOINT = '/api/config';
|
||||
|
||||
const useConfigManager = ({ sessionToken, authorizedFetch, setStatus, setError, setLoading }) => {
|
||||
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;
|
||||
69
src/hooks/useDirtyNavigationGuard.js
Normal file
69
src/hooks/useDirtyNavigationGuard.js
Normal file
@@ -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;
|
||||
204
src/hooks/useStoreSync.js
Normal file
204
src/hooks/useStoreSync.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user