From dff35b8218c9d666930c7b1d6c399343a09d1085 Mon Sep 17 00:00:00 2001 From: Meik Date: Mon, 10 Nov 2025 10:39:56 +0100 Subject: [PATCH] refactoring node 24 --- src/App.js | 1107 ++--------------------- src/components/AdminAccessMessage.js | 16 + src/components/AdminSettingsPanel.js | 253 ++++++ src/components/ConfirmationDialog.js | 44 + src/components/DashboardView.js | 299 ++++++ src/components/DirtyNavigationDialog.js | 39 + src/components/LoginView.js | 73 ++ src/components/NavigationTabs.js | 49 + src/components/NotificationPanel.js | 196 ++++ src/components/StoreSyncOverlay.js | 45 + 10 files changed, 1102 insertions(+), 1019 deletions(-) create mode 100644 src/components/AdminAccessMessage.js create mode 100644 src/components/AdminSettingsPanel.js create mode 100644 src/components/ConfirmationDialog.js create mode 100644 src/components/DashboardView.js create mode 100644 src/components/DirtyNavigationDialog.js create mode 100644 src/components/LoginView.js create mode 100644 src/components/NavigationTabs.js create mode 100644 src/components/NotificationPanel.js create mode 100644 src/components/StoreSyncOverlay.js diff --git a/src/App.js b/src/App.js index 04504fd..f705751 100644 --- a/src/App.js +++ b/src/App.js @@ -1,14 +1,21 @@ 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 { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { startOfDay } from 'date-fns'; -import { de } from 'date-fns/locale'; 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 { formatDateValue, formatRangeLabel } from './utils/dateUtils'; import useSyncProgress from './hooks/useSyncProgress'; import useNotificationSettings from './hooks/useNotificationSettings'; +import NavigationTabs from './components/NavigationTabs'; +import LoginView from './components/LoginView'; +import DashboardView from './components/DashboardView'; +import AdminSettingsPanel from './components/AdminSettingsPanel'; +import AdminAccessMessage from './components/AdminAccessMessage'; +import DirtyNavigationDialog from './components/DirtyNavigationDialog'; +import ConfirmationDialog from './components/ConfirmationDialog'; +import StoreSyncOverlay from './components/StoreSyncOverlay'; +import RangePickerModal from './components/RangePickerModal'; const TOKEN_STORAGE_KEY = 'pickupConfigToken'; @@ -1059,65 +1066,14 @@ function App() { if (!session?.token) { return ( <> -
-
{ - if (initializing) { - e.preventDefault(); - return; - } - handleLogin(e); - }} - > -
-

Pickup Config Login

-

- Die Anwendung authentifiziert sich direkt bei foodsharing.de. Bitte verwende deine persönlichen Login-Daten. -

-
- {error && ( -
- {error} -
- )} -
- - setCredentials({ ...credentials, email: e.target.value })} - className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - required - /> -
-
- - setCredentials({ ...credentials, password: e.target.value })} - className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - required - /> -
- -
-
+ ); @@ -1152,796 +1108,64 @@ function App() { } const dashboardContent = ( -
-

Foodsharing Pickup Manager

-
-
-
-

Angemeldet

-

{session.profile.name}

-

Profil-ID: {session.profile.id}

-
-
- - - - {notificationLoading && Lade…} -
-
-
- - {notificationPanelOpen && ( -
- {notificationError && ( -
- {notificationError} -
- )} - {notificationMessage && ( -
- {notificationMessage} -
- )} -
-
-
-
-

ntfy

-

- Push aufs Handy über die ntfy-App oder Browser (Themenkanal notwendig). -

-
- -
- {!notificationCapabilities.ntfy.enabled ? ( -

- Diese Option wurde vom Admin deaktiviert. Bitte frage nach, wenn du ntfy nutzen möchtest. -

- ) : ( -
-
- - handleNotificationFieldChange('ntfy', 'topic', e.target.value)} - placeholder="z. B. mein-pickup-topic" - className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - disabled={!notificationSettings.ntfy.enabled} - /> -
- {notificationCapabilities.ntfy.serverUrl && ( -

- Server: {notificationCapabilities.ntfy.serverUrl} (vom Admin festgelegt) -

- )} - {notificationCapabilities.ntfy.topicPrefix && ( -

- Präfix: {notificationCapabilities.ntfy.topicPrefix} (Bindestrich wird automatisch ergänzt) -

- )} - {ntfyPreviewUrl && ( -
- - {ntfyPreviewUrl} - - -
- )} - {copyFeedback && ( -

{copyFeedback}

- )} - -
- )} -
- -
-
-
-

Telegram

-

- Nutze den Bot des Admins, um Nachrichten direkt in Telegram zu erhalten. -

-
- -
- {!notificationCapabilities.telegram.enabled ? ( -

- Telegram-Benachrichtigungen sind derzeit deaktiviert oder der Bot ist nicht konfiguriert. -

- ) : ( -
-
- - handleNotificationFieldChange('telegram', 'chatId', e.target.value)} - placeholder="z. B. 123456789" - className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - disabled={!notificationSettings.telegram.enabled} - /> -

- Tipp: Schreibe dem Telegram-Bot und nutze{' '} - - @userinfobot - {' '} - oder ein Chat-ID-Tool. -

-
- -
- )} -
-
-
- - -
-
- )} - -
- - {!availableCollapsed && ( -
- {stores.length === 0 && ( -
Noch keine Betriebe geladen. Aktualisiere nach dem Login.
- )} - {stores.map((store) => { - const storeId = String(store.id); - const entry = configMap.get(storeId); - const isVisible = entry && !entry.hidden; - const needsRestore = entry && entry.hidden; - const blockedByNoPickups = store.hasPickupSlots === false; - let statusLabel = 'Hinzufügen'; - let statusClass = 'text-blue-600'; - if (isVisible) { - statusLabel = 'Bereits in Konfiguration'; - statusClass = 'text-gray-500'; - } else if (needsRestore) { - statusLabel = 'Ausgeblendet – erneut hinzufügen'; - statusClass = 'text-amber-600'; - } - if (blockedByNoPickups) { - statusLabel = 'Keine Pickups – automatisch verborgen'; - statusClass = 'text-red-600'; - } - return ( - - ); - })} -
- )} -
- -
- { - if (!isDirty) { - return; - } - event.preventDefault(); - requestNavigation('das Foodsharing-Dashboard zu öffnen', () => - window.open('https://foodsharing.de/?page=dashboard', '_blank', 'noopener,noreferrer') - ); - }} - > - - - - Foodsharing Dashboard - -
- - {error && ( -
- {error} - -
- )} - - {status && ( -
- {status} -
- )} - -
- - - - - - - - - - - - - - {visibleConfig.length === 0 && ( - - - - )} - {visibleConfig.map((item, index) => { - const normalizedRange = item.desiredDateRange - ? { ...item.desiredDateRange } - : item.desiredDate - ? { start: item.desiredDate, end: item.desiredDate } - : null; - const rangeStart = normalizedRange?.start || ''; - const rangeEnd = normalizedRange?.end || ''; - const hasDateRange = Boolean(rangeStart || rangeEnd); - return ( - - - - - - - - - - ); - })} - -
AktivGeschäftProfil prüfenNur benachrichtigenWochentagZeitraum (Datum oder Range)Aktionen
- Keine sichtbaren Einträge. Nutze „Verfügbare Betriebe“, um Betriebe hinzuzufügen oder ausgeblendete Einträge zurückzuholen. -
- handleToggleActive(item.id)} - className="h-5 w-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500" - /> - -
- {item.label} -
- ID: {item.id} -
-
- handleToggleProfileCheck(item.id)} - className="h-5 w-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500" - /> - - handleToggleOnlyNotify(item.id)} - className="h-5 w-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500" - /> - - - - - -
- - {session?.isAdmin && ( - - )} -
-
-
- -
- -
- - {activeRangeEntry && !activeRangeEntry.desiredWeekday && ( -
setActiveRangePicker(null)} - > -
event.stopPropagation()} - > -
-

Zeitraum auswählen für

-

- {activeRangeEntry.label || `Store ${activeRangeEntry.id}`} -

-
-
- { - const { startDate, endDate } = ranges.selection; - handleDateRangeSelection(activeRangeEntry.id, startDate, endDate); - }} - moveRangeOnFirstSelection={false} - ranges={[ - buildSelectionRange( - activeRangeEntry.desiredDateRange?.start, - activeRangeEntry.desiredDateRange?.end, - minSelectableDate - ) - ]} - rangeColors={['#2563EB']} - months={1} - direction="horizontal" - showDateDisplay={false} - locale={de} - minDate={minSelectableDate} - /> -
-
- -
- - -
-
-
-
- )} -
+ setNotificationPanelOpen((prev) => !prev)} + notificationProps={{ + error: notificationError, + message: notificationMessage, + settings: notificationSettings, + capabilities: notificationCapabilities, + loading: notificationLoading, + dirty: notificationDirty, + saving: notificationSaving, + onReset: loadNotificationSettings, + onSave: saveNotificationSettings, + onFieldChange: handleNotificationFieldChange, + onSendTest: sendNotificationTest, + onCopyLink: copyToClipboard, + copyFeedback, + ntfyPreviewUrl + }} + stores={stores} + availableCollapsed={availableCollapsed} + onToggleStores={() => setAvailableCollapsed((prev) => !prev)} + onStoreSelect={handleStoreSelection} + configMap={configMap} + error={error} + onDismissError={() => setError('')} + status={status} + visibleConfig={visibleConfig} + config={config} + onToggleActive={handleToggleActive} + onToggleProfileCheck={handleToggleProfileCheck} + onToggleOnlyNotify={handleToggleOnlyNotify} + onWeekdayChange={handleWeekdayChange} + weekdays={weekdays} + onRangePickerRequest={setActiveRangePicker} + formatRangeLabel={formatRangeLabel} + onSaveConfig={saveConfig} + onResetConfig={() => fetchConfig()} + /> ); const adminPageContent = session?.isAdmin ? ( -
-
-
-

Admin-Einstellungen

-

Globale Abläufe und Verzögerungen für die Abhol-Automation verwalten.

-
- - Zur Konfiguration - -
- {error && ( -
- {error} - -
- )} - {status && ( -
- {status} -
- )} -
- {adminSettingsLoading &&

Lade Admin-Einstellungen...

} - {!adminSettingsLoading && !adminSettings && ( -

Keine Admin-Einstellungen verfügbar.

- )} - {adminSettings && ( - <> -
-
- - handleAdminSettingChange('scheduleCron', e.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. 0 * * * *" - /> -
-
- -
- handleAdminSettingChange('initialDelayMinSeconds', e.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" - /> - handleAdminSettingChange('initialDelayMaxSeconds', e.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" - /> -
-
-
- -
- handleAdminSettingChange('randomDelayMinSeconds', e.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" - /> - handleAdminSettingChange('randomDelayMaxSeconds', e.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" - /> -
-
-
- - handleAdminSettingChange('storePickupCheckDelayMs', e.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. 400" - /> -

Hilft Rate-Limits beim Abfragen der Pickups zu vermeiden.

-
-
- -
-

Benachrichtigungen

-

- Lege fest, welche Kanäle zur Verfügung stehen und welche Zugangsdaten verwendet werden. -

-
-
-
-
-

ntfy

-

Server-basierte Push-Benachrichtigungen

-
- -
-
-
- - handleAdminNotificationChange('ntfy', 'serverUrl', e.target.value)} - className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" - placeholder="https://ntfy.sh" - /> -
-
- - handleAdminNotificationChange('ntfy', 'topicPrefix', e.target.value)} - className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" - placeholder="optional" - /> -

- Wird vor jedes User-Topic gesetzt (z. B. pickup-); getrennt wird automatisch per Bindestrich. -

-
-
-
- - handleAdminNotificationChange('ntfy', 'username', e.target.value)} - className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" - placeholder="optional" - /> -
-
- - handleAdminNotificationChange('ntfy', 'password', e.target.value)} - className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" - placeholder="optional" - /> -
-
-
-
- -
-
-
-

Telegram

-

Benachrichtigungen über einen Bot

-
- -
-
- - handleAdminNotificationChange('telegram', 'botToken', e.target.value)} - className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" - placeholder="123456:ABCDEF" - /> -

- Erstelle einen Bot via @BotFather und trage den Token hier ein. -

-
-
-
-
- -
-
-

Ignorierte Slots

- -
- {(!adminSettings.ignoredSlots || adminSettings.ignoredSlots.length === 0) && ( -

Keine Regeln definiert.

- )} - {adminSettings.ignoredSlots?.map((slot, index) => ( -
- handleIgnoredSlotChange(index, 'storeId', e.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" - /> - handleIgnoredSlotChange(index, 'description', e.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" - /> - -
- ))} -
- -
- -
- - )} -
-
+ setError('')} + onSettingChange={handleAdminSettingChange} + onIgnoredSlotChange={handleIgnoredSlotChange} + onAddIgnoredSlot={addIgnoredSlot} + onRemoveIgnoredSlot={removeIgnoredSlot} + onNotificationChange={handleAdminNotificationChange} + onSave={saveAdminSettings} + /> ) : ( ); @@ -1978,178 +1202,23 @@ function App() { onConfirm={() => handleConfirmDialog(true)} onCancel={() => handleConfirmDialog(false)} /> + + activeRangeEntry ? handleDateRangeSelection(activeRangeEntry.id, startDate, endDate) : null + } + onResetRange={() => { + if (activeRangeEntry) { + handleDateRangeSelection(activeRangeEntry.id, null, null); + } + setActiveRangePicker(null); + }} + onClose={() => setActiveRangePicker(null)} + /> ); } -function NavigationTabs({ isAdmin, onProtectedNavigate }) { - const location = useLocation(); - const navigate = useNavigate(); - if (!isAdmin) { - return null; - } - const tabs = [ - { to: '/', label: 'Konfiguration' }, - { to: '/admin', label: 'Admin' } - ]; - - const handleClick = (event, tab) => { - event.preventDefault(); - if (location.pathname === tab.to) { - return; - } - if (onProtectedNavigate) { - onProtectedNavigate(`zur Seite "${tab.label}" wechseln`, () => navigate(tab.to)); - } else { - navigate(tab.to); - } - }; - - return ( -
- {tabs.map((tab) => { - const isActive = location.pathname === tab.to; - return ( - handleClick(event, tab)} - className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${ - isActive ? 'bg-blue-600 text-white shadow' : 'bg-white text-blue-600 border border-blue-200 hover:bg-blue-50' - }`} - > - {tab.label} - - ); - })} -
- ); -} - -function AdminAccessMessage() { - return ( -
-

Kein Zugriff

-

Dieser Bereich ist nur für Administratoren verfügbar.

- - Zurück zur Konfiguration - -
- ); -} - -function DirtyNavigationDialog({ open, message, onSave, onDiscard, onCancel, saving }) { - if (!open) { - return null; - } - return ( -
-
-

Änderungen nicht gespeichert

-

{message || 'Es gibt ungespeicherte Änderungen. Wie möchtest du fortfahren?'}

-
- - - -
-
-
- ); -} - -function ConfirmationDialog({ open, title, message, confirmLabel, cancelLabel, confirmTone = 'primary', onConfirm, onCancel }) { - if (!open) { - return null; - } - const confirmClasses = - confirmTone === 'danger' - ? 'bg-red-600 hover:bg-red-700 focus:ring-red-500' - : 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'; - return ( -
-
-

{title || 'Bitte bestätigen'}

-

{message || 'Bist du sicher?'}

-
- - -
-
-
- ); -} - -function formatEta(seconds) { - if (seconds == null || seconds === Infinity) { - return null; - } - const clamped = Math.max(0, seconds); - const mins = Math.floor(clamped / 60); - const secs = clamped % 60; - if (mins > 0) { - return `${mins}m ${secs.toString().padStart(2, '0')}s`; - } - return `${secs}s`; -} - -function StoreSyncOverlay({ state }) { - if (!state?.active) { - return null; - } - const percent = Math.round(state.percent || 0); - const backgroundColor = state.block ? 'rgba(255,255,255,0.95)' : 'rgba(15,23,42,0.4)'; - const etaLabel = formatEta(state.etaSeconds); - - return ( -
-
-

{state.message || 'Synchronisiere...'}

-
-
-
-
- {percent}% - {etaLabel ? `ETA ~ ${etaLabel}` : 'Bitte warten...'} -
-

- Die Verzögerung schützt vor Rate-Limits während die Betriebe geprüft werden. -

-
-
- ); -} - export default App; diff --git a/src/components/AdminAccessMessage.js b/src/components/AdminAccessMessage.js new file mode 100644 index 0000000..d345b7c --- /dev/null +++ b/src/components/AdminAccessMessage.js @@ -0,0 +1,16 @@ +import { Link } from 'react-router-dom'; + +const AdminAccessMessage = () => ( +
+

Kein Zugriff

+

Dieser Bereich ist nur für Administratoren verfügbar.

+ + Zurück zur Konfiguration + +
+); + +export default AdminAccessMessage; diff --git a/src/components/AdminSettingsPanel.js b/src/components/AdminSettingsPanel.js new file mode 100644 index 0000000..b0995af --- /dev/null +++ b/src/components/AdminSettingsPanel.js @@ -0,0 +1,253 @@ +import { Link } from 'react-router-dom'; + +const AdminSettingsPanel = ({ + adminSettings, + adminSettingsLoading, + status, + error, + onDismissError, + onSettingChange, + onIgnoredSlotChange, + onAddIgnoredSlot, + onRemoveIgnoredSlot, + onNotificationChange, + onSave +}) => { + return ( +
+
+
+

Admin-Einstellungen

+

Globale Abläufe und Verzögerungen für die Abhol-Automation verwalten.

+
+ + Zur Konfiguration + +
+ + {error && ( +
+ {error} + +
+ )} + + {status && ( +
+ {status} +
+ )} + +
+ {adminSettingsLoading &&

Lade Admin-Einstellungen...

} + {!adminSettingsLoading && !adminSettings &&

Keine Admin-Einstellungen verfügbar.

} + + {adminSettings && ( + <> +
+
+ + 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. 0 * * * *" + /> +
+ +
+ +
+ 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" + /> + 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" + /> +
+
+ +
+ +
+ 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" + /> + 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" + /> +
+
+ +
+ + 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" + /> +
+
+ +
+
+

Ignorierte Slots

+ +
+ {(!adminSettings.ignoredSlots || adminSettings.ignoredSlots.length === 0) && ( +

Keine Regeln definiert.

+ )} + {adminSettings.ignoredSlots?.map((slot, index) => ( +
+ 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" + /> + 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" + /> + +
+ ))} +
+ +
+
+
+
+

ntfy

+

Konfiguration für Systembenachrichtigungen via ntfy.

+
+ +
+
+ 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" + /> + 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" + /> + 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" + /> + 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" + /> +
+
+ +
+
+
+

Telegram

+

Bot-Konfiguration für Telegram-Benachrichtigungen.

+
+ +
+ 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" + /> +
+
+ +
+ +
+ + )} +
+
+ ); +}; + +export default AdminSettingsPanel; diff --git a/src/components/ConfirmationDialog.js b/src/components/ConfirmationDialog.js new file mode 100644 index 0000000..ca893aa --- /dev/null +++ b/src/components/ConfirmationDialog.js @@ -0,0 +1,44 @@ +const ConfirmationDialog = ({ + open, + title, + message, + confirmLabel, + cancelLabel, + confirmTone = 'primary', + onConfirm, + onCancel +}) => { + if (!open) { + return null; + } + + const confirmClasses = + confirmTone === 'danger' + ? 'bg-red-600 hover:bg-red-700 focus:ring-red-500' + : 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'; + + return ( +
+
+

{title || 'Bitte bestätigen'}

+

{message || 'Bist du sicher?'}

+
+ + +
+
+
+ ); +}; + +export default ConfirmationDialog; diff --git a/src/components/DashboardView.js b/src/components/DashboardView.js new file mode 100644 index 0000000..188d564 --- /dev/null +++ b/src/components/DashboardView.js @@ -0,0 +1,299 @@ +import NotificationPanel from './NotificationPanel'; + +const DashboardView = ({ + session, + onRefresh, + onLogout, + notificationPanelOpen, + onToggleNotificationPanel, + notificationProps, + stores, + availableCollapsed, + onToggleStores, + onStoreSelect, + configMap, + error, + onDismissError, + status, + visibleConfig, + config, + onToggleActive, + onToggleProfileCheck, + onToggleOnlyNotify, + onWeekdayChange, + weekdays, + onRangePickerRequest, + formatRangeLabel, + onSaveConfig, + onResetConfig +}) => { + const { + error: notificationError, + message: notificationMessage, + settings: notificationSettings, + capabilities: notificationCapabilities, + loading: notificationLoading, + dirty: notificationDirty, + saving: notificationSaving, + onReset: onNotificationReset, + onSave: onNotificationSave, + onFieldChange: onNotificationFieldChange, + onSendTest: onNotificationTest, + onCopyLink: onNotificationCopy, + copyFeedback, + ntfyPreviewUrl + } = notificationProps; + + return ( +
+

Foodsharing Pickup Manager

+
+
+
+

Angemeldet

+

{session.profile.name}

+

Profil-ID: {session.profile.id}

+
+
+ + + + {notificationLoading && Lade…} +
+
+
+ + {notificationPanelOpen && ( + + )} + +
+ + {!availableCollapsed && ( +
+ {stores.map((store) => { + const storeId = String(store.id); + const entry = configMap.get(storeId); + const isVisible = entry && !entry.hidden; + const blockedByNoPickups = store.hasPickupSlots === false; + let statusLabel = 'Hinzufügen'; + let statusClass = 'text-blue-600'; + if (isVisible) { + statusLabel = 'Bereits in Konfiguration'; + statusClass = 'text-gray-500'; + } else if (entry) { + statusLabel = 'Ausgeblendet – erneut hinzufügen'; + statusClass = 'text-amber-600'; + } else if (blockedByNoPickups) { + statusLabel = 'Keine Pickups – automatisch verborgen'; + statusClass = 'text-red-600'; + } + return ( +
+
+

{store.name}

+

+ {store.zip} {store.city} +

+

{statusLabel}

+
+ +
+ ); + })} +
+ )} +
+ + {error && ( +
+ {error} + +
+ )} + + {status && ( +
+ {status} +
+ )} + +
+ + + + + + + + + + + + + {visibleConfig.map((item, index) => { + const normalizedRange = item.desiredDateRange + ? { ...item.desiredDateRange } + : item.desiredDate + ? { start: item.desiredDate, end: item.desiredDate } + : null; + const rangeStart = normalizedRange?.start || ''; + const rangeEnd = normalizedRange?.end || ''; + const hasDateRange = Boolean(rangeStart || rangeEnd); + return ( + + + + + + + + + ); + })} + +
AktivGeschäftProfil prüfenNur benachrichtigenWochentagDatum / Zeitraum
+ onToggleActive(item.id)} + className="h-5 w-5" + /> + + {item.label} + {item.hidden && (ausgeblendet)} + + onToggleProfileCheck(item.id)} + className="h-5 w-5" + /> + + onToggleOnlyNotify(item.id)} + className="h-5 w-5" + /> + + + + +
+
+ +
+ + +
+ +
+

Aktuelle JSON-Konfiguration:

+
{JSON.stringify(config, null, 2)}
+
+
+ ); +}; + +export default DashboardView; diff --git a/src/components/DirtyNavigationDialog.js b/src/components/DirtyNavigationDialog.js new file mode 100644 index 0000000..bb3ac22 --- /dev/null +++ b/src/components/DirtyNavigationDialog.js @@ -0,0 +1,39 @@ +const DirtyNavigationDialog = ({ open, message, onSave, onDiscard, onCancel, saving }) => { + if (!open) { + return null; + } + + return ( +
+
+

Änderungen nicht gespeichert

+

+ {message || 'Es gibt ungespeicherte Änderungen. Wie möchtest du fortfahren?'} +

+
+ + + +
+
+
+ ); +}; + +export default DirtyNavigationDialog; diff --git a/src/components/LoginView.js b/src/components/LoginView.js new file mode 100644 index 0000000..bd6d326 --- /dev/null +++ b/src/components/LoginView.js @@ -0,0 +1,73 @@ +const LoginView = ({ credentials, onCredentialsChange, error, loading, initializing, onSubmit }) => { + const handleChange = (field) => (event) => { + onCredentialsChange({ ...credentials, [field]: event.target.value }); + }; + + return ( +
+
{ + if (initializing) { + event.preventDefault(); + return; + } + onSubmit(event); + }} + > +
+

Pickup Config Login

+

+ Die Anwendung authentifiziert sich direkt bei foodsharing.de. Bitte verwende deine persönlichen Login-Daten. +

+
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+ +
+ + +
+ + +
+
+ ); +}; + +export default LoginView; diff --git a/src/components/NavigationTabs.js b/src/components/NavigationTabs.js new file mode 100644 index 0000000..e603e58 --- /dev/null +++ b/src/components/NavigationTabs.js @@ -0,0 +1,49 @@ +import { Link, useLocation, useNavigate } from 'react-router-dom'; + +const NavigationTabs = ({ isAdmin, onProtectedNavigate }) => { + const location = useLocation(); + const navigate = useNavigate(); + + if (!isAdmin) { + return null; + } + + const tabs = [ + { to: '/', label: 'Konfiguration' }, + { to: '/admin', label: 'Admin' } + ]; + + const handleClick = (event, to) => { + event.preventDefault(); + if (to === location.pathname) { + return; + } + onProtectedNavigate(`zur Seite "${tabs.find((tab) => tab.to === to)?.label || ''}" zu wechseln`, () => + navigate(to) + ); + }; + + return ( + + ); +}; + +export default NavigationTabs; diff --git a/src/components/NotificationPanel.js b/src/components/NotificationPanel.js new file mode 100644 index 0000000..6557cfb --- /dev/null +++ b/src/components/NotificationPanel.js @@ -0,0 +1,196 @@ +const NotificationPanel = ({ + error, + message, + settings, + capabilities, + loading, + dirty, + saving, + onReset, + onSave, + onFieldChange, + onSendTest, + onCopyLink, + copyFeedback, + ntfyPreviewUrl +}) => { + return ( +
+ {error && ( +
+ {error} +
+ )} + + {message && ( +
+ {message} +
+ )} + +
+
+
+
+

ntfy

+

+ Push aufs Handy über die ntfy-App oder Browser (Themenkanal notwendig). +

+
+ +
+ + {!capabilities.ntfy.enabled ? ( +

+ Diese Option wurde vom Admin deaktiviert. Bitte frage nach, wenn du ntfy nutzen möchtest. +

+ ) : ( +
+
+ + onFieldChange('ntfy', 'topic', event.target.value)} + placeholder="z. B. mein-pickup-topic" + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + disabled={!settings.ntfy.enabled} + /> +
+ {capabilities.ntfy.serverUrl && ( +

+ Server: {capabilities.ntfy.serverUrl} (vom Admin festgelegt) +

+ )} + {capabilities.ntfy.topicPrefix && ( +

+ Präfix: {capabilities.ntfy.topicPrefix} (Bindestrich wird automatisch ergänzt) +

+ )} + {ntfyPreviewUrl && ( +
+ + {ntfyPreviewUrl} + + +
+ )} + {copyFeedback &&

{copyFeedback}

} + +
+ )} +
+ +
+
+
+

Telegram

+

+ Nutze den Bot des Admins, um Nachrichten direkt in Telegram zu erhalten. +

+
+ +
+ + {!capabilities.telegram.enabled ? ( +

+ Telegram-Benachrichtigungen sind derzeit deaktiviert oder der Bot ist nicht konfiguriert. +

+ ) : ( +
+
+ + onFieldChange('telegram', 'chatId', event.target.value)} + placeholder="z. B. 123456789" + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + disabled={!settings.telegram.enabled} + /> +

+ Tipp: Schreibe dem Telegram-Bot und nutze{' '} + + @userinfobot + {' '} + oder ein Chat-ID-Tool. +

+
+ +
+ )} +
+
+ +
+ + +
+
+ ); +}; + +export default NotificationPanel; diff --git a/src/components/StoreSyncOverlay.js b/src/components/StoreSyncOverlay.js new file mode 100644 index 0000000..a2f07c6 --- /dev/null +++ b/src/components/StoreSyncOverlay.js @@ -0,0 +1,45 @@ +const formatEta = (seconds) => { + if (seconds == null || seconds === Infinity) { + return null; + } + const clamped = Math.max(0, seconds); + const mins = Math.floor(clamped / 60); + const secs = clamped % 60; + if (mins > 0) { + return `${mins}m ${secs.toString().padStart(2, '0')}s`; + } + return `${secs}s`; +}; + +const StoreSyncOverlay = ({ state }) => { + if (!state?.active) { + return null; + } + + const percent = Math.round(state.percent || 0); + const backgroundColor = state.block ? 'rgba(255,255,255,0.95)' : 'rgba(15,23,42,0.4)'; + const etaLabel = formatEta(state.etaSeconds); + + return ( +
+
+

{state.message || 'Synchronisiere...'}

+
+
+
+
+ {percent}% + {etaLabel ? `ETA ~ ${etaLabel}` : 'Bitte warten...'} +
+

+ Die Verzögerung schützt vor Rate-Limits während die Betriebe geprüft werden. +

+
+
+ ); +}; + +export default StoreSyncOverlay;