aktueller Stand

This commit is contained in:
root
2025-11-09 14:31:52 +01:00
parent dc95156793
commit cd93800ffb
5 changed files with 266 additions and 124 deletions

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, Link, useLocation } from 'react-router-dom';
import './App.css';
const emptyEntry = {
@@ -659,7 +660,7 @@ function App() {
);
}
return (
const dashboardContent = (
<div className="p-4 max-w-6xl mx-auto bg-white shadow-lg rounded-lg mt-4">
<h1 className="text-2xl font-bold mb-4 text-center text-gray-800">Foodsharing Pickup Manager</h1>
<div className="bg-blue-50 border border-blue-100 rounded-lg p-4 mb-4">
@@ -877,124 +878,6 @@ function App() {
</table>
</div>
{session?.isAdmin && (
<div className="mb-6 border border-purple-200 rounded-lg p-4 bg-purple-50">
<h2 className="text-lg font-semibold text-purple-900 mb-4">Admin-Einstellungen</h2>
{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 && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Cron-Ausdruck</label>
<input
type="text"
value={adminSettings.scheduleCron}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Initiale Verzögerung (Sek.)</label>
<div className="grid grid-cols-2 gap-2">
<input
type="number"
min="0"
value={adminSettings.initialDelayMinSeconds}
onChange={(e) => 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"
/>
<input
type="number"
min="0"
value={adminSettings.initialDelayMaxSeconds}
onChange={(e) => 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"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Prüfverzögerung (Sek.)</label>
<div className="grid grid-cols-2 gap-2">
<input
type="number"
min="0"
value={adminSettings.randomDelayMinSeconds}
onChange={(e) => 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"
/>
<input
type="number"
min="0"
value={adminSettings.randomDelayMaxSeconds}
onChange={(e) => 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"
/>
</div>
</div>
</div>
<div className="border border-purple-200 rounded-lg bg-white p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-purple-900">Ignorierte Slots</h3>
<button
type="button"
onClick={addIgnoredSlot}
className="text-sm bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded focus:outline-none focus:ring-2 focus:ring-purple-400"
>
Regel hinzufügen
</button>
</div>
{(!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 mb-2 items-center">
<input
type="text"
value={slot.storeId}
onChange={(e) => 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"
/>
<input
type="text"
value={slot.description}
onChange={(e) => 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"
/>
<button
type="button"
onClick={() => removeIgnoredSlot(index)}
className="text-sm text-red-600 hover:text-red-800 focus:outline-none"
>
Entfernen
</button>
</div>
))}
</div>
<div className="flex justify-end mt-4">
<button
type="button"
onClick={saveAdminSettings}
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>
)}
{showNewEntryForm ? (
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200 mb-6">
<h2 className="text-lg font-semibold mb-4">Neuen Eintrag hinzufügen</h2>
@@ -1126,6 +1009,210 @@ function App() {
</div>
</div>
);
const adminPageContent = session?.isAdmin ? (
<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 Verzögerungen für die Abhol-Automation verwalten.</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"
>
Zur Konfiguration
</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={() => setError('')}>
<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 && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Cron-Ausdruck</label>
<input
type="text"
value={adminSettings.scheduleCron}
onChange={(e) => 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 * * * *"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Initiale Verzögerung (Sek.)</label>
<div className="grid grid-cols-2 gap-2">
<input
type="number"
min="0"
value={adminSettings.initialDelayMinSeconds}
onChange={(e) => 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"
/>
<input
type="number"
min="0"
value={adminSettings.initialDelayMaxSeconds}
onChange={(e) => 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"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Prüfverzögerung (Sek.)</label>
<div className="grid grid-cols-2 gap-2">
<input
type="number"
min="0"
value={adminSettings.randomDelayMinSeconds}
onChange={(e) => 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"
/>
<input
type="number"
min="0"
value={adminSettings.randomDelayMaxSeconds}
onChange={(e) => 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"
/>
</div>
</div>
</div>
<div className="border border-purple-200 rounded-lg bg-white p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-purple-900">Ignorierte Slots</h3>
<button
type="button"
onClick={addIgnoredSlot}
className="text-sm bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded focus:outline-none focus:ring-2 focus:ring-purple-400"
>
Regel hinzufügen
</button>
</div>
{(!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 mb-2 items-center">
<input
type="text"
value={slot.storeId}
onChange={(e) => 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"
/>
<input
type="text"
value={slot.description}
onChange={(e) => 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"
/>
<button
type="button"
onClick={() => removeIgnoredSlot(index)}
className="text-sm text-red-600 hover:text-red-800 focus:outline-none"
>
Entfernen
</button>
</div>
))}
</div>
<div className="flex justify-end mt-4">
<button
type="button"
onClick={saveAdminSettings}
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>
) : (
<AdminAccessMessage />
);
return (
<Router>
<div className="min-h-screen bg-gray-100 py-6">
<div className="max-w-7xl mx-auto px-4">
<NavigationTabs isAdmin={session?.isAdmin} />
<Routes>
<Route path="/" element={dashboardContent} />
<Route path="/admin" element={adminPageContent} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</div>
</div>
</Router>
);
}
function NavigationTabs({ isAdmin }) {
const location = useLocation();
const tabs = [{ to: '/', label: 'Konfiguration' }];
if (isAdmin) {
tabs.push({ to: '/admin', label: 'Admin' });
}
return (
<div className="flex flex-wrap gap-2 mb-4">
{tabs.map((tab) => {
const isActive = location.pathname === tab.to;
return (
<Link
key={tab.to}
to={tab.to}
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}
</Link>
);
})}
</div>
);
}
function AdminAccessMessage() {
return (
<div className="p-6 max-w-lg 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
to="/"
className="inline-flex items-center justify-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Zurück zur Konfiguration
</Link>
</div>
);
}
export default App;

View File

@@ -1,8 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
test('zeigt das Login-Formular an, wenn keine Session aktiv ist', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
expect(screen.getByText(/Pickup Config Login/i)).toBeInTheDocument();
expect(screen.getByLabelText(/E-Mail/i)).toBeInTheDocument();
});