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