import { useCallback, useEffect, useMemo, useState } from 'react'; import { createColumnHelper, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; import { haversineDistanceKm } from '../utils/distance'; const REGION_STORAGE_KEY = 'storeWatchRegionSelection'; const WATCH_TABLE_STATE_KEY = 'storeWatchTableState'; 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 ( ); }; function readWatchTableState() { if (typeof window === 'undefined') { return { sorting: [], columnFilters: [{ id: 'isOpen', value: 'true' }] }; } try { const raw = window.localStorage.getItem(WATCH_TABLE_STATE_KEY); if (!raw) { return { sorting: [], columnFilters: [{ id: 'isOpen', value: 'true' }] }; } const parsed = JSON.parse(raw); return { sorting: Array.isArray(parsed.sorting) ? parsed.sorting : [], columnFilters: Array.isArray(parsed.columnFilters) && parsed.columnFilters.length > 0 ? parsed.columnFilters : [{ id: 'isOpen', value: 'true' }] }; } catch { return { sorting: [], columnFilters: [{ id: 'isOpen', value: 'true' }] }; } } function persistWatchTableState(state) { if (typeof window === 'undefined') { return; } try { window.localStorage.setItem(WATCH_TABLE_STATE_KEY, JSON.stringify(state)); } catch { /* ignore */ } } const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) => { const [regions, setRegions] = useState([]); const [selectedRegionId, setSelectedRegionId] = useState(() => { if (typeof window === 'undefined') { return 'all'; } try { return window.localStorage.getItem(REGION_STORAGE_KEY) || 'all'; } catch { return 'all'; } }); 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 initialTableState = useMemo(() => readWatchTableState(), []); const [sorting, setSorting] = useState(initialTableState.sorting); const [columnFilters, setColumnFilters] = useState(initialTableState.columnFilters); useEffect(() => { if (typeof window === 'undefined') { return; } try { window.localStorage.setItem(REGION_STORAGE_KEY, selectedRegionId || 'all'); } catch { /* ignore */ } }, [selectedRegionId]); useEffect(() => { persistWatchTableState({ sorting, columnFilters }); }, [sorting, columnFilters]); 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 selectedStatusMeta = useMemo(() => { if (selectedRegionId === 'all') { const metas = regions .map((region) => storesByRegion[String(region.id)]?.statusMeta) .filter(Boolean); if (metas.length === 0) { return null; } const aggregated = metas.reduce( (acc, meta) => ({ total: acc.total + (meta.total || 0), refreshed: acc.refreshed + (meta.refreshed || 0), fromCache: acc.fromCache + (meta.fromCache || 0), missing: acc.missing + (meta.missing || 0), generatedAt: Math.max(acc.generatedAt, meta.generatedAt || 0) }), { total: 0, refreshed: 0, fromCache: 0, missing: 0, generatedAt: 0 } ); return aggregated; } return storesByRegion[String(selectedRegionId)]?.statusMeta || null; }, [selectedRegionId, storesByRegion, regions]); 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.filter((store) => Number(store.cooperationStatus) === 5), [currentStores] ); const statusSummary = useMemo(() => { if (!selectedStatusMeta) { return 'Team-Status noch nicht geladen.'; } const parts = [ `${selectedStatusMeta.refreshed || 0} aktualisiert`, `${selectedStatusMeta.fromCache || 0} aus Cache` ]; if (selectedStatusMeta.missing) { parts.push(`${selectedStatusMeta.missing} ohne Daten`); } const timestamp = selectedStatusMeta.generatedAt ? new Date(selectedStatusMeta.generatedAt).toLocaleString('de-DE') : null; return `Team-Status: ${parts.join(', ')}${timestamp ? ` (Stand ${timestamp})` : ''}`; }, [selectedStatusMeta]); 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 (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 statusValue = store.teamSearchStatus === null || store.teamSearchStatus === undefined ? null : Number(store.teamSearchStatus); const distance = userLocation && Number.isFinite(lat) && Number.isFinite(lon) ? haversineDistanceKm(userLocation.lat, userLocation.lon, lat, lon) : null; const isOpen = statusValue === 1; return { ...store, membership, distanceKm: distance, teamStatusUpdatedAt: store.teamStatusUpdatedAt || null, teamSearchStatus: statusValue, 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(); const label = value ? new Date(value).toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' }) : '–'; return
{label}
; }, 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: ({ row, getValue }) => { const value = getValue(); const updatedAt = row.original.teamStatusUpdatedAt ? new Date(row.original.teamStatusUpdatedAt).toLocaleDateString('de-DE') : null; if (value === null) { return ; } return (
{value ? 'Ja' : 'Nein'} {updatedAt &&

{updatedAt}

}
); }, 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]); 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, forceStatus } = {}) => { if (!authorizedFetch || !regionId) { return; } if (!force && !forceStatus && storesByRegion[String(regionId)]) { return; } if (!silent) { setStoresLoading(true); } setError(''); try { const params = new URLSearchParams(); if (force) { params.append('force', '1'); } if (forceStatus) { params.append('forceStatus', '1'); } const qs = params.toString(); const endpoint = `/api/store-watch/regions/${regionId}/stores${qs ? `?${qs}` : ''}`; 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(), statusMeta: data.statusMeta || null } })); } catch (err) { setError(`Betriebe konnten nicht geladen werden: ${err.message}`); } finally { if (!silent) { setStoresLoading(false); } } }, [authorizedFetch, storesByRegion] ); const fetchAllRegions = useCallback( async ({ force, forceStatus } = {}) => { if (!authorizedFetch || regions.length === 0) { return; } const targets = regions.filter( (region) => force || forceStatus || !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, forceStatus }); } } 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]); const handleStatusRefresh = useCallback(() => { if (selectedRegionId === 'all') { fetchAllRegions({ force: true, forceStatus: true }); } else if (selectedRegionId) { fetchStoresForRegion(selectedRegionId, { forceStatus: true }); } }, [selectedRegionId, fetchAllRegions, fetchStoresForRegion]); 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. {statusSummary}

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) => ( ))} ))} {table.getRowModel().rows.length === 0 && ( )}
{flexRender(header.column.columnDef.header, header.getContext())}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
Keine Betriebe entsprechen den aktuellen Filtern.
)}

Ü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;