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 (

Keine Session aktiv.

); } 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) => ( ))} ))} {table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} ))}
{flexRender(header.column.columnDef.header, header.getContext())}
{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;