refactoring

This commit is contained in:
2025-11-10 09:57:48 +01:00
parent 083c79d82e
commit 248b0bcba4
5 changed files with 317 additions and 19533 deletions

19242
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,10 +13,11 @@
"date-fns": "^2.30.0",
"express": "^4.18.2",
"node-cron": "^3.0.3",
"react": "^19.1.0",
"react": "18.2.0",
"react-date-range": "^1.4.0",
"react-dom": "^19.1.0",
"react-dom": "18.2.0",
"react-router-dom": "^7.9.5",
"react-scripts": "5.0.1",
"uuid": "^11.0.3",
"web-vitals": "^2.1.4"
},
@@ -43,8 +44,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"react-scripts": "^5.0.1"
}
}

View File

@@ -1,71 +1,15 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, Link, useLocation, useNavigate } from 'react-router-dom';
import { DateRange } from 'react-date-range';
import { format, parseISO, isValid, startOfDay } from 'date-fns';
import { de } from 'date-fns/locale';
import { startOfDay } from 'date-fns';
import './App.css';
import 'react-date-range/dist/styles.css';
import 'react-date-range/dist/theme/default.css';
import { buildSelectionRange, formatDateValue, formatRangeLabel } from './utils/dateUtils';
import useSyncProgress from './hooks/useSyncProgress';
import useNotificationSettings from './hooks/useNotificationSettings';
const TOKEN_STORAGE_KEY = 'pickupConfigToken';
const parseDateValue = (value) => {
if (!value) {
return null;
}
const parsed = parseISO(value);
return isValid(parsed) ? parsed : null;
};
const formatDateValue = (date) => {
if (!(date instanceof Date) || !isValid(date)) {
return null;
}
return format(date, 'yyyy-MM-dd');
};
const formatRangeLabel = (start, end) => {
const startDate = parseDateValue(start);
const endDate = parseDateValue(end);
if (startDate && endDate) {
const startLabel = format(startDate, 'dd.MM.yyyy', { locale: de });
const endLabel = format(endDate, 'dd.MM.yyyy', { locale: de });
if (startLabel === endLabel) {
return startLabel;
}
return `${startLabel} ${endLabel}`;
}
if (startDate) {
return format(startDate, 'dd.MM.yyyy', { locale: de });
}
return 'Zeitraum auswählen';
};
const buildSelectionRange = (start, end, minDate) => {
const minimum = minDate || startOfDay(new Date());
let startDate = parseDateValue(start) || parseDateValue(end) || minimum;
let endDate = parseDateValue(end) || parseDateValue(start) || startDate;
if (startDate < minimum) {
startDate = minimum;
}
if (endDate < minimum) {
endDate = startDate;
}
return {
startDate,
endDate,
key: 'selection'
};
};
const defaultNotificationSettings = {
ntfy: { enabled: false, topic: '', serverUrl: '' },
telegram: { enabled: false, chatId: '' }
};
const defaultNotificationCapabilities = {
ntfy: { enabled: false, serverUrl: '', topicPrefix: '' },
telegram: { enabled: false }
};
function App() {
const [session, setSession] = useState(null);
@@ -78,13 +22,6 @@ function App() {
const [availableCollapsed, setAvailableCollapsed] = useState(true);
const [adminSettings, setAdminSettings] = useState(null);
const [adminSettingsLoading, setAdminSettingsLoading] = useState(false);
const [syncProgress, setSyncProgress] = useState({
active: false,
percent: 0,
message: '',
block: false,
etaSeconds: null
});
const [initializing, setInitializing] = useState(false);
const [isDirty, setIsDirty] = useState(false);
const [dirtyDialogOpen, setDirtyDialogOpen] = useState(false);
@@ -93,15 +30,7 @@ function App() {
const [dirtyDialogSaving, setDirtyDialogSaving] = useState(false);
const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null });
const [activeRangePicker, setActiveRangePicker] = useState(null);
const [notificationSettings, setNotificationSettings] = useState(defaultNotificationSettings);
const [notificationCapabilities, setNotificationCapabilities] = useState(defaultNotificationCapabilities);
const [notificationDirty, setNotificationDirty] = useState(false);
const [notificationLoading, setNotificationLoading] = useState(false);
const [notificationSaving, setNotificationSaving] = useState(false);
const [notificationMessage, setNotificationMessage] = useState('');
const [notificationError, setNotificationError] = useState('');
const [notificationPanelOpen, setNotificationPanelOpen] = useState(false);
const [copyFeedback, setCopyFeedback] = useState('');
const minSelectableDate = useMemo(() => startOfDay(new Date()), []);
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
@@ -143,57 +72,13 @@ function App() {
}, []);
const delay = useCallback((ms) => new Promise((resolve) => setTimeout(resolve, ms)), []);
const startSyncProgress = useCallback((message, percent, block = false) => {
setSyncProgress({ active: true, percent, message, block, etaSeconds: null });
}, []);
const updateSyncProgress = useCallback((message, percent, extra = {}) => {
setSyncProgress((prev) => {
if (!prev.active) {
return prev;
}
let nextPercent = prev.percent ?? 0;
if (typeof percent === 'number' && Number.isFinite(percent)) {
const bounded = Math.min(100, Math.max(percent, 0));
nextPercent = Math.max(bounded, nextPercent);
}
return {
...prev,
message: message ?? prev.message,
percent: nextPercent,
etaSeconds:
Object.prototype.hasOwnProperty.call(extra, 'etaSeconds') && extra.etaSeconds !== undefined
? extra.etaSeconds
: prev.etaSeconds ?? null
};
});
}, []);
const finishSyncProgress = useCallback(() => {
setSyncProgress((prev) => {
if (!prev.active) {
return prev;
}
return { ...prev, percent: 100, etaSeconds: null };
});
setTimeout(() => {
setSyncProgress({ active: false, percent: 0, message: '', block: false, etaSeconds: null });
}, 400);
}, []);
const nudgeSyncProgress = useCallback((message, increment = 2, ceiling = 80) => {
setSyncProgress((prev) => {
if (!prev.active) {
return prev;
}
const nextPercent = Math.min(ceiling, (prev.percent || 0) + increment);
return {
...prev,
percent: nextPercent,
message: message || prev.message
};
});
}, []);
const {
syncProgress,
startSyncProgress,
updateSyncProgress,
finishSyncProgress,
nudgeSyncProgress
} = useSyncProgress();
const normalizeAdminSettings = useCallback((raw) => {
if (!raw) {
@@ -238,13 +123,6 @@ function App() {
setAdminSettingsLoading(false);
setAvailableCollapsed(true);
setInitializing(false);
setNotificationSettings(defaultNotificationSettings);
setNotificationCapabilities(defaultNotificationCapabilities);
setNotificationDirty(false);
setNotificationError('');
setNotificationMessage('');
setNotificationLoading(false);
setNotificationSaving(false);
}, []);
const handleUnauthorized = useCallback(() => {
@@ -329,46 +207,24 @@ function App() {
[handleUnauthorized, session?.token]
);
const loadNotificationSettings = useCallback(async () => {
if (!session?.token) {
return;
}
setNotificationLoading(true);
setNotificationError('');
try {
const response = await authorizedFetch('/api/notifications/settings');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setNotificationSettings({
ntfy: {
enabled: Boolean(data?.settings?.ntfy?.enabled),
topic: data?.settings?.ntfy?.topic || '',
serverUrl: data?.settings?.ntfy?.serverUrl || ''
},
telegram: {
enabled: Boolean(data?.settings?.telegram?.enabled),
chatId: data?.settings?.telegram?.chatId || ''
}
const {
notificationSettings,
notificationCapabilities,
notificationDirty,
notificationLoading,
notificationSaving,
notificationMessage,
notificationError,
copyFeedback,
loadNotificationSettings,
handleNotificationFieldChange,
saveNotificationSettings,
sendNotificationTest,
copyToClipboard
} = useNotificationSettings({
authorizedFetch,
sessionToken: session?.token
});
setNotificationCapabilities({
ntfy: {
enabled: Boolean(data?.capabilities?.ntfy?.enabled),
serverUrl: data?.capabilities?.ntfy?.serverUrl || '',
topicPrefix: data?.capabilities?.ntfy?.topicPrefix || ''
},
telegram: {
enabled: Boolean(data?.capabilities?.telegram?.enabled)
}
});
setNotificationDirty(false);
} catch (err) {
setNotificationError(`Benachrichtigungseinstellungen konnten nicht geladen werden: ${err.message}`);
} finally {
setNotificationLoading(false);
}
}, [authorizedFetch, session?.token]);
useEffect(() => {
if (!session?.token || !session.isAdmin) {
@@ -529,13 +385,6 @@ function App() {
}
}, [session?.token, authorizedFetch]);
useEffect(() => {
if (!session?.token) {
return;
}
loadNotificationSettings();
}, [session?.token, loadNotificationSettings]);
const syncStoresWithProgress = useCallback(
async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false, tokenOverride } = {}) => {
const effectiveToken = tokenOverride || session?.token;
@@ -1072,111 +921,6 @@ function App() {
);
};
const handleNotificationFieldChange = (channel, field, value) => {
if (channel === 'ntfy' && field === 'serverUrl') {
return;
}
setNotificationSettings((prev) => {
const nextChannel = {
...prev[channel],
[field]: value
};
return {
...prev,
[channel]: nextChannel
};
});
setNotificationDirty(true);
};
const saveNotificationSettings = async () => {
if (!session?.token) {
return;
}
setNotificationSaving(true);
setNotificationError('');
setNotificationMessage('');
try {
const response = await authorizedFetch('/api/notifications/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notifications: notificationSettings })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setNotificationSettings({
ntfy: {
enabled: Boolean(data?.ntfy?.enabled),
topic: data?.ntfy?.topic || notificationSettings.ntfy.topic,
serverUrl: data?.ntfy?.serverUrl || notificationSettings.ntfy.serverUrl
},
telegram: {
enabled: Boolean(data?.telegram?.enabled),
chatId: data?.telegram?.chatId || notificationSettings.telegram.chatId
}
});
setNotificationDirty(false);
setNotificationMessage('Benachrichtigungseinstellungen gespeichert.');
setTimeout(() => setNotificationMessage(''), 4000);
} catch (err) {
setNotificationError(`Speichern der Benachrichtigungen fehlgeschlagen: ${err.message}`);
} finally {
setNotificationSaving(false);
}
};
const sendNotificationTest = async (channel) => {
if (!session?.token) {
return;
}
setNotificationError('');
setNotificationMessage('');
try {
const response = await authorizedFetch('/api/notifications/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
await response.json();
setNotificationMessage('Testbenachrichtigung gesendet.');
setTimeout(() => setNotificationMessage(''), 4000);
} catch (err) {
setNotificationError(`Testbenachrichtigung fehlgeschlagen: ${err.message}`);
}
};
const copyToClipboard = async (text) => {
if (!text) {
return;
}
setCopyFeedback('');
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const tempInput = document.createElement('textarea');
tempInput.value = text;
tempInput.style.position = 'fixed';
tempInput.style.top = '-9999px';
document.body.appendChild(tempInput);
tempInput.focus();
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
}
setCopyFeedback('Link kopiert!');
setTimeout(() => setCopyFeedback(''), 2000);
} catch (err) {
setCopyFeedback(`Kopieren fehlgeschlagen: ${err.message}`);
setTimeout(() => setCopyFeedback(''), 3000);
}
};
const handleAdminSettingChange = (field, value, isNumber = false) => {
setAdminSettings((prev) => {
if (!prev) {

View File

@@ -0,0 +1,209 @@
import { useCallback, useEffect, useState } from 'react';
const createDefaultSettings = () => ({
ntfy: { enabled: false, topic: '', serverUrl: '' },
telegram: { enabled: false, chatId: '' }
});
const createDefaultCapabilities = () => ({
ntfy: { enabled: false, serverUrl: '', topicPrefix: '' },
telegram: { enabled: false }
});
const useNotificationSettings = ({ authorizedFetch, sessionToken }) => {
const [settings, setSettings] = useState(createDefaultSettings);
const [capabilities, setCapabilities] = useState(createDefaultCapabilities);
const [dirty, setDirty] = useState(false);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const [copyFeedback, setCopyFeedback] = useState('');
const resetState = useCallback(() => {
setSettings(createDefaultSettings());
setCapabilities(createDefaultCapabilities());
setDirty(false);
setLoading(false);
setSaving(false);
setMessage('');
setError('');
setCopyFeedback('');
}, []);
const loadNotificationSettings = useCallback(async () => {
if (!sessionToken) {
return;
}
setLoading(true);
setError('');
try {
const response = await authorizedFetch('/api/notifications/settings');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setSettings({
ntfy: {
enabled: Boolean(data?.settings?.ntfy?.enabled),
topic: data?.settings?.ntfy?.topic || '',
serverUrl: data?.settings?.ntfy?.serverUrl || ''
},
telegram: {
enabled: Boolean(data?.settings?.telegram?.enabled),
chatId: data?.settings?.telegram?.chatId || ''
}
});
setCapabilities({
ntfy: {
enabled: Boolean(data?.capabilities?.ntfy?.enabled),
serverUrl: data?.capabilities?.ntfy?.serverUrl || '',
topicPrefix: data?.capabilities?.ntfy?.topicPrefix || ''
},
telegram: {
enabled: Boolean(data?.capabilities?.telegram?.enabled)
}
});
setDirty(false);
} catch (err) {
setError(`Benachrichtigungseinstellungen konnten nicht geladen werden: ${err.message}`);
} finally {
setLoading(false);
}
}, [authorizedFetch, sessionToken]);
const handleFieldChange = useCallback((channel, field, value) => {
if (channel === 'ntfy' && field === 'serverUrl') {
return;
}
setSettings((prev) => {
const nextChannel = {
...prev[channel],
[field]: value
};
return {
...prev,
[channel]: nextChannel
};
});
setDirty(true);
}, []);
const saveNotificationSettings = useCallback(async () => {
if (!sessionToken) {
return;
}
setSaving(true);
setError('');
setMessage('');
try {
const response = await authorizedFetch('/api/notifications/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notifications: settings })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setSettings({
ntfy: {
enabled: Boolean(data?.ntfy?.enabled),
topic: data?.ntfy?.topic || settings.ntfy.topic,
serverUrl: data?.ntfy?.serverUrl || settings.ntfy.serverUrl
},
telegram: {
enabled: Boolean(data?.telegram?.enabled),
chatId: data?.telegram?.chatId || settings.telegram.chatId
}
});
setDirty(false);
setMessage('Benachrichtigungseinstellungen gespeichert.');
setTimeout(() => setMessage(''), 4000);
} catch (err) {
setError(`Speichern der Benachrichtigungen fehlgeschlagen: ${err.message}`);
} finally {
setSaving(false);
}
}, [authorizedFetch, sessionToken, settings]);
const sendNotificationTest = useCallback(
async (channel) => {
if (!sessionToken) {
return;
}
setError('');
setMessage('');
try {
const response = await authorizedFetch('/api/notifications/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
await response.json();
setMessage('Testbenachrichtigung gesendet.');
setTimeout(() => setMessage(''), 4000);
} catch (err) {
setError(`Testbenachrichtigung fehlgeschlagen: ${err.message}`);
}
},
[authorizedFetch, sessionToken]
);
const copyToClipboard = useCallback(async (text) => {
if (!text) {
return;
}
setCopyFeedback('');
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const tempInput = document.createElement('textarea');
tempInput.value = text;
tempInput.style.position = 'fixed';
tempInput.style.top = '-9999px';
document.body.appendChild(tempInput);
tempInput.focus();
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
}
setCopyFeedback('Link kopiert!');
setTimeout(() => setCopyFeedback(''), 2000);
} catch (err) {
setCopyFeedback(`Kopieren fehlgeschlagen: ${err.message}`);
setTimeout(() => setCopyFeedback(''), 3000);
}
}, []);
useEffect(() => {
if (!sessionToken) {
resetState();
return;
}
loadNotificationSettings();
}, [sessionToken, loadNotificationSettings, resetState]);
return {
notificationSettings: settings,
notificationCapabilities: capabilities,
notificationDirty: dirty,
notificationLoading: loading,
notificationSaving: saving,
notificationMessage: message,
notificationError: error,
copyFeedback,
loadNotificationSettings,
handleNotificationFieldChange: handleFieldChange,
saveNotificationSettings,
sendNotificationTest,
copyToClipboard,
resetNotificationState: resetState
};
};
export default useNotificationSettings;

View File

@@ -0,0 +1,75 @@
import { useCallback, useState } from 'react';
const initialState = {
active: false,
percent: 0,
message: '',
block: false,
etaSeconds: null
};
const useSyncProgress = () => {
const [syncProgress, setSyncProgress] = useState(initialState);
const startSyncProgress = useCallback((message, percent = 0, block = false) => {
setSyncProgress({ active: true, percent, message, block, etaSeconds: null });
}, []);
const updateSyncProgress = useCallback((message, percent, extra = {}) => {
setSyncProgress((prev) => {
if (!prev.active) {
return prev;
}
let nextPercent = prev.percent ?? 0;
if (typeof percent === 'number' && Number.isFinite(percent)) {
const bounded = Math.min(100, Math.max(percent, 0));
nextPercent = Math.max(bounded, nextPercent);
}
return {
...prev,
message: message ?? prev.message,
percent: nextPercent,
etaSeconds:
Object.prototype.hasOwnProperty.call(extra, 'etaSeconds') && extra.etaSeconds !== undefined
? extra.etaSeconds
: prev.etaSeconds ?? null
};
});
}, []);
const finishSyncProgress = useCallback(() => {
setSyncProgress((prev) => {
if (!prev.active) {
return prev;
}
return { ...prev, percent: 100, etaSeconds: null };
});
setTimeout(() => {
setSyncProgress(initialState);
}, 400);
}, []);
const nudgeSyncProgress = useCallback((message, increment = 2, ceiling = 80) => {
setSyncProgress((prev) => {
if (!prev.active) {
return prev;
}
const nextPercent = Math.min(ceiling, (prev.percent || 0) + increment);
return {
...prev,
percent: nextPercent,
message: message || prev.message
};
});
}, []);
return {
syncProgress,
startSyncProgress,
updateSyncProgress,
finishSyncProgress,
nudgeSyncProgress
};
};
export default useSyncProgress;