diff --git a/.commitmessage b/.commitmessage index 8ef843a..07f235e 100644 --- a/.commitmessage +++ b/.commitmessage @@ -1,4 +1,4 @@ -feat: refresh stale last pickup information +feat: restrict admin areas and show mobile pickups -Track the successful member-list check time and refresh stale pickup information -asynchronously after the GUI loads without triggering dormant warnings. +Limit monitoring and journal access to admins, reduce regular-user navigation, +and show the latest pickup directly in mobile configuration cards. diff --git a/server.js b/server.js index 9bb1b58..3be2559 100644 --- a/server.js +++ b/server.js @@ -1020,7 +1020,7 @@ app.get('/api/location/nearest-store', requireAuth, async (req, res) => { } }); -app.get('/api/store-watch/regions', requireAuth, async (req, res) => { +app.get('/api/store-watch/regions', requireAuth, requireAdmin, async (req, res) => { try { const details = await fetchProfileWithCache(req.session); const regions = Array.isArray(details?.regions) @@ -1033,7 +1033,7 @@ app.get('/api/store-watch/regions', requireAuth, async (req, res) => { } }); -app.get('/api/store-watch/regions/:regionId/stores', requireAuth, async (req, res) => { +app.get('/api/store-watch/regions/:regionId/stores', requireAuth, requireAdmin, async (req, res) => { const { regionId } = req.params; if (!regionId) { return res.status(400).json({ error: 'Region-ID fehlt' }); @@ -1086,12 +1086,12 @@ app.get('/api/store-watch/regions/:regionId/stores', requireAuth, async (req, re }); }); -app.get('/api/store-watch/subscriptions', requireAuth, (req, res) => { +app.get('/api/store-watch/subscriptions', requireAuth, requireAdmin, (req, res) => { const stores = readStoreWatch(req.session.profile.id); res.json({ stores }); }); -app.post('/api/store-watch/subscriptions', requireAuth, (req, res) => { +app.post('/api/store-watch/subscriptions', requireAuth, requireAdmin, (req, res) => { if (!req.body || !Array.isArray(req.body.stores)) { return res.status(400).json({ error: 'Erwartet eine Liste von Betrieben' }); } @@ -1121,7 +1121,7 @@ app.post('/api/store-watch/subscriptions', requireAuth, (req, res) => { res.json({ success: true, stores: persisted }); }); -app.post('/api/store-watch/check', requireAuth, async (req, res) => { +app.post('/api/store-watch/check', requireAuth, requireAdmin, async (req, res) => { try { maintenanceMode.ensureInactive('Ad-hoc-Store-Watch ist während des Wartungsmodus deaktiviert.'); const settings = adminConfig.readSettings(); @@ -1191,7 +1191,7 @@ app.post('/api/user/preferences/location', requireAuth, (req, res) => { res.json({ location: updated.location }); }); -app.get('/api/journal', requireAuth, (req, res) => { +app.get('/api/journal', requireAuth, requireAdmin, (req, res) => { const entries = readJournal(req.session.profile.id); const normalized = entries.map((entry) => ({ ...entry, @@ -1205,7 +1205,7 @@ app.get('/api/journal', requireAuth, (req, res) => { res.json(normalized); }); -app.post('/api/journal', requireAuth, (req, res) => { +app.post('/api/journal', requireAuth, requireAdmin, (req, res) => { const profileId = req.session.profile.id; const { storeId, storeName, pickupDate, note, reminder, images } = req.body || {}; if (!storeId || !pickupDate) { @@ -1276,7 +1276,7 @@ app.post('/api/journal', requireAuth, (req, res) => { }); }); -app.put('/api/journal/:id', requireAuth, (req, res) => { +app.put('/api/journal/:id', requireAuth, requireAdmin, (req, res) => { const profileId = req.session.profile.id; const { storeId, storeName, pickupDate, note, reminder, images, keepImageIds } = req.body || {}; if (!storeId || !pickupDate) { @@ -1358,7 +1358,7 @@ app.put('/api/journal/:id', requireAuth, (req, res) => { }); }); -app.delete('/api/journal/:id', requireAuth, (req, res) => { +app.delete('/api/journal/:id', requireAuth, requireAdmin, (req, res) => { const profileId = req.session.profile.id; const entries = readJournal(profileId); const filtered = entries.filter((entry) => entry.id !== req.params.id); @@ -1373,7 +1373,7 @@ app.delete('/api/journal/:id', requireAuth, (req, res) => { res.json({ success: true }); }); -app.get('/api/journal/images/:imageId', requireAuth, (req, res) => { +app.get('/api/journal/images/:imageId', requireAuth, requireAdmin, (req, res) => { const profileId = req.session.profile.id; const entries = readJournal(profileId); const imageEntry = entries diff --git a/services/pickupScheduler.js b/services/pickupScheduler.js index 6c8ede5..4aaa3a2 100644 --- a/services/pickupScheduler.js +++ b/services/pickupScheduler.js @@ -1037,7 +1037,7 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option return []; } const session = sessionStore.get(sessionId); - if (!session?.profile?.id) { + if (!session?.profile?.id || !session.isAdmin) { return []; } const watchers = readStoreWatch(session.profile.id); @@ -1132,7 +1132,7 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option function scheduleStoreWatchers(sessionId, settings) { const effectiveSettings = settings || DEFAULT_SETTINGS; const session = sessionStore.get(sessionId); - if (!session?.profile?.id) { + if (!session?.profile?.id || !session.isAdmin) { return false; } const watchers = readStoreWatch(session.profile.id); diff --git a/src/App.js b/src/App.js index 4984d8c..c5fafdf 100644 --- a/src/App.js +++ b/src/App.js @@ -910,22 +910,24 @@ function App() { setNotificationPanelOpen((prev) => !prev)} - notificationProps={sharedNotificationProps} - isAdmin={Boolean(session?.isAdmin)} - /> + session?.isAdmin ? ( + setNotificationPanelOpen((prev) => !prev)} + notificationProps={sharedNotificationProps} + isAdmin + /> + ) : } /> } + element={session?.isAdmin ? : } /> { + const date = new Date(value); + if (!value || Number.isNaN(date.getTime())) { + return { relative: 'Unbekannt', exact: null }; + } + return { + relative: formatDistanceToNowStrict(date, { addSuffix: true, locale: de }), + exact: date.toLocaleDateString('de-DE') + }; +}; + const MobileConfigList = ({ entries, regularPickupMap, @@ -84,6 +97,7 @@ const MobileConfigList = ({ {filteredEntries.map((entry) => { const range = entry.desiredDateRange || (entry.desiredDate ? { start: entry.desiredDate, end: entry.desiredDate } : null); const slots = formatRegularPickup(regularPickupMap?.[entry.id]); + const lastPickup = formatLastPickup(entry.lastPickupAt); return (
{entry.label || `Store ${entry.id}`}

#{entry.id}

{slots &&

Slots: {slots}

} +

+ Letzte Abholung: {lastPickup.relative} +

{ expect(screen.queryByText('Donat')).not.toBeInTheDocument(); }); + it('shows the last pickup directly in the card header', () => { + render(); + + expect(screen.getAllByText(/Letzte Abholung:/)[0]).toBeInTheDocument(); + }); + it('uses the existing configuration callbacks', () => { render(); diff --git a/src/components/NavigationTabs.css b/src/components/NavigationTabs.css index a12816a..afc3085 100644 --- a/src/components/NavigationTabs.css +++ b/src/components/NavigationTabs.css @@ -2,6 +2,10 @@ margin-bottom: 1rem; } +.app-navigation--compact { + margin-bottom: 0; +} + .app-navigation__mobile-header, .app-navigation__mobile-menu { display: none; @@ -37,6 +41,10 @@ margin: 0 -0.25rem 0.75rem; } + .app-navigation--compact { + margin-bottom: 0.35rem; + } + .app-navigation__desktop { display: none; } diff --git a/src/components/NavigationTabs.js b/src/components/NavigationTabs.js index a234617..1be73f6 100644 --- a/src/components/NavigationTabs.js +++ b/src/components/NavigationTabs.js @@ -17,17 +17,16 @@ const NavigationTabs = ({ const [mobileOpen, setMobileOpen] = useState(false); const tabs = useMemo(() => { - const items = [ - { to: '/', label: 'Slots buchen' }, - { to: '/store-watch', label: 'Betriebs-Monitoring' }, - { to: '/journal', label: 'Abhol-Journal' } - ]; + const items = [{ to: '/', label: 'Slots buchen' }]; if (isAdmin) { + items.push({ to: '/store-watch', label: 'Betriebs-Monitoring' }); + items.push({ to: '/journal', label: 'Abhol-Journal' }); items.push({ to: '/debug', label: 'Debug' }); items.push({ to: '/admin', label: 'Admin' }); } return items; }, [isAdmin]); + const hasNavigation = tabs.length > 1; const handleClick = (event, to) => { event.preventDefault(); @@ -68,13 +67,13 @@ const NavigationTabs = ({ }; return ( -