Neue Seite um Betriebe zu überwachen
This commit is contained in:
@@ -696,7 +696,7 @@ function App() {
|
|||||||
<NavigationTabs isAdmin={session?.isAdmin} onProtectedNavigate={requestNavigation} />
|
<NavigationTabs isAdmin={session?.isAdmin} onProtectedNavigate={requestNavigation} />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={dashboardContent} />
|
<Route path="/" element={dashboardContent} />
|
||||||
<Route path="/store-watch" element={<StoreWatchPage authorizedFetch={authorizedFetch} />} />
|
<Route path="/store-watch" element={<StoreWatchPage authorizedFetch={authorizedFetch} knownStores={stores} />} />
|
||||||
<Route path="/admin" element={adminPageContent} />
|
<Route path="/admin" element={adminPageContent} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
const StoreWatchPage = ({ authorizedFetch }) => {
|
const StoreWatchPage = ({ authorizedFetch, knownStores = [] }) => {
|
||||||
const [regions, setRegions] = useState([]);
|
const [regions, setRegions] = useState([]);
|
||||||
const [selectedRegionId, setSelectedRegionId] = useState('');
|
const [selectedRegionId, setSelectedRegionId] = useState('');
|
||||||
const [storesByRegion, setStoresByRegion] = useState({});
|
const [storesByRegion, setStoresByRegion] = useState({});
|
||||||
@@ -12,6 +12,8 @@ const StoreWatchPage = ({ authorizedFetch }) => {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [filterText, setFilterText] = useState('');
|
||||||
|
const [sortBy, setSortBy] = useState('name');
|
||||||
|
|
||||||
const watchedIds = useMemo(
|
const watchedIds = useMemo(
|
||||||
() => new Set(watchList.map((entry) => String(entry.storeId))),
|
() => new Set(watchList.map((entry) => String(entry.storeId))),
|
||||||
@@ -36,6 +38,58 @@ const StoreWatchPage = ({ authorizedFetch }) => {
|
|||||||
[currentStores]
|
[currentStores]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const membershipMap = useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
(knownStores || []).forEach((store) => {
|
||||||
|
if (store?.id) {
|
||||||
|
map.set(String(store.id), store);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [knownStores]);
|
||||||
|
|
||||||
|
const filteredStores = useMemo(() => {
|
||||||
|
const search = filterText.trim().toLowerCase();
|
||||||
|
const data = !search
|
||||||
|
? [...eligibleStores]
|
||||||
|
: eligibleStores.filter((store) => {
|
||||||
|
const haystack = [
|
||||||
|
store.name,
|
||||||
|
store.city,
|
||||||
|
store.street,
|
||||||
|
store.zipCode,
|
||||||
|
store.id
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((value) => String(value).toLowerCase());
|
||||||
|
return haystack.some((value) => value.includes(search));
|
||||||
|
});
|
||||||
|
const compareString = (a = '', b = '') => a.localeCompare(b, 'de', { sensitivity: 'base' });
|
||||||
|
data.sort((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'city':
|
||||||
|
return compareString(a.city || '', b.city || '') || compareString(a.name || '', b.name || '');
|
||||||
|
case 'created-desc': {
|
||||||
|
const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
||||||
|
const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
||||||
|
if (timeA === timeB) {
|
||||||
|
return compareString(a.name || '', b.name || '');
|
||||||
|
}
|
||||||
|
return timeB - timeA;
|
||||||
|
}
|
||||||
|
case 'membership':
|
||||||
|
return (
|
||||||
|
Number(membershipMap.has(String(b.id))) - Number(membershipMap.has(String(a.id))) ||
|
||||||
|
compareString(a.name || '', b.name || '')
|
||||||
|
);
|
||||||
|
case 'name':
|
||||||
|
default:
|
||||||
|
return compareString(a.name || '', b.name || '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}, [eligibleStores, filterText, sortBy, membershipMap]);
|
||||||
|
|
||||||
const loadRegions = useCallback(async () => {
|
const loadRegions = useCallback(async () => {
|
||||||
if (!authorizedFetch) {
|
if (!authorizedFetch) {
|
||||||
return;
|
return;
|
||||||
@@ -279,6 +333,42 @@ const StoreWatchPage = ({ authorizedFetch }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1" htmlFor="store-filter">
|
||||||
|
Betriebe filtern
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="store-filter"
|
||||||
|
type="text"
|
||||||
|
value={filterText}
|
||||||
|
onChange={(event) => setFilterText(event.target.value)}
|
||||||
|
className="border rounded-md p-2 w-full"
|
||||||
|
placeholder="Name, Ort oder PLZ"
|
||||||
|
disabled={!selectedRegionId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1" htmlFor="store-sort">
|
||||||
|
Sortieren nach
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="store-sort"
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(event) => setSortBy(event.target.value)}
|
||||||
|
className="border rounded-md p-2 w-full"
|
||||||
|
disabled={!selectedRegionId}
|
||||||
|
>
|
||||||
|
<option value="name">Name (A-Z)</option>
|
||||||
|
<option value="city">Ort (A-Z)</option>
|
||||||
|
<option value="created-desc">Kooperation (neueste zuerst)</option>
|
||||||
|
<option value="membership">Eigene Betriebe zuerst</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Betriebe in der Region</h2>
|
<h2 className="text-lg font-semibold text-gray-800">Betriebe in der Region</h2>
|
||||||
@@ -290,27 +380,33 @@ const StoreWatchPage = ({ authorizedFetch }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{storesLoading && <p className="text-sm text-gray-600">Lade Betriebe...</p>}
|
{storesLoading && <p className="text-sm text-gray-600">Lade Betriebe...</p>}
|
||||||
{!storesLoading && (!selectedRegionId || eligibleStores.length === 0) && (
|
{!storesLoading && !selectedRegionId && (
|
||||||
|
<p className="text-sm text-gray-500">Bitte zuerst eine Region auswählen.</p>
|
||||||
|
)}
|
||||||
|
{!storesLoading && selectedRegionId && filteredStores.length === 0 && (
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{selectedRegionId
|
Keine Betriebe gefunden. Prüfe Filter oder sortiere anders.
|
||||||
? 'Keine geeigneten Betriebe (Status "aktiv") in dieser Region.'
|
|
||||||
: 'Bitte zuerst eine Region auswählen.'}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{!storesLoading && eligibleStores.length > 0 && (
|
{!storesLoading && filteredStores.length > 0 && (
|
||||||
<div className="overflow-x-auto border border-gray-200 rounded-lg">
|
<div className="overflow-x-auto border border-gray-200 rounded-lg">
|
||||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||||
<thead className="bg-gray-100">
|
<thead className="bg-gray-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left">Betrieb</th>
|
<th className="px-4 py-2 text-left">Betrieb</th>
|
||||||
<th className="px-4 py-2 text-left">Ort</th>
|
<th className="px-4 py-2 text-left">Ort</th>
|
||||||
<th className="px-4 py-2 text-left">Kooperation</th>
|
<th className="px-4 py-2 text-left">Kooperation seit</th>
|
||||||
|
<th className="px-4 py-2 text-center">Mitglied</th>
|
||||||
<th className="px-4 py-2 text-center">Überwachen</th>
|
<th className="px-4 py-2 text-center">Überwachen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{eligibleStores.map((store) => {
|
{filteredStores.map((store) => {
|
||||||
const checked = watchedIds.has(String(store.id));
|
const checked = watchedIds.has(String(store.id));
|
||||||
|
const isMember = membershipMap.has(String(store.id));
|
||||||
|
const sinceLabel = store.createdAt
|
||||||
|
? new Date(store.createdAt).toLocaleDateString('de-DE')
|
||||||
|
: 'unbekannt';
|
||||||
return (
|
return (
|
||||||
<tr key={store.id} className="bg-white">
|
<tr key={store.id} className="bg-white">
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
@@ -321,8 +417,15 @@ const StoreWatchPage = ({ authorizedFetch }) => {
|
|||||||
<p className="text-gray-800 text-sm">{store.city || 'unbekannt'}</p>
|
<p className="text-gray-800 text-sm">{store.city || 'unbekannt'}</p>
|
||||||
<p className="text-xs text-gray-500">{store.street || ''}</p>
|
<p className="text-xs text-gray-500">{store.street || ''}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-sm text-gray-600">
|
<td className="px-4 py-2 text-sm text-gray-600">{sinceLabel}</td>
|
||||||
Seit {store.createdAt ? new Date(store.createdAt).toLocaleDateString('de-DE') : 'n/a'}
|
<td className="px-4 py-2 text-center">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center justify-center px-2 py-1 text-xs font-semibold rounded ${
|
||||||
|
isMember ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isMember ? 'Ja' : 'Nein'}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-center">
|
<td className="px-4 py-2 text-center">
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user