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

@@ -2,6 +2,6 @@
"839246": { "839246": {
"email": "meikdre@gmx.de", "email": "meikdre@gmx.de",
"password": "R67aJUj2-wWVfP8", "password": "R67aJUj2-wWVfP8",
"token": "1fdccfbe-2182-4749-9f42-ac79345c143d" "token": "5e5b273c-fd9c-4a1f-a4cd-55b26a3b6419"
} }
} }

54
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.9.5",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
@@ -15232,6 +15233,53 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.9.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
"integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.9.5",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz",
"integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==",
"license": "MIT",
"dependencies": {
"react-router": "7.9.5"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-router/node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/react-scripts": { "node_modules/react-scripts": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -16159,6 +16207,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",

View File

@@ -3,17 +3,18 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"axios": "^1.7.7",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.7.7",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.9.5",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, Link, useLocation } from 'react-router-dom';
import './App.css'; import './App.css';
const emptyEntry = { 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"> <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> <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"> <div className="bg-blue-50 border border-blue-100 rounded-lg p-4 mb-4">
@@ -877,124 +878,6 @@ function App() {
</table> </table>
</div> </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 ? ( {showNewEntryForm ? (
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200 mb-6"> <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> <h2 className="text-lg font-semibold mb-4">Neuen Eintrag hinzufügen</h2>
@@ -1126,6 +1009,210 @@ function App() {
</div> </div>
</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; export default App;

View File

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