Watch Stores mit Offen-Spalte
This commit is contained in:
@@ -144,6 +144,29 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
|
||||
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
|
||||
@@ -179,7 +202,27 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
return regionEntry.stores;
|
||||
}, [regions, storesByRegion, selectedRegionId]);
|
||||
|
||||
const regionStores = useMemo(() => currentStores, [currentStores]);
|
||||
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();
|
||||
@@ -197,7 +240,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
const storeId = String(store.id || store.storeId);
|
||||
const existing = prev.find((entry) => entry.storeId === storeId);
|
||||
if (checked) {
|
||||
if (!store.isOpen || existing) {
|
||||
if (existing) {
|
||||
return prev;
|
||||
}
|
||||
setDirty(true);
|
||||
@@ -239,15 +282,20 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
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 = Number(store.cooperationStatus) === 5;
|
||||
const isOpen = statusValue === 1;
|
||||
return {
|
||||
...store,
|
||||
membership,
|
||||
distanceKm: distance,
|
||||
teamStatusUpdatedAt: store.teamStatusUpdatedAt || null,
|
||||
teamSearchStatus: statusValue,
|
||||
isOpen
|
||||
};
|
||||
}),
|
||||
@@ -364,16 +412,25 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
cell: ({ row, getValue }) => {
|
||||
const value = getValue();
|
||||
const updatedAt = row.original.teamStatusUpdatedAt
|
||||
? new Date(row.original.teamStatusUpdatedAt).toLocaleDateString('de-DE')
|
||||
: null;
|
||||
if (value === null) {
|
||||
return <span className="text-sm text-gray-500">–</span>;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center justify-center rounded px-2 py-1 text-xs font-semibold ${
|
||||
value ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{value ? 'Ja' : 'Nein'}
|
||||
</span>
|
||||
<div className="text-center">
|
||||
<span
|
||||
className={`inline-flex items-center justify-center rounded px-2 py-1 text-xs font-semibold ${
|
||||
value ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{value ? 'Ja' : 'Nein'}
|
||||
</span>
|
||||
{updatedAt && <p className="text-[10px] text-gray-500 mt-0.5">{updatedAt}</p>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
filterFn: (row, columnId, value) => {
|
||||
@@ -571,11 +628,11 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
}, [authorizedFetch]);
|
||||
|
||||
const fetchStoresForRegion = useCallback(
|
||||
async (regionId, { force, silent } = {}) => {
|
||||
async (regionId, { force, silent, forceStatus } = {}) => {
|
||||
if (!authorizedFetch || !regionId) {
|
||||
return;
|
||||
}
|
||||
if (!force && storesByRegion[String(regionId)]) {
|
||||
if (!force && !forceStatus && storesByRegion[String(regionId)]) {
|
||||
return;
|
||||
}
|
||||
if (!silent) {
|
||||
@@ -583,9 +640,15 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
}
|
||||
setError('');
|
||||
try {
|
||||
const endpoint = force
|
||||
? `/api/store-watch/regions/${regionId}/stores?force=1`
|
||||
: `/api/store-watch/regions/${regionId}/stores`;
|
||||
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}`);
|
||||
@@ -596,7 +659,8 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
[String(regionId)]: {
|
||||
total: Number(data.total) || 0,
|
||||
stores: Array.isArray(data.stores) ? data.stores : [],
|
||||
fetchedAt: Date.now()
|
||||
fetchedAt: Date.now(),
|
||||
statusMeta: data.statusMeta || null
|
||||
}
|
||||
}));
|
||||
} catch (err) {
|
||||
@@ -611,12 +675,12 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
);
|
||||
|
||||
const fetchAllRegions = useCallback(
|
||||
async ({ force } = {}) => {
|
||||
async ({ force, forceStatus } = {}) => {
|
||||
if (!authorizedFetch || regions.length === 0) {
|
||||
return;
|
||||
}
|
||||
const targets = regions.filter(
|
||||
(region) => force || !storesByRegion[String(region.id)]
|
||||
(region) => force || forceStatus || !storesByRegion[String(region.id)]
|
||||
);
|
||||
if (targets.length === 0) {
|
||||
return;
|
||||
@@ -625,7 +689,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
setError('');
|
||||
try {
|
||||
for (const region of targets) {
|
||||
await fetchStoresForRegion(region.id, { force, silent: true });
|
||||
await fetchStoresForRegion(region.id, { force, silent: true, forceStatus });
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Betriebe konnten nicht geladen werden: ${err.message}`);
|
||||
@@ -684,6 +748,14 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
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 (
|
||||
<div className="p-4 max-w-4xl mx-auto">
|
||||
@@ -735,7 +807,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadRegions()}
|
||||
@@ -756,11 +828,20 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
>
|
||||
Betriebe aktualisieren
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStatusRefresh}
|
||||
className="px-4 py-2 border rounded-md text-sm bg-white hover:bg-gray-100"
|
||||
disabled={regionLoading || (selectedRegionId === 'all' && regions.length === 0)}
|
||||
>
|
||||
Team-Status aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Es werden nur Regionen angezeigt, in denen du Mitglied mit Klassifikation 1 bist.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mt-2 text-xs text-gray-500">
|
||||
<span>Es werden nur Regionen angezeigt, in denen du Mitglied mit Klassifikation 1 bist.</span>
|
||||
<span>{statusSummary}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
|
||||
Reference in New Issue
Block a user