aktueller stand
This commit is contained in:
@@ -14,11 +14,13 @@ const DEFAULT_SETTINGS = {
|
||||
storeWatchInitialDelayMinSeconds: 10,
|
||||
storeWatchInitialDelayMaxSeconds: 60,
|
||||
storeWatchRequestDelayMs: 1000,
|
||||
storeWatchStatusCacheMaxAgeMinutes: 120,
|
||||
storePickupCheckDelayMs: 400,
|
||||
ignoredSlots: [
|
||||
{
|
||||
storeId: '51450',
|
||||
description: 'TVS'
|
||||
slotName: 'TVS',
|
||||
info: ''
|
||||
}
|
||||
],
|
||||
notifications: {
|
||||
@@ -56,11 +58,15 @@ function sanitizeIgnoredSlots(slots = []) {
|
||||
return DEFAULT_SETTINGS.ignoredSlots;
|
||||
}
|
||||
return slots
|
||||
.map((slot) => ({
|
||||
storeId: slot?.storeId ? String(slot.storeId) : '',
|
||||
description: slot?.description ? String(slot.description) : ''
|
||||
}))
|
||||
.filter((slot) => slot.storeId);
|
||||
.map((slot) => {
|
||||
const slotName = slot?.slotName ?? slot?.description;
|
||||
return {
|
||||
storeId: slot?.storeId ? String(slot.storeId) : '',
|
||||
slotName: slotName ? String(slotName) : '',
|
||||
info: slot?.info ? String(slot.info) : ''
|
||||
};
|
||||
})
|
||||
.filter((slot) => slot.storeId && slot.slotName);
|
||||
}
|
||||
|
||||
function sanitizeString(value) {
|
||||
@@ -120,6 +126,10 @@ function readSettings() {
|
||||
parsed.storeWatchRequestDelayMs,
|
||||
DEFAULT_SETTINGS.storeWatchRequestDelayMs
|
||||
),
|
||||
storeWatchStatusCacheMaxAgeMinutes: sanitizeNumber(
|
||||
parsed.storeWatchStatusCacheMaxAgeMinutes,
|
||||
DEFAULT_SETTINGS.storeWatchStatusCacheMaxAgeMinutes
|
||||
),
|
||||
storePickupCheckDelayMs: sanitizeNumber(
|
||||
parsed.storePickupCheckDelayMs,
|
||||
DEFAULT_SETTINGS.storePickupCheckDelayMs
|
||||
@@ -154,6 +164,10 @@ function writeSettings(patch = {}) {
|
||||
patch.storeWatchRequestDelayMs,
|
||||
current.storeWatchRequestDelayMs
|
||||
),
|
||||
storeWatchStatusCacheMaxAgeMinutes: sanitizeNumber(
|
||||
patch.storeWatchStatusCacheMaxAgeMinutes,
|
||||
current.storeWatchStatusCacheMaxAgeMinutes
|
||||
),
|
||||
storePickupCheckDelayMs: sanitizeNumber(
|
||||
patch.storePickupCheckDelayMs,
|
||||
current.storePickupCheckDelayMs
|
||||
|
||||
@@ -6,7 +6,7 @@ const notificationService = require('./notificationService');
|
||||
const { readConfig, writeConfig } = require('./configStore');
|
||||
const { readStoreWatch, writeStoreWatch } = require('./storeWatchStore');
|
||||
const { readJournal, writeJournal } = require('./journalStore');
|
||||
const { setStoreStatus, persistStoreStatusCache } = require('./storeStatusCache');
|
||||
const { getStoreStatus, setStoreStatus, persistStoreStatusCache } = require('./storeStatusCache');
|
||||
const { sendDormantPickupWarning, sendJournalReminderNotification } = require('./notificationService');
|
||||
const { ensureSession, withSessionRetry } = require('./sessionRefresh');
|
||||
|
||||
@@ -17,6 +17,53 @@ function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES = 120;
|
||||
const storeWatchInFlight = new Map();
|
||||
|
||||
async function fetchSharedStoreStatus(session, storeId, { forceRefresh = false, maxAgeMs } = {}) {
|
||||
if (!storeId) {
|
||||
return { status: null, fetchedAt: null, fromCache: false };
|
||||
}
|
||||
const cacheEntry = getStoreStatus(storeId);
|
||||
const cachedStatus = cacheEntry?.teamSearchStatus;
|
||||
const hasCachedStatus = cachedStatus === 0 || cachedStatus === 1;
|
||||
const cachedAt = Number(cacheEntry?.fetchedAt) || 0;
|
||||
const effectiveMaxAge =
|
||||
Number.isFinite(maxAgeMs) && maxAgeMs >= 0
|
||||
? maxAgeMs
|
||||
: DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES * 60 * 1000;
|
||||
const cacheFresh = hasCachedStatus && Date.now() - cachedAt <= effectiveMaxAge;
|
||||
if (cacheFresh && !forceRefresh) {
|
||||
return { status: cachedStatus, fetchedAt: cachedAt, fromCache: true };
|
||||
}
|
||||
|
||||
const key = String(storeId);
|
||||
if (!forceRefresh && storeWatchInFlight.has(key)) {
|
||||
return storeWatchInFlight.get(key);
|
||||
}
|
||||
|
||||
const fetchPromise = (async () => {
|
||||
const details = await withSessionRetry(
|
||||
session,
|
||||
() => foodsharingClient.fetchStoreDetails(storeId, session.cookieHeader, session),
|
||||
{ label: 'fetchStoreDetails' }
|
||||
);
|
||||
const status = details?.teamSearchStatus === 1 ? 1 : 0;
|
||||
const fetchedAt = Date.now();
|
||||
setStoreStatus(storeId, { teamSearchStatus: status, fetchedAt });
|
||||
return { status, fetchedAt, fromCache: false };
|
||||
})();
|
||||
|
||||
storeWatchInFlight.set(key, fetchPromise);
|
||||
try {
|
||||
return await fetchPromise;
|
||||
} finally {
|
||||
if (storeWatchInFlight.get(key) === fetchPromise) {
|
||||
storeWatchInFlight.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const weekdayMap = {
|
||||
Montag: 'Monday',
|
||||
Dienstag: 'Tuesday',
|
||||
@@ -103,6 +150,9 @@ function resolveSettings(settings) {
|
||||
storeWatchRequestDelayMs: Number.isFinite(settings.storeWatchRequestDelayMs)
|
||||
? settings.storeWatchRequestDelayMs
|
||||
: DEFAULT_SETTINGS.storeWatchRequestDelayMs,
|
||||
storeWatchStatusCacheMaxAgeMinutes: Number.isFinite(settings.storeWatchStatusCacheMaxAgeMinutes)
|
||||
? settings.storeWatchStatusCacheMaxAgeMinutes
|
||||
: DEFAULT_SETTINGS.storeWatchStatusCacheMaxAgeMinutes,
|
||||
ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots,
|
||||
notifications: {
|
||||
ntfy: {
|
||||
@@ -305,10 +355,8 @@ function shouldIgnoreSlot(entry, pickup, settings) {
|
||||
if (String(rule.storeId) !== entry.id) {
|
||||
return false;
|
||||
}
|
||||
if (rule.description) {
|
||||
return pickup.description === rule.description;
|
||||
}
|
||||
return true;
|
||||
const slotName = rule.slotName || rule.description;
|
||||
return slotName ? pickup.description === slotName : true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -469,12 +517,13 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option
|
||||
for (let index = 0; index < watchers.length; index += 1) {
|
||||
const watcher = watchers[index];
|
||||
try {
|
||||
const details = await withSessionRetry(
|
||||
session,
|
||||
() => foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader, session),
|
||||
{ label: 'fetchStoreDetails' }
|
||||
);
|
||||
const status = details?.teamSearchStatus === 1 ? 1 : 0;
|
||||
const cacheMaxAgeMs = Math.max(
|
||||
0,
|
||||
Number(settings?.storeWatchStatusCacheMaxAgeMinutes ?? DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES)
|
||||
) * 60 * 1000;
|
||||
const { status, fromCache } = await fetchSharedStoreStatus(session, watcher.storeId, {
|
||||
maxAgeMs: cacheMaxAgeMs
|
||||
});
|
||||
const checkedAt = Date.now();
|
||||
if (status === 1 && watcher.lastTeamSearchStatus !== 1) {
|
||||
await notificationService.sendStoreWatchNotification({
|
||||
@@ -490,8 +539,9 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option
|
||||
}
|
||||
watcher.lastStatusCheckAt = checkedAt;
|
||||
changed = true;
|
||||
setStoreStatus(watcher.storeId, { teamSearchStatus: status, fetchedAt: checkedAt });
|
||||
statusCacheUpdated = true;
|
||||
if (!fromCache) {
|
||||
statusCacheUpdated = true;
|
||||
}
|
||||
summary.push({
|
||||
storeId: watcher.storeId,
|
||||
storeName: watcher.storeName,
|
||||
|
||||
@@ -784,7 +784,7 @@ function App() {
|
||||
<Router>
|
||||
<>
|
||||
<div className="min-h-screen bg-gray-100 py-6">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<NavigationTabs isAdmin={session?.isAdmin} onProtectedNavigate={requestNavigation} />
|
||||
<Routes>
|
||||
<Route path="/" element={dashboardContent} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const AdminAccessMessage = () => (
|
||||
<div className="p-6 max-w-lg mx-auto bg-white shadow rounded-lg mt-8 text-center">
|
||||
<div className="p-6 max-w-6xl w-full mx-auto bg-white shadow rounded-lg mt-8 text-center">
|
||||
<h1 className="text-2xl font-semibold text-gray-800 mb-2">Kein Zugriff</h1>
|
||||
<p className="text-gray-600 mb-4">Dieser Bereich ist nur für Administratoren verfügbar.</p>
|
||||
<Link
|
||||
|
||||
@@ -49,7 +49,7 @@ const AdminSettingsPanel = ({
|
||||
onSave
|
||||
}) => {
|
||||
return (
|
||||
<div className="p-4 max-w-4xl mx-auto bg-white shadow-lg rounded-lg mt-4">
|
||||
<div className="p-4 max-w-6xl w-full mx-auto bg-white shadow-lg rounded-lg mt-4">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-purple-900">Admin-Einstellungen</h1>
|
||||
@@ -224,6 +224,22 @@ const AdminSettingsPanel = ({
|
||||
placeholder="z. B. 1000"
|
||||
/>
|
||||
</SettingField>
|
||||
|
||||
<SettingField
|
||||
label="Store-Watch Status-Cache (Minuten)"
|
||||
description="Wie lange Ergebnisse pro Store wiederverwendet werden, bevor erneut abgefragt wird."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={adminSettings.storeWatchStatusCacheMaxAgeMinutes}
|
||||
onChange={(event) =>
|
||||
onSettingChange('storeWatchStatusCacheMaxAgeMinutes', event.target.value, true)
|
||||
}
|
||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="z. B. 120"
|
||||
/>
|
||||
</SettingField>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection
|
||||
@@ -233,23 +249,50 @@ const AdminSettingsPanel = ({
|
||||
{(!adminSettings.ignoredSlots || adminSettings.ignoredSlots.length === 0) && (
|
||||
<p className="text-sm text-gray-500">Keine Regeln definiert.</p>
|
||||
)}
|
||||
{adminSettings.ignoredSlots?.length > 0 && (
|
||||
<div className="hidden md:grid md:grid-cols-6 gap-2 text-xs font-semibold text-gray-500 px-1">
|
||||
<span>Store-ID</span>
|
||||
<span className="md:col-span-2">Slotname</span>
|
||||
<span className="md:col-span-2">Info</span>
|
||||
<span>Aktion</span>
|
||||
</div>
|
||||
)}
|
||||
{adminSettings.ignoredSlots?.map((slot, index) => (
|
||||
(() => {
|
||||
const storeIdMissing = !slot.storeId;
|
||||
const slotNameMissing = !slot.slotName;
|
||||
const showErrors = storeIdMissing || slotNameMissing;
|
||||
return (
|
||||
<div
|
||||
key={`${index}-${slot.storeId}`}
|
||||
className="grid grid-cols-1 md:grid-cols-5 gap-2 items-center border border-purple-100 rounded p-3"
|
||||
className={`grid grid-cols-1 md:grid-cols-6 gap-2 items-center rounded p-3 ${
|
||||
showErrors ? 'border border-red-200 bg-red-50/30' : 'border border-purple-100'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={slot.storeId}
|
||||
onChange={(event) => onIgnoredSlotChange(index, 'storeId', event.target.value)}
|
||||
placeholder="Store-ID"
|
||||
className="md:col-span-2 border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
className={`border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 ${
|
||||
storeIdMissing ? 'border-red-400 focus:ring-red-500 focus:border-red-500' : ''
|
||||
}`}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={slot.description}
|
||||
onChange={(event) => onIgnoredSlotChange(index, 'description', event.target.value)}
|
||||
placeholder="Beschreibung (optional)"
|
||||
value={slot.slotName}
|
||||
onChange={(event) => onIgnoredSlotChange(index, 'slotName', event.target.value)}
|
||||
placeholder="Slotname"
|
||||
required
|
||||
className={`md:col-span-2 border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 ${
|
||||
slotNameMissing ? 'border-red-400 focus:ring-red-500 focus:border-red-500' : ''
|
||||
}`}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={slot.info}
|
||||
onChange={(event) => onIgnoredSlotChange(index, 'info', event.target.value)}
|
||||
placeholder="Info (optional)"
|
||||
className="md:col-span-2 border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
<button
|
||||
@@ -259,7 +302,14 @@ const AdminSettingsPanel = ({
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
{showErrors && (
|
||||
<p className="md:col-span-6 text-xs text-red-600">
|
||||
Store-ID und Slotname sind Pflichtfelder.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -335,7 +335,7 @@ const DashboardView = ({
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'skipDormantCheck',
|
||||
header: () => <span>Ruhe-Prüfung</span>,
|
||||
header: () => <span>Inaktivitätswarnung</span>,
|
||||
cell: ({ row }) => (
|
||||
<div
|
||||
className="text-center"
|
||||
@@ -348,9 +348,9 @@ const DashboardView = ({
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-5 w-5"
|
||||
checked={!!row.original.skipDormantCheck}
|
||||
checked={!row.original.skipDormantCheck}
|
||||
onChange={() => onToggleDormantSkip(row.original.id)}
|
||||
title="Warnung bei ausbleibenden Abholungen/Hygiene für diesen Betrieb deaktivieren"
|
||||
title="Warnung bei ausbleibenden Abholungen/Hygiene für diesen Betrieb aktivieren"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
@@ -670,11 +670,11 @@ const DashboardView = ({
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4"
|
||||
checked={!!entry?.skipDormantCheck}
|
||||
checked={!entry?.skipDormantCheck}
|
||||
onChange={() => onToggleDormantSkip(storeId)}
|
||||
title="Warnung bei ausbleibenden Abholungen/Hygiene für diesen Betrieb deaktivieren"
|
||||
title="Warnung bei ausbleibenden Abholungen/Hygiene für diesen Betrieb aktivieren"
|
||||
/>
|
||||
<span>Ruhe-Prüfung</span>
|
||||
<span>Inaktivitätswarnung</span>
|
||||
</label>
|
||||
<button
|
||||
onClick={() => onStoreSelect(store)}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
const NavigationTabs = ({ isAdmin, onProtectedNavigate }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const tabs = [
|
||||
{ to: '/', label: 'Slots buchen' },
|
||||
@@ -28,25 +30,99 @@ const NavigationTabs = ({ isAdmin, onProtectedNavigate }) => {
|
||||
proceed();
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="mb-4 flex gap-2" aria-label="Navigation">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = location.pathname === tab.to;
|
||||
return (
|
||||
<Link
|
||||
key={tab.to}
|
||||
to={tab.to}
|
||||
onClick={(event) => handleClick(event, tab.to)}
|
||||
className={`px-4 py-2 rounded-md border transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-500 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-400'
|
||||
useEffect(() => {
|
||||
setMobileOpen(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
const renderLink = (tab, className, options = {}) => {
|
||||
const isActive = location.pathname === tab.to;
|
||||
const combinedClassName = typeof className === 'function' ? className(isActive) : className;
|
||||
return (
|
||||
<Link
|
||||
key={tab.to}
|
||||
to={tab.to}
|
||||
onClick={(event) => handleClick(event, tab.to)}
|
||||
className={combinedClassName}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
<span className="relative z-10">{tab.label}</span>
|
||||
{options.showUnderline ? (
|
||||
<span
|
||||
className={`absolute inset-x-3 -bottom-1 h-0.5 rounded-full transition-all ${
|
||||
isActive ? 'bg-blue-500 opacity-100' : 'bg-blue-200 opacity-0 group-hover:opacity-60'
|
||||
}`}
|
||||
/>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="mb-4" aria-label="Navigation">
|
||||
<div className="flex items-center justify-between sm:hidden">
|
||||
<span className="text-sm font-semibold text-gray-600">Navigation</span>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-700 shadow-sm"
|
||||
aria-expanded={mobileOpen}
|
||||
aria-controls="mobile-navigation"
|
||||
onClick={() => setMobileOpen((open) => !open)}
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{tab.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{mobileOpen ? (
|
||||
<>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="mobile-navigation"
|
||||
className={`mt-3 flex flex-col gap-2 sm:hidden ${mobileOpen ? '' : 'hidden'}`}
|
||||
>
|
||||
{tabs.map((tab) =>
|
||||
renderLink(
|
||||
tab,
|
||||
(isActive) =>
|
||||
`px-4 py-3 rounded-md border text-left transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-500 text-white border-blue-600'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-400'
|
||||
}`
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden items-center gap-6 rounded-xl border border-gray-200 bg-white/80 px-3 py-2 shadow-sm sm:flex">
|
||||
{tabs.map((tab) =>
|
||||
renderLink(
|
||||
tab,
|
||||
(isActive) =>
|
||||
`group relative rounded-md px-2 pb-2 pt-1 text-sm font-semibold tracking-tight transition-colors ${
|
||||
isActive
|
||||
? 'text-blue-600 bg-blue-50/70'
|
||||
: 'text-gray-600 hover:bg-gray-100/80 hover:text-gray-900'
|
||||
}`,
|
||||
{ showUnderline: true }
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1167,14 +1167,14 @@ const StoreWatchPage = ({
|
||||
|
||||
if (!authorizedFetch) {
|
||||
return (
|
||||
<div className="p-4 max-w-4xl mx-auto">
|
||||
<div className="p-4 max-w-6xl w-full mx-auto">
|
||||
<p className="text-red-600">Keine Session aktiv.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-5xl mx-auto bg-white shadow rounded-lg mt-4">
|
||||
<div className="p-4 max-w-6xl w-full mx-auto bg-white shadow rounded-lg mt-4">
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<h1 className="text-2xl font-bold text-blue-900">Betriebs-Monitoring</h1>
|
||||
<p className="text-gray-600 text-sm">
|
||||
|
||||
@@ -126,7 +126,7 @@ const useAdminSettings = ({ session, authorizedFetch, setStatus, setError }) =>
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
ignoredSlots: [...(prev.ignoredSlots || []), { storeId: '', description: '' }]
|
||||
ignoredSlots: [...(prev.ignoredSlots || []), { storeId: '', slotName: '', info: '' }]
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
@@ -149,6 +149,13 @@ const useAdminSettings = ({ session, authorizedFetch, setStatus, setError }) =>
|
||||
if (!session?.token || !session.isAdmin || !adminSettings) {
|
||||
return;
|
||||
}
|
||||
const invalidIgnoredSlot = (adminSettings.ignoredSlots || []).find(
|
||||
(slot) => !slot?.storeId || !slot?.slotName
|
||||
);
|
||||
if (invalidIgnoredSlot) {
|
||||
setError('Ignorierte Slots: Store-ID und Slotname sind Pflichtfelder.');
|
||||
return;
|
||||
}
|
||||
setStatus('Admin-Einstellungen werden gespeichert...');
|
||||
setError('');
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ export const normalizeAdminSettings = (raw) => {
|
||||
storeWatchInitialDelayMinSeconds: raw.storeWatchInitialDelayMinSeconds ?? '',
|
||||
storeWatchInitialDelayMaxSeconds: raw.storeWatchInitialDelayMaxSeconds ?? '',
|
||||
storeWatchRequestDelayMs: raw.storeWatchRequestDelayMs ?? '',
|
||||
storeWatchStatusCacheMaxAgeMinutes: raw.storeWatchStatusCacheMaxAgeMinutes ?? '',
|
||||
ignoredSlots: Array.isArray(raw.ignoredSlots)
|
||||
? raw.ignoredSlots.map((slot) => ({
|
||||
storeId: slot?.storeId ? String(slot.storeId) : '',
|
||||
description: slot?.description || ''
|
||||
slotName: slot?.slotName || slot?.description || '',
|
||||
info: slot?.info || ''
|
||||
}))
|
||||
: [],
|
||||
notifications: {
|
||||
@@ -59,9 +61,11 @@ export const serializeAdminSettings = (adminSettings) => {
|
||||
storeWatchInitialDelayMinSeconds: toNumberOrUndefined(adminSettings.storeWatchInitialDelayMinSeconds),
|
||||
storeWatchInitialDelayMaxSeconds: toNumberOrUndefined(adminSettings.storeWatchInitialDelayMaxSeconds),
|
||||
storeWatchRequestDelayMs: toNumberOrUndefined(adminSettings.storeWatchRequestDelayMs),
|
||||
storeWatchStatusCacheMaxAgeMinutes: toNumberOrUndefined(adminSettings.storeWatchStatusCacheMaxAgeMinutes),
|
||||
ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({
|
||||
storeId: slot.storeId || '',
|
||||
description: slot.description || ''
|
||||
slotName: slot.slotName || '',
|
||||
info: slot.info || ''
|
||||
})),
|
||||
notifications: {
|
||||
ntfy: {
|
||||
|
||||
Reference in New Issue
Block a user