Files
Pickup-Config/src/components/AdminSettingsPanel.js
2025-11-10 22:51:27 +01:00

372 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { Link } from 'react-router-dom';
const SettingField = ({ label, description, children }) => {
const [showHelp, setShowHelp] = useState(false);
return (
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<label className="block text-sm font-medium text-gray-700">{label}</label>
{description && (
<button
type="button"
onClick={() => setShowHelp((prev) => !prev)}
className="text-xs text-purple-600 hover:text-purple-900 focus:outline-none"
>
{showHelp ? 'Hilfe ausblenden' : 'Hilfe anzeigen'}
</button>
)}
</div>
{children}
{showHelp && description && (
<p className="text-xs text-gray-600 bg-white border border-purple-100 rounded p-2">{description}</p>
)}
</div>
);
};
const SettingSection = ({ title, subtitle, children }) => (
<section className="mb-6">
<div className="mb-3">
<h2 className="text-xl font-semibold text-purple-900">{title}</h2>
{subtitle && <p className="text-sm text-gray-600">{subtitle}</p>}
</div>
<div className="space-y-4 bg-white border border-purple-100 rounded-lg p-4 shadow-sm">{children}</div>
</section>
);
const AdminSettingsPanel = ({
adminSettings,
adminSettingsLoading,
status,
error,
onDismissError,
onSettingChange,
onIgnoredSlotChange,
onAddIgnoredSlot,
onRemoveIgnoredSlot,
onNotificationChange,
onSave
}) => {
return (
<div className="p-4 max-w-4xl 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>
<p className="text-sm text-gray-600">Globale Abläufe und Benachrichtigungen feinjustieren.</p>
</div>
<Link
to="/"
className="inline-flex items-center justify-center px-4 py-2 text-sm font-medium border border-purple-200 rounded-md text-purple-700 hover:bg-purple-50 transition-colors"
>
Zu Slots buchen
</Link>
</div>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 relative">
<span className="block sm:inline">{error}</span>
<button className="absolute top-0 bottom-0 right-0 px-4 py-3" onClick={onDismissError}>
<span className="text-xl">&times;</span>
</button>
</div>
)}
{status && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4 relative">
<span className="block sm:inline">{status}</span>
</div>
)}
<div className="border border-purple-200 rounded-lg p-4 bg-purple-50">
{adminSettingsLoading && <p className="text-sm text-purple-700">Lade Admin-Einstellungen...</p>}
{!adminSettingsLoading && !adminSettings && (
<p className="text-sm text-purple-700">Keine Admin-Einstellungen verfügbar.</p>
)}
{adminSettings && (
<>
<SettingSection
title="Scheduler für Slot-Suche"
subtitle="Bestimmt, wann der Bot freie Slots sucht und wie stark er Anfragen verteilt."
>
<SettingField
label="Cron-Ausdruck"
description="Legt fest, zu welchen Zeiten die reine Slot-Suche läuft. Nutzt die klassische Cron-Syntax (Serverzeit)."
>
<input
type="text"
value={adminSettings.scheduleCron}
onChange={(event) => onSettingChange('scheduleCron', event.target.value)}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="z. B. */10 7-22 * * *"
/>
</SettingField>
<SettingField
label="Initiale Verzögerung (Sek.)"
description="Zufällige Wartezeit beim Start einer Session. Hilft, die Last des ersten Laufs auf mehrere Sekunden zu verteilen."
>
<div className="grid grid-cols-2 gap-2">
<input
type="number"
min="0"
value={adminSettings.initialDelayMinSeconds}
onChange={(event) => onSettingChange('initialDelayMinSeconds', 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="Min"
/>
<input
type="number"
min="0"
value={adminSettings.initialDelayMaxSeconds}
onChange={(event) => onSettingChange('initialDelayMaxSeconds', 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="Max"
/>
</div>
</SettingField>
<SettingField
label="Verzögerung zwischen Jobs (Sek.)"
description="Pause zwischen Cron-Trigger und tatsächlicher Anfrage. Größere Intervalle = mehr Streuung zwischen Sessions."
>
<div className="grid grid-cols-2 gap-2">
<input
type="number"
min="0"
value={adminSettings.randomDelayMinSeconds}
onChange={(event) => onSettingChange('randomDelayMinSeconds', 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="Min"
/>
<input
type="number"
min="0"
value={adminSettings.randomDelayMaxSeconds}
onChange={(event) => onSettingChange('randomDelayMaxSeconds', 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="Max"
/>
</div>
</SettingField>
<SettingField
label="Verzögerung zwischen Store-Abfragen (ms)"
description="Drosselt den Sync der Foodsharing-Stores. Erhöhe den Wert bei Rate-Limit-Fehlern oder wenn der Server langsam reagiert."
>
<input
type="number"
min="0"
value={adminSettings.storePickupCheckDelayMs}
onChange={(event) => onSettingChange('storePickupCheckDelayMs', 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. 3000"
/>
</SettingField>
</SettingSection>
<SettingSection
title="Store-Watch Monitoring"
subtitle="Steuert die Team-Status-Abfragen für geschlossene Betriebe (teamSearchStatus)."
>
<SettingField
label="Store-Watch Cron"
description="Cron-Ausdruck, wann Team-Status-Prüfungen laufen. Bei sehr vielen Stores Werte etwas streuen (z. B. alle 3060 Minuten)."
>
<input
type="text"
value={adminSettings.storeWatchCron || ''}
onChange={(event) => onSettingChange('storeWatchCron', event.target.value)}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="z. B. */30 * * * *"
/>
</SettingField>
<SettingField
label="Store-Watch Startverzögerung (Sek.)"
description="Randomisierte Wartezeit, bevor nach dem Cron-Signal die Statusprüfungen beginnen. Verhindert Traffic-Spitzen."
>
<div className="grid grid-cols-2 gap-2">
<input
type="number"
min="0"
value={adminSettings.storeWatchInitialDelayMinSeconds}
onChange={(event) =>
onSettingChange('storeWatchInitialDelayMinSeconds', 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="Min"
/>
<input
type="number"
min="0"
value={adminSettings.storeWatchInitialDelayMaxSeconds}
onChange={(event) =>
onSettingChange('storeWatchInitialDelayMaxSeconds', 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="Max"
/>
</div>
</SettingField>
<SettingField
label="Verzögerung zwischen Status-Abfragen (ms)"
description="Pausiert zwischen einzelnen Store-Requests, um das Foodsharing-API zu schonen."
>
<input
type="number"
min="0"
value={adminSettings.storeWatchRequestDelayMs}
onChange={(event) => onSettingChange('storeWatchRequestDelayMs', 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. 1000"
/>
</SettingField>
</SettingSection>
<SettingSection
title="Ignorierte Slots"
subtitle="Diese Einträge überspringt der Scheduler dauerhaft (z. B. TVS-Zeiten oder Test-Slots)."
>
{(!adminSettings.ignoredSlots || adminSettings.ignoredSlots.length === 0) && (
<p className="text-sm text-gray-500">Keine Regeln definiert.</p>
)}
{adminSettings.ignoredSlots?.map((slot, index) => (
<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"
>
<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"
/>
<input
type="text"
value={slot.description}
onChange={(event) => onIgnoredSlotChange(index, 'description', event.target.value)}
placeholder="Beschreibung (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
type="button"
onClick={() => onRemoveIgnoredSlot(index)}
className="text-sm text-red-600 hover:text-red-800 focus:outline-none"
>
Entfernen
</button>
</div>
))}
<button
type="button"
onClick={onAddIgnoredSlot}
className="text-sm text-purple-700 hover:text-purple-900 flex items-center gap-1"
>
+ Slot hinzufügen
</button>
</SettingSection>
<SettingSection
title="System-Benachrichtigungen"
subtitle="Aktiviere hier die Kanäle, über die Nutzer später personalisierte Benachrichtigungen empfangen können."
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-lg font-semibold text-gray-800">ntfy</h3>
<p className="text-xs text-gray-500">Push-Nachrichten via ntfy-Server.</p>
</div>
<label className="inline-flex items-center space-x-2 text-sm text-gray-700">
<input
type="checkbox"
checked={adminSettings.notifications?.ntfy?.enabled || false}
onChange={(event) => onNotificationChange('ntfy', 'enabled', event.target.checked)}
className="h-4 w-4 text-purple-600 rounded focus:ring-purple-500"
/>
<span>Aktiv</span>
</label>
</div>
<div className="space-y-3">
<input
type="text"
value={adminSettings.notifications?.ntfy?.serverUrl || ''}
onChange={(event) => onNotificationChange('ntfy', 'serverUrl', event.target.value)}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Server-URL"
/>
<input
type="text"
value={adminSettings.notifications?.ntfy?.topicPrefix || ''}
onChange={(event) => onNotificationChange('ntfy', 'topicPrefix', event.target.value)}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Themenpräfix"
/>
<input
type="text"
value={adminSettings.notifications?.ntfy?.username || ''}
onChange={(event) => onNotificationChange('ntfy', 'username', event.target.value)}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Benutzername (optional)"
/>
<input
type="password"
value={adminSettings.notifications?.ntfy?.password || ''}
onChange={(event) => onNotificationChange('ntfy', 'password', event.target.value)}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Passwort (optional)"
/>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-lg font-semibold text-gray-800">Telegram</h3>
<p className="text-xs text-gray-500">Bot-Einstellungen für Telegram-Nachrichten.</p>
</div>
<label className="inline-flex items-center space-x-2 text-sm text-gray-700">
<input
type="checkbox"
checked={adminSettings.notifications?.telegram?.enabled || false}
onChange={(event) => onNotificationChange('telegram', 'enabled', event.target.checked)}
className="h-4 w-4 text-purple-600 rounded focus:ring-purple-500"
/>
<span>Aktiv</span>
</label>
</div>
<div className="space-y-3">
<input
type="text"
value={adminSettings.notifications?.telegram?.botToken || ''}
onChange={(event) => onNotificationChange('telegram', 'botToken', event.target.value)}
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Bot-Token"
/>
</div>
</div>
</div>
</SettingSection>
<div className="flex justify-end mt-4">
<button
type="button"
onClick={onSave}
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded focus:outline-none focus:ring-2 focus:ring-purple-500"
>
Admin-Einstellungen speichern
</button>
</div>
</>
)}
</div>
</div>
);
};
export default AdminSettingsPanel;