aktueller Stand
This commit is contained in:
325
src/App.js
325
src/App.js
@@ -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">×</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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user