refactoring

This commit is contained in:
2025-11-10 13:30:25 +01:00
parent 2e27395ed0
commit 5341a2e4ba
4 changed files with 454 additions and 325 deletions

View 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;

View 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
View 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;