Watch Stores mit Offen-Spalte
This commit is contained in:
163
server.js
163
server.js
@@ -13,6 +13,7 @@ const { readNotificationSettings, writeNotificationSettings } = require('./servi
|
|||||||
const notificationService = require('./services/notificationService');
|
const notificationService = require('./services/notificationService');
|
||||||
const { readStoreWatch, writeStoreWatch } = require('./services/storeWatchStore');
|
const { readStoreWatch, writeStoreWatch } = require('./services/storeWatchStore');
|
||||||
const { readPreferences, writePreferences } = require('./services/userPreferencesStore');
|
const { readPreferences, writePreferences } = require('./services/userPreferencesStore');
|
||||||
|
const { readStoreStatus, writeStoreStatus } = require('./services/storeStatusStore');
|
||||||
|
|
||||||
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
||||||
const adminEmail = (process.env.ADMIN_EMAIL || '').toLowerCase();
|
const adminEmail = (process.env.ADMIN_EMAIL || '').toLowerCase();
|
||||||
@@ -24,6 +25,35 @@ const storeRefreshJobs = new Map();
|
|||||||
const cachedStoreSnapshots = new Map();
|
const cachedStoreSnapshots = new Map();
|
||||||
const regionStoreCache = new Map();
|
const regionStoreCache = new Map();
|
||||||
const REGION_STORE_CACHE_MS = 15 * 60 * 1000;
|
const REGION_STORE_CACHE_MS = 15 * 60 * 1000;
|
||||||
|
const STORE_STATUS_MAX_AGE_MS = 60 * 24 * 60 * 60 * 1000;
|
||||||
|
const storeStatusCache = new Map();
|
||||||
|
|
||||||
|
(function bootstrapStoreStatusCache() {
|
||||||
|
try {
|
||||||
|
const cached = readStoreStatus();
|
||||||
|
Object.entries(cached || {}).forEach(([storeId, entry]) => {
|
||||||
|
if (entry && typeof entry === 'object') {
|
||||||
|
storeStatusCache.set(String(storeId), {
|
||||||
|
teamSearchStatus:
|
||||||
|
entry.teamSearchStatus === null || entry.teamSearchStatus === undefined
|
||||||
|
? null
|
||||||
|
: Number(entry.teamSearchStatus),
|
||||||
|
fetchedAt: Number(entry.fetchedAt) || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[STORE-STATUS] Bootstrap fehlgeschlagen:', error.message);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
function persistStoreStatusCache() {
|
||||||
|
const payload = {};
|
||||||
|
storeStatusCache.forEach((value, key) => {
|
||||||
|
payload[key] = value;
|
||||||
|
});
|
||||||
|
writeStoreStatus(payload);
|
||||||
|
}
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json({ limit: '1mb' }));
|
app.use(express.json({ limit: '1mb' }));
|
||||||
@@ -111,6 +141,90 @@ function setCachedRegionStores(regionId, payload) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCachedStoreStatus(storeId) {
|
||||||
|
return storeStatusCache.get(String(storeId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshStoreStatus(storeIds = [], cookieHeader, { force = false } = {}) {
|
||||||
|
if (!Array.isArray(storeIds) || storeIds.length === 0 || !cookieHeader) {
|
||||||
|
return { refreshed: 0 };
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
let refreshed = 0;
|
||||||
|
for (const id of storeIds) {
|
||||||
|
const storeId = String(id);
|
||||||
|
const entry = storeStatusCache.get(storeId);
|
||||||
|
const ageOk = entry && now - (entry.fetchedAt || 0) <= STORE_STATUS_MAX_AGE_MS;
|
||||||
|
if (!force && ageOk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const details = await foodsharingClient.fetchStoreDetails(storeId, cookieHeader);
|
||||||
|
const status = Number(details?.teamSearchStatus);
|
||||||
|
const normalized = Number.isFinite(status) ? status : null;
|
||||||
|
storeStatusCache.set(storeId, {
|
||||||
|
teamSearchStatus: normalized,
|
||||||
|
fetchedAt: now
|
||||||
|
});
|
||||||
|
refreshed += 1;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[STORE-STATUS] Status für Store ${storeId} konnte nicht aktualisiert werden:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (refreshed > 0) {
|
||||||
|
persistStoreStatusCache();
|
||||||
|
}
|
||||||
|
return { refreshed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enrichStoresWithTeamStatus(stores = [], cookieHeader, { forceRefresh = false } = {}) {
|
||||||
|
if (!Array.isArray(stores) || stores.length === 0) {
|
||||||
|
return { stores, statusMeta: { total: 0, refreshed: 0, fromCache: 0, missing: 0 } };
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
const staleIds = [];
|
||||||
|
let cacheHits = 0;
|
||||||
|
stores.forEach((store) => {
|
||||||
|
const entry = getCachedStoreStatus(store.id);
|
||||||
|
const fresh = entry && now - (entry.fetchedAt || 0) <= STORE_STATUS_MAX_AGE_MS;
|
||||||
|
if (entry && fresh && !forceRefresh) {
|
||||||
|
store.teamSearchStatus = entry.teamSearchStatus;
|
||||||
|
store.teamStatusUpdatedAt = entry.fetchedAt || null;
|
||||||
|
cacheHits += 1;
|
||||||
|
} else {
|
||||||
|
staleIds.push(store.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let refreshed = 0;
|
||||||
|
if (staleIds.length > 0) {
|
||||||
|
const result = await refreshStoreStatus(staleIds, cookieHeader, { force: forceRefresh });
|
||||||
|
refreshed = result.refreshed || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
stores.forEach((store) => {
|
||||||
|
if (store.teamSearchStatus === undefined) {
|
||||||
|
const entry = getCachedStoreStatus(store.id);
|
||||||
|
if (entry) {
|
||||||
|
store.teamSearchStatus = entry.teamSearchStatus;
|
||||||
|
store.teamStatusUpdatedAt = entry.fetchedAt || null;
|
||||||
|
} else {
|
||||||
|
store.teamSearchStatus = null;
|
||||||
|
store.teamStatusUpdatedAt = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusMeta = {
|
||||||
|
total: stores.length,
|
||||||
|
refreshed,
|
||||||
|
fromCache: cacheHits,
|
||||||
|
missing: stores.filter((store) => store.teamStatusUpdatedAt == null).length,
|
||||||
|
generatedAt: Date.now()
|
||||||
|
};
|
||||||
|
return { stores, statusMeta };
|
||||||
|
}
|
||||||
|
|
||||||
function isStoreCacheFresh(session) {
|
function isStoreCacheFresh(session) {
|
||||||
if (!session?.storesCache?.fetchedAt) {
|
if (!session?.storesCache?.fetchedAt) {
|
||||||
return false;
|
return false;
|
||||||
@@ -409,25 +523,46 @@ app.get('/api/store-watch/regions/:regionId/stores', requireAuth, async (req, re
|
|||||||
if (!regionId) {
|
if (!regionId) {
|
||||||
return res.status(400).json({ error: 'Region-ID fehlt' });
|
return res.status(400).json({ error: 'Region-ID fehlt' });
|
||||||
}
|
}
|
||||||
const forceRefresh = req.query.force === '1';
|
const forceRegionRefresh = req.query.force === '1';
|
||||||
if (!forceRefresh) {
|
const forceStatusRefresh = req.query.forceStatus === '1';
|
||||||
|
|
||||||
|
let basePayload = null;
|
||||||
|
if (!forceRegionRefresh) {
|
||||||
const cached = getCachedRegionStores(regionId);
|
const cached = getCachedRegionStores(regionId);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json(cached);
|
basePayload = cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const result = await foodsharingClient.fetchRegionStores(regionId, req.session.cookieHeader);
|
if (!basePayload) {
|
||||||
const payload = {
|
try {
|
||||||
total: Number(result?.total) || 0,
|
const result = await foodsharingClient.fetchRegionStores(regionId, req.session.cookieHeader);
|
||||||
stores: Array.isArray(result?.stores) ? result.stores : []
|
basePayload = {
|
||||||
};
|
total: Number(result?.total) || 0,
|
||||||
setCachedRegionStores(regionId, payload);
|
stores: Array.isArray(result?.stores) ? result.stores : []
|
||||||
res.json(payload);
|
};
|
||||||
} catch (error) {
|
setCachedRegionStores(regionId, basePayload);
|
||||||
console.error(`[STORE-WATCH] Stores für Region ${regionId} konnten nicht geladen werden:`, error.message);
|
} catch (error) {
|
||||||
res.status(500).json({ error: 'Betriebe konnten nicht geladen werden' });
|
console.error(`[STORE-WATCH] Stores für Region ${regionId} konnten nicht geladen werden:`, error.message);
|
||||||
|
return res.status(500).json({ error: 'Betriebe konnten nicht geladen werden' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredStores = basePayload.stores
|
||||||
|
.filter((store) => Number(store.cooperationStatus) === 5)
|
||||||
|
.map((store) => ({ ...store, id: String(store.id) }));
|
||||||
|
|
||||||
|
const { stores: enrichedStores, statusMeta } = await enrichStoresWithTeamStatus(
|
||||||
|
filteredStores,
|
||||||
|
req.session.cookieHeader,
|
||||||
|
{ forceRefresh: forceStatusRefresh }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
total: filteredStores.length,
|
||||||
|
stores: enrichedStores,
|
||||||
|
statusMeta
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/store-watch/subscriptions', requireAuth, (req, res) => {
|
app.get('/api/store-watch/subscriptions', requireAuth, (req, res) => {
|
||||||
|
|||||||
40
services/storeStatusStore.js
Normal file
40
services/storeStatusStore.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const STORE_STATUS_FILE = path.join(__dirname, '..', 'config', 'store-watch-status.json');
|
||||||
|
|
||||||
|
function ensureDir() {
|
||||||
|
const dir = path.dirname(STORE_STATUS_FILE);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStoreStatus() {
|
||||||
|
ensureDir();
|
||||||
|
if (!fs.existsSync(STORE_STATUS_FILE)) {
|
||||||
|
fs.writeFileSync(STORE_STATUS_FILE, JSON.stringify({}, null, 2));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(STORE_STATUS_FILE, 'utf8');
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[STORE-STATUS] Konnte Cache nicht lesen:', error.message);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStoreStatus(cache = {}) {
|
||||||
|
ensureDir();
|
||||||
|
fs.writeFileSync(STORE_STATUS_FILE, JSON.stringify(cache, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
readStoreStatus,
|
||||||
|
writeStoreStatus
|
||||||
|
};
|
||||||
@@ -144,6 +144,29 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
|
|
||||||
const activeRegionLabel = selectedRegionId === 'all' ? 'allen Regionen' : selectedRegion?.name || 'dieser Region';
|
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(() => {
|
const lastUpdatedAt = useMemo(() => {
|
||||||
if (selectedRegionId === 'all') {
|
if (selectedRegionId === 'all') {
|
||||||
const timestamps = regions
|
const timestamps = regions
|
||||||
@@ -179,7 +202,27 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
return regionEntry.stores;
|
return regionEntry.stores;
|
||||||
}, [regions, storesByRegion, selectedRegionId]);
|
}, [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 membershipMap = useMemo(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
@@ -197,7 +240,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
const storeId = String(store.id || store.storeId);
|
const storeId = String(store.id || store.storeId);
|
||||||
const existing = prev.find((entry) => entry.storeId === storeId);
|
const existing = prev.find((entry) => entry.storeId === storeId);
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (!store.isOpen || existing) {
|
if (existing) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
@@ -239,15 +282,20 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
const membership = membershipMap.has(String(store.id));
|
const membership = membershipMap.has(String(store.id));
|
||||||
const lat = Number(store.location?.lat);
|
const lat = Number(store.location?.lat);
|
||||||
const lon = Number(store.location?.lon);
|
const lon = Number(store.location?.lon);
|
||||||
|
const statusValue = store.teamSearchStatus === null || store.teamSearchStatus === undefined
|
||||||
|
? null
|
||||||
|
: Number(store.teamSearchStatus);
|
||||||
const distance =
|
const distance =
|
||||||
userLocation && Number.isFinite(lat) && Number.isFinite(lon)
|
userLocation && Number.isFinite(lat) && Number.isFinite(lon)
|
||||||
? haversineDistanceKm(userLocation.lat, userLocation.lon, lat, lon)
|
? haversineDistanceKm(userLocation.lat, userLocation.lon, lat, lon)
|
||||||
: null;
|
: null;
|
||||||
const isOpen = Number(store.cooperationStatus) === 5;
|
const isOpen = statusValue === 1;
|
||||||
return {
|
return {
|
||||||
...store,
|
...store,
|
||||||
membership,
|
membership,
|
||||||
distanceKm: distance,
|
distanceKm: distance,
|
||||||
|
teamStatusUpdatedAt: store.teamStatusUpdatedAt || null,
|
||||||
|
teamSearchStatus: statusValue,
|
||||||
isOpen
|
isOpen
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -364,16 +412,25 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ getValue }) => {
|
cell: ({ row, getValue }) => {
|
||||||
const value = 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 (
|
return (
|
||||||
<span
|
<div className="text-center">
|
||||||
className={`inline-flex items-center justify-center rounded px-2 py-1 text-xs font-semibold ${
|
<span
|
||||||
value ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
|
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>
|
{value ? 'Ja' : 'Nein'}
|
||||||
|
</span>
|
||||||
|
{updatedAt && <p className="text-[10px] text-gray-500 mt-0.5">{updatedAt}</p>}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
filterFn: (row, columnId, value) => {
|
filterFn: (row, columnId, value) => {
|
||||||
@@ -571,11 +628,11 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
}, [authorizedFetch]);
|
}, [authorizedFetch]);
|
||||||
|
|
||||||
const fetchStoresForRegion = useCallback(
|
const fetchStoresForRegion = useCallback(
|
||||||
async (regionId, { force, silent } = {}) => {
|
async (regionId, { force, silent, forceStatus } = {}) => {
|
||||||
if (!authorizedFetch || !regionId) {
|
if (!authorizedFetch || !regionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!force && storesByRegion[String(regionId)]) {
|
if (!force && !forceStatus && storesByRegion[String(regionId)]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
@@ -583,9 +640,15 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
}
|
}
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const endpoint = force
|
const params = new URLSearchParams();
|
||||||
? `/api/store-watch/regions/${regionId}/stores?force=1`
|
if (force) {
|
||||||
: `/api/store-watch/regions/${regionId}/stores`;
|
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);
|
const response = await authorizedFetch(endpoint);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
@@ -596,7 +659,8 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
[String(regionId)]: {
|
[String(regionId)]: {
|
||||||
total: Number(data.total) || 0,
|
total: Number(data.total) || 0,
|
||||||
stores: Array.isArray(data.stores) ? data.stores : [],
|
stores: Array.isArray(data.stores) ? data.stores : [],
|
||||||
fetchedAt: Date.now()
|
fetchedAt: Date.now(),
|
||||||
|
statusMeta: data.statusMeta || null
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -611,12 +675,12 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fetchAllRegions = useCallback(
|
const fetchAllRegions = useCallback(
|
||||||
async ({ force } = {}) => {
|
async ({ force, forceStatus } = {}) => {
|
||||||
if (!authorizedFetch || regions.length === 0) {
|
if (!authorizedFetch || regions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const targets = regions.filter(
|
const targets = regions.filter(
|
||||||
(region) => force || !storesByRegion[String(region.id)]
|
(region) => force || forceStatus || !storesByRegion[String(region.id)]
|
||||||
);
|
);
|
||||||
if (targets.length === 0) {
|
if (targets.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -625,7 +689,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
for (const region of targets) {
|
for (const region of targets) {
|
||||||
await fetchStoresForRegion(region.id, { force, silent: true });
|
await fetchStoresForRegion(region.id, { force, silent: true, forceStatus });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Betriebe konnten nicht geladen werden: ${err.message}`);
|
setError(`Betriebe konnten nicht geladen werden: ${err.message}`);
|
||||||
@@ -684,6 +748,14 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
loadSubscriptions();
|
loadSubscriptions();
|
||||||
}, [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) {
|
if (!authorizedFetch) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 max-w-4xl mx-auto">
|
<div className="p-4 max-w-4xl mx-auto">
|
||||||
@@ -735,7 +807,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => loadRegions()}
|
onClick={() => loadRegions()}
|
||||||
@@ -756,11 +828,20 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
|||||||
>
|
>
|
||||||
Betriebe aktualisieren
|
Betriebe aktualisieren
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mt-2 text-xs text-gray-500">
|
||||||
Es werden nur Regionen angezeigt, in denen du Mitglied mit Klassifikation 1 bist.
|
<span>Es werden nur Regionen angezeigt, in denen du Mitglied mit Klassifikation 1 bist.</span>
|
||||||
</p>
|
<span>{statusSummary}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user