import { useCallback, useEffect, useMemo, useState } from 'react';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
useReactTable
} from '@tanstack/react-table';
import { haversineDistanceKm } from '../utils/distance';
const columnHelper = createColumnHelper();
const ColumnTextFilter = ({ column, placeholder }) => {
if (!column.getCanFilter()) {
return null;
}
return (
column.setFilterValue(event.target.value)}
placeholder={placeholder}
className="mt-1 w-full rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
);
};
const ColumnSelectFilter = ({ column, options }) => {
if (!column.getCanFilter()) {
return null;
}
return (
);
};
const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) => {
const [regions, setRegions] = useState([]);
const [selectedRegionId, setSelectedRegionId] = useState('');
const [storesByRegion, setStoresByRegion] = useState({});
const [watchList, setWatchList] = useState([]);
const [regionLoading, setRegionLoading] = useState(false);
const [storesLoading, setStoresLoading] = useState(false);
const [subscriptionsLoading, setSubscriptionsLoading] = useState(false);
const [status, setStatus] = useState('');
const [error, setError] = useState('');
const [dirty, setDirty] = useState(false);
const [saving, setSaving] = useState(false);
const [sorting, setSorting] = useState([]);
const [columnFilters, setColumnFilters] = useState([{ id: 'isOpen', value: 'true' }]);
const watchedIds = useMemo(
() => new Set(watchList.map((entry) => String(entry.storeId))),
[watchList]
);
const selectedRegion = useMemo(() => {
if (selectedRegionId === 'all') {
return null;
}
return regions.find((region) => String(region.id) === String(selectedRegionId)) || null;
}, [regions, selectedRegionId]);
const activeRegionLabel = selectedRegionId === 'all' ? 'allen Regionen' : selectedRegion?.name || 'dieser Region';
const lastUpdatedAt = useMemo(() => {
if (selectedRegionId === 'all') {
const timestamps = regions
.map((region) => storesByRegion[String(region.id)]?.fetchedAt)
.filter(Boolean);
if (timestamps.length === 0) {
return null;
}
return Math.max(...timestamps);
}
return storesByRegion[String(selectedRegionId)]?.fetchedAt || null;
}, [regions, selectedRegionId, storesByRegion]);
const currentStores = useMemo(() => {
if (selectedRegionId === 'all') {
const combined = new Map();
regions.forEach((region) => {
const entry = storesByRegion[String(region.id)];
if (entry?.stores) {
entry.stores.forEach((store) => {
if (store?.id) {
combined.set(String(store.id), store);
}
});
}
});
return Array.from(combined.values());
}
const regionEntry = storesByRegion[String(selectedRegionId)];
if (!regionEntry || !Array.isArray(regionEntry.stores)) {
return [];
}
return regionEntry.stores;
}, [regions, storesByRegion, selectedRegionId]);
const regionStores = useMemo(() => 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 handleToggleStore = useCallback(
(store, checked) => {
setWatchList((prev) => {
const storeId = String(store.id || store.storeId);
const existing = prev.find((entry) => entry.storeId === storeId);
if (checked) {
if (!store.isOpen || existing) {
return prev;
}
setDirty(true);
const regionName = store.region?.name || selectedRegion?.name || existing?.regionName || '';
return [
...prev,
{
storeId,
storeName: store.name || store.storeName || `Store ${storeId}`,
regionId: String(store.region?.id || selectedRegionId || existing?.regionId || ''),
regionName,
lastTeamSearchStatus: existing?.lastTeamSearchStatus ?? null
}
];
}
if (!existing) {
return prev;
}
setDirty(true);
return prev.filter((entry) => entry.storeId !== storeId);
});
},
[selectedRegion, selectedRegionId]
);
const handleRemoveWatch = useCallback((storeId) => {
setWatchList((prev) => {
const next = prev.filter((entry) => entry.storeId !== storeId);
if (next.length !== prev.length) {
setDirty(true);
}
return next;
});
}, []);
const tableData = useMemo(
() =>
regionStores.map((store) => {
const membership = membershipMap.has(String(store.id));
const lat = Number(store.location?.lat);
const lon = Number(store.location?.lon);
const distance =
userLocation && Number.isFinite(lat) && Number.isFinite(lon)
? haversineDistanceKm(userLocation.lat, userLocation.lon, lat, lon)
: null;
const isOpen = Number(store.cooperationStatus) === 5;
return {
...store,
membership,
distanceKm: distance,
isOpen
};
}),
[regionStores, membershipMap, userLocation]
);
const columns = useMemo(
() => [
columnHelper.accessor('name', {
header: ({ column }) => (
),
cell: ({ row }) => (
{row.original.name}
#{row.original.id}
),
sortingFn: 'alphanumeric',
enableColumnFilter: true,
filterFn: 'includesString'
}),
columnHelper.accessor((row) => row.city || '', {
id: 'city',
header: ({ column }) => (
),
cell: ({ row }) => (
{row.original.city || 'unbekannt'}
{row.original.street || ''}
),
sortingFn: 'alphanumeric',
filterFn: 'includesString'
}),
columnHelper.accessor('createdAt', {
header: ({ column }) => (
),
cell: ({ getValue }) => {
const value = getValue();
return (
{value ? new Date(value).toLocaleDateString('de-DE') : 'unbekannt'}
);
},
sortingFn: (rowA, rowB, columnId) => {
const a = rowA.getValue(columnId);
const b = rowB.getValue(columnId);
return new Date(a || 0).getTime() - new Date(b || 0).getTime();
}
}),
columnHelper.accessor('isOpen', {
header: ({ column }) => (
),
cell: ({ getValue }) => {
const value = getValue();
return (
{value ? 'Ja' : 'Nein'}
);
},
filterFn: (row, columnId, value) => {
if (value === undefined) {
return true;
}
const boolValue = value === 'true';
return row.getValue(columnId) === boolValue;
},
sortingFn: (rowA, rowB, columnId) => {
const a = rowA.getValue(columnId);
const b = rowB.getValue(columnId);
return Number(b) - Number(a);
}
}),
columnHelper.accessor('membership', {
header: ({ column }) => (
),
cell: ({ getValue }) => {
const value = getValue();
return (
{value ? 'Ja' : 'Nein'}
);
},
filterFn: (row, columnId, value) => {
if (value === undefined) {
return true;
}
const boolValue = value === 'true';
return row.getValue(columnId) === boolValue;
},
sortingFn: (rowA, rowB, columnId) => {
const a = rowA.getValue(columnId);
const b = rowB.getValue(columnId);
return Number(b) - Number(a);
}
}),
columnHelper.accessor('distanceKm', {
header: ({ column }) => (
{!userLocation &&
Standort erforderlich
}
),
cell: ({ getValue }) => {
const value = getValue();
if (!value && value !== 0) {
return –;
}
return {value.toFixed(2)} km;
},
sortingFn: (rowA, rowB, columnId) => {
const a = rowA.getValue(columnId);
const b = rowB.getValue(columnId);
if (a === null || a === undefined) {
return 1;
}
if (b === null || b === undefined) {
return -1;
}
return a - b;
},
enableColumnFilter: false,
enableSorting: !!userLocation
}),
columnHelper.display({
id: 'watch',
header: () => Überwachen,
cell: ({ row }) => {
const store = row.original;
const checked = watchedIds.has(String(store.id));
return (
handleToggleStore(store, event.target.checked)}
/>
);
}
})
],
[handleToggleStore, watchedIds, userLocation]
);
const table = useReactTable({
data: tableData,
columns,
state: {
sorting,
columnFilters
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel()
});
const loadRegions = useCallback(async () => {
if (!authorizedFetch) {
return;
}
setRegionLoading(true);
setError('');
try {
const response = await authorizedFetch('/api/store-watch/regions');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const normalized = Array.isArray(data.regions) ? data.regions : [];
setRegions(normalized);
setSelectedRegionId((prev) => {
if (!prev) {
return 'all';
}
if (prev === 'all') {
return prev;
}
const exists = normalized.some((region) => String(region.id) === String(prev));
if (!exists) {
return normalized.length > 0 ? String(normalized[0].id) : 'all';
}
return prev;
});
} catch (err) {
setError(`Regionen konnten nicht geladen werden: ${err.message}`);
} finally {
setRegionLoading(false);
}
}, [authorizedFetch, selectedRegionId]);
const loadSubscriptions = useCallback(async () => {
if (!authorizedFetch) {
return;
}
setSubscriptionsLoading(true);
setError('');
try {
const response = await authorizedFetch('/api/store-watch/subscriptions');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const normalized = Array.isArray(data.stores) ? data.stores : [];
setWatchList(normalized);
setDirty(false);
} catch (err) {
setError(`Überwachte Betriebe konnten nicht geladen werden: ${err.message}`);
} finally {
setSubscriptionsLoading(false);
}
}, [authorizedFetch]);
const fetchStoresForRegion = useCallback(
async (regionId, { force, silent } = {}) => {
if (!authorizedFetch || !regionId) {
return;
}
if (!force && storesByRegion[String(regionId)]) {
return;
}
if (!silent) {
setStoresLoading(true);
}
setError('');
try {
const endpoint = force
? `/api/store-watch/regions/${regionId}/stores?force=1`
: `/api/store-watch/regions/${regionId}/stores`;
const response = await authorizedFetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setStoresByRegion((prev) => ({
...prev,
[String(regionId)]: {
total: Number(data.total) || 0,
stores: Array.isArray(data.stores) ? data.stores : [],
fetchedAt: Date.now()
}
}));
} catch (err) {
setError(`Betriebe konnten nicht geladen werden: ${err.message}`);
} finally {
if (!silent) {
setStoresLoading(false);
}
}
},
[authorizedFetch, storesByRegion]
);
const fetchAllRegions = useCallback(
async ({ force } = {}) => {
if (!authorizedFetch || regions.length === 0) {
return;
}
const targets = regions.filter(
(region) => force || !storesByRegion[String(region.id)]
);
if (targets.length === 0) {
return;
}
setStoresLoading(true);
setError('');
try {
for (const region of targets) {
await fetchStoresForRegion(region.id, { force, silent: true });
}
} catch (err) {
setError(`Betriebe konnten nicht geladen werden: ${err.message}`);
} finally {
setStoresLoading(false);
}
},
[authorizedFetch, regions, storesByRegion, fetchStoresForRegion]
);
useEffect(() => {
loadRegions();
loadSubscriptions();
}, [loadRegions, loadSubscriptions]);
useEffect(() => {
if (!selectedRegionId) {
return;
}
if (selectedRegionId === 'all') {
fetchAllRegions({ force: false });
} else {
fetchStoresForRegion(selectedRegionId);
}
}, [selectedRegionId, fetchStoresForRegion, fetchAllRegions]);
const handleSave = useCallback(async () => {
if (!authorizedFetch || saving || !dirty) {
return;
}
setSaving(true);
setStatus('');
setError('');
try {
const response = await authorizedFetch('/api/store-watch/subscriptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stores: watchList })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setWatchList(Array.isArray(data.stores) ? data.stores : []);
setDirty(false);
setStatus('Überwachung gespeichert.');
setTimeout(() => setStatus(''), 4000);
} catch (err) {
setError(`Speichern fehlgeschlagen: ${err.message}`);
} finally {
setSaving(false);
}
}, [authorizedFetch, dirty, saving, watchList]);
const handleReset = useCallback(() => {
loadSubscriptions();
}, [loadSubscriptions]);
if (!authorizedFetch) {
return (
);
}
return (
Betriebs-Monitoring
Wähle Betriebe aus, die bei offenem Team-Status automatisch gemeldet werden sollen.
{(error || status) && (
{error && (
{error}
)}
{status && (
{status}
)}
)}
Es werden nur Regionen angezeigt, in denen du Mitglied mit Klassifikation 1 bist.
Betriebe in {activeRegionLabel}
{lastUpdatedAt && (
Aktualisiert: {new Date(lastUpdatedAt).toLocaleTimeString('de-DE')}
)}
{storesLoading &&
Lade Betriebe...
}
{!storesLoading && table.getRowModel().rows.length === 0 && (
Keine Betriebe gefunden. Prüfe Filter oder sortiere anders.
)}
{!storesLoading && table.getRowModel().rows.length > 0 && (
{table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header) => (
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
))}
))}
{table.getRowModel().rows.map((row) => (
{row.getVisibleCells().map((cell) => (
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
))}
))}
)}
Überwachte Betriebe ({watchList.length})
{subscriptionsLoading &&
Lade aktuelle Auswahl...
}
{!subscriptionsLoading && watchList.length === 0 && (
Noch keine Betriebe ausgewählt.
)}
{watchList.length > 0 && (
{watchList.map((entry) => (
{entry.storeName}
#{entry.storeId} — {entry.regionName || 'Region unbekannt'}
Letzter Status:{' '}
{entry.lastTeamSearchStatus === 1
? 'Suchend'
: entry.lastTeamSearchStatus === 0
? 'Nicht suchend'
: 'Unbekannt'}
))}
)}
{dirty && (
Es gibt ungespeicherte Änderungen. Bitte "Speichern" klicken.
)}
);
};
export default StoreWatchPage;