refactoring
This commit is contained in:
19242
package-lock.json
generated
19242
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,10 +13,11 @@
|
|||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"react": "^19.1.0",
|
"react": "18.2.0",
|
||||||
"react-date-range": "^1.4.0",
|
"react-date-range": "^1.4.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "18.2.0",
|
||||||
"react-router-dom": "^7.9.5",
|
"react-router-dom": "^7.9.5",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
@@ -43,8 +44,5 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"react-scripts": "^5.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
316
src/App.js
316
src/App.js
@@ -1,71 +1,15 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate, Link, useLocation, useNavigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate, Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { DateRange } from 'react-date-range';
|
import { DateRange } from 'react-date-range';
|
||||||
import { format, parseISO, isValid, startOfDay } from 'date-fns';
|
import { startOfDay } from 'date-fns';
|
||||||
import { de } from 'date-fns/locale';
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import 'react-date-range/dist/styles.css';
|
import 'react-date-range/dist/styles.css';
|
||||||
import 'react-date-range/dist/theme/default.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 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() {
|
function App() {
|
||||||
const [session, setSession] = useState(null);
|
const [session, setSession] = useState(null);
|
||||||
@@ -78,13 +22,6 @@ function App() {
|
|||||||
const [availableCollapsed, setAvailableCollapsed] = useState(true);
|
const [availableCollapsed, setAvailableCollapsed] = useState(true);
|
||||||
const [adminSettings, setAdminSettings] = useState(null);
|
const [adminSettings, setAdminSettings] = useState(null);
|
||||||
const [adminSettingsLoading, setAdminSettingsLoading] = useState(false);
|
const [adminSettingsLoading, setAdminSettingsLoading] = useState(false);
|
||||||
const [syncProgress, setSyncProgress] = useState({
|
|
||||||
active: false,
|
|
||||||
percent: 0,
|
|
||||||
message: '',
|
|
||||||
block: false,
|
|
||||||
etaSeconds: null
|
|
||||||
});
|
|
||||||
const [initializing, setInitializing] = useState(false);
|
const [initializing, setInitializing] = useState(false);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
const [dirtyDialogOpen, setDirtyDialogOpen] = useState(false);
|
const [dirtyDialogOpen, setDirtyDialogOpen] = useState(false);
|
||||||
@@ -93,15 +30,7 @@ function App() {
|
|||||||
const [dirtyDialogSaving, setDirtyDialogSaving] = useState(false);
|
const [dirtyDialogSaving, setDirtyDialogSaving] = useState(false);
|
||||||
const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null });
|
const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null });
|
||||||
const [activeRangePicker, setActiveRangePicker] = useState(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 [notificationPanelOpen, setNotificationPanelOpen] = useState(false);
|
||||||
const [copyFeedback, setCopyFeedback] = useState('');
|
|
||||||
const minSelectableDate = useMemo(() => startOfDay(new Date()), []);
|
const minSelectableDate = useMemo(() => startOfDay(new Date()), []);
|
||||||
|
|
||||||
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
|
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 delay = useCallback((ms) => new Promise((resolve) => setTimeout(resolve, ms)), []);
|
||||||
|
|
||||||
const startSyncProgress = useCallback((message, percent, block = false) => {
|
const {
|
||||||
setSyncProgress({ active: true, percent, message, block, etaSeconds: null });
|
syncProgress,
|
||||||
}, []);
|
startSyncProgress,
|
||||||
|
updateSyncProgress,
|
||||||
const updateSyncProgress = useCallback((message, percent, extra = {}) => {
|
finishSyncProgress,
|
||||||
setSyncProgress((prev) => {
|
nudgeSyncProgress
|
||||||
if (!prev.active) {
|
} = useSyncProgress();
|
||||||
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 normalizeAdminSettings = useCallback((raw) => {
|
const normalizeAdminSettings = useCallback((raw) => {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
@@ -238,13 +123,6 @@ function App() {
|
|||||||
setAdminSettingsLoading(false);
|
setAdminSettingsLoading(false);
|
||||||
setAvailableCollapsed(true);
|
setAvailableCollapsed(true);
|
||||||
setInitializing(false);
|
setInitializing(false);
|
||||||
setNotificationSettings(defaultNotificationSettings);
|
|
||||||
setNotificationCapabilities(defaultNotificationCapabilities);
|
|
||||||
setNotificationDirty(false);
|
|
||||||
setNotificationError('');
|
|
||||||
setNotificationMessage('');
|
|
||||||
setNotificationLoading(false);
|
|
||||||
setNotificationSaving(false);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUnauthorized = useCallback(() => {
|
const handleUnauthorized = useCallback(() => {
|
||||||
@@ -329,46 +207,24 @@ function App() {
|
|||||||
[handleUnauthorized, session?.token]
|
[handleUnauthorized, session?.token]
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadNotificationSettings = useCallback(async () => {
|
const {
|
||||||
if (!session?.token) {
|
notificationSettings,
|
||||||
return;
|
notificationCapabilities,
|
||||||
}
|
notificationDirty,
|
||||||
setNotificationLoading(true);
|
notificationLoading,
|
||||||
setNotificationError('');
|
notificationSaving,
|
||||||
try {
|
notificationMessage,
|
||||||
const response = await authorizedFetch('/api/notifications/settings');
|
notificationError,
|
||||||
if (!response.ok) {
|
copyFeedback,
|
||||||
throw new Error(`HTTP ${response.status}`);
|
loadNotificationSettings,
|
||||||
}
|
handleNotificationFieldChange,
|
||||||
const data = await response.json();
|
saveNotificationSettings,
|
||||||
setNotificationSettings({
|
sendNotificationTest,
|
||||||
ntfy: {
|
copyToClipboard
|
||||||
enabled: Boolean(data?.settings?.ntfy?.enabled),
|
} = useNotificationSettings({
|
||||||
topic: data?.settings?.ntfy?.topic || '',
|
authorizedFetch,
|
||||||
serverUrl: data?.settings?.ntfy?.serverUrl || ''
|
sessionToken: session?.token
|
||||||
},
|
});
|
||||||
telegram: {
|
|
||||||
enabled: Boolean(data?.settings?.telegram?.enabled),
|
|
||||||
chatId: data?.settings?.telegram?.chatId || ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (!session?.token || !session.isAdmin) {
|
if (!session?.token || !session.isAdmin) {
|
||||||
@@ -529,13 +385,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [session?.token, authorizedFetch]);
|
}, [session?.token, authorizedFetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!session?.token) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadNotificationSettings();
|
|
||||||
}, [session?.token, loadNotificationSettings]);
|
|
||||||
|
|
||||||
const syncStoresWithProgress = useCallback(
|
const syncStoresWithProgress = useCallback(
|
||||||
async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false, tokenOverride } = {}) => {
|
async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false, tokenOverride } = {}) => {
|
||||||
const effectiveToken = tokenOverride || session?.token;
|
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) => {
|
const handleAdminSettingChange = (field, value, isNumber = false) => {
|
||||||
setAdminSettings((prev) => {
|
setAdminSettings((prev) => {
|
||||||
if (!prev) {
|
if (!prev) {
|
||||||
@@ -2407,4 +2151,4 @@ function StoreSyncOverlay({ state }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
209
src/hooks/useNotificationSettings.js
Normal file
209
src/hooks/useNotificationSettings.js
Normal 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;
|
||||||
75
src/hooks/useSyncProgress.js
Normal file
75
src/hooks/useSyncProgress.js
Normal 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;
|
||||||
Reference in New Issue
Block a user