feat: restrict admin areas and show mobile pickups
Limit monitoring and journal access to admins, reduce regular-user navigation, and show the latest pickup directly in mobile configuration cards.
This commit is contained in:
@@ -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
|
Limit monitoring and journal access to admins, reduce regular-user navigation,
|
||||||
asynchronously after the GUI loads without triggering dormant warnings.
|
and show the latest pickup directly in mobile configuration cards.
|
||||||
|
|||||||
20
server.js
20
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 {
|
try {
|
||||||
const details = await fetchProfileWithCache(req.session);
|
const details = await fetchProfileWithCache(req.session);
|
||||||
const regions = Array.isArray(details?.regions)
|
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;
|
const { regionId } = req.params;
|
||||||
if (!regionId) {
|
if (!regionId) {
|
||||||
return res.status(400).json({ error: 'Region-ID fehlt' });
|
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);
|
const stores = readStoreWatch(req.session.profile.id);
|
||||||
res.json({ stores });
|
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)) {
|
if (!req.body || !Array.isArray(req.body.stores)) {
|
||||||
return res.status(400).json({ error: 'Erwartet eine Liste von Betrieben' });
|
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 });
|
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 {
|
try {
|
||||||
maintenanceMode.ensureInactive('Ad-hoc-Store-Watch ist während des Wartungsmodus deaktiviert.');
|
maintenanceMode.ensureInactive('Ad-hoc-Store-Watch ist während des Wartungsmodus deaktiviert.');
|
||||||
const settings = adminConfig.readSettings();
|
const settings = adminConfig.readSettings();
|
||||||
@@ -1191,7 +1191,7 @@ app.post('/api/user/preferences/location', requireAuth, (req, res) => {
|
|||||||
res.json({ location: updated.location });
|
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 entries = readJournal(req.session.profile.id);
|
||||||
const normalized = entries.map((entry) => ({
|
const normalized = entries.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
@@ -1205,7 +1205,7 @@ app.get('/api/journal', requireAuth, (req, res) => {
|
|||||||
res.json(normalized);
|
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 profileId = req.session.profile.id;
|
||||||
const { storeId, storeName, pickupDate, note, reminder, images } = req.body || {};
|
const { storeId, storeName, pickupDate, note, reminder, images } = req.body || {};
|
||||||
if (!storeId || !pickupDate) {
|
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 profileId = req.session.profile.id;
|
||||||
const { storeId, storeName, pickupDate, note, reminder, images, keepImageIds } = req.body || {};
|
const { storeId, storeName, pickupDate, note, reminder, images, keepImageIds } = req.body || {};
|
||||||
if (!storeId || !pickupDate) {
|
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 profileId = req.session.profile.id;
|
||||||
const entries = readJournal(profileId);
|
const entries = readJournal(profileId);
|
||||||
const filtered = entries.filter((entry) => entry.id !== req.params.id);
|
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 });
|
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 profileId = req.session.profile.id;
|
||||||
const entries = readJournal(profileId);
|
const entries = readJournal(profileId);
|
||||||
const imageEntry = entries
|
const imageEntry = entries
|
||||||
|
|||||||
@@ -1037,7 +1037,7 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const session = sessionStore.get(sessionId);
|
const session = sessionStore.get(sessionId);
|
||||||
if (!session?.profile?.id) {
|
if (!session?.profile?.id || !session.isAdmin) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const watchers = readStoreWatch(session.profile.id);
|
const watchers = readStoreWatch(session.profile.id);
|
||||||
@@ -1132,7 +1132,7 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option
|
|||||||
function scheduleStoreWatchers(sessionId, settings) {
|
function scheduleStoreWatchers(sessionId, settings) {
|
||||||
const effectiveSettings = settings || DEFAULT_SETTINGS;
|
const effectiveSettings = settings || DEFAULT_SETTINGS;
|
||||||
const session = sessionStore.get(sessionId);
|
const session = sessionStore.get(sessionId);
|
||||||
if (!session?.profile?.id) {
|
if (!session?.profile?.id || !session.isAdmin) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const watchers = readStoreWatch(session.profile.id);
|
const watchers = readStoreWatch(session.profile.id);
|
||||||
|
|||||||
26
src/App.js
26
src/App.js
@@ -910,22 +910,24 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="/store-watch"
|
path="/store-watch"
|
||||||
element={
|
element={
|
||||||
<StoreWatchPage
|
session?.isAdmin ? (
|
||||||
authorizedFetch={authorizedFetch}
|
<StoreWatchPage
|
||||||
knownStores={stores}
|
authorizedFetch={authorizedFetch}
|
||||||
userLocation={userLocationWithLabel}
|
knownStores={stores}
|
||||||
locationLoading={preferencesLoading}
|
userLocation={userLocationWithLabel}
|
||||||
locationError={preferencesError}
|
locationLoading={preferencesLoading}
|
||||||
notificationPanelOpen={notificationPanelOpen}
|
locationError={preferencesError}
|
||||||
onToggleNotificationPanel={() => setNotificationPanelOpen((prev) => !prev)}
|
notificationPanelOpen={notificationPanelOpen}
|
||||||
notificationProps={sharedNotificationProps}
|
onToggleNotificationPanel={() => setNotificationPanelOpen((prev) => !prev)}
|
||||||
isAdmin={Boolean(session?.isAdmin)}
|
notificationProps={sharedNotificationProps}
|
||||||
/>
|
isAdmin
|
||||||
|
/>
|
||||||
|
) : <Navigate to="/" replace />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/journal"
|
path="/journal"
|
||||||
element={<JournalPage authorizedFetch={authorizedFetch} stores={stores} />}
|
element={session?.isAdmin ? <JournalPage authorizedFetch={authorizedFetch} stores={stores} /> : <Navigate to="/" replace />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/debug"
|
path="/debug"
|
||||||
|
|||||||
@@ -121,7 +121,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-config-card__id,
|
.mobile-config-card__id,
|
||||||
.mobile-config-card__slots {
|
.mobile-config-card__slots,
|
||||||
|
.mobile-config-card__last-pickup {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
@@ -130,6 +131,16 @@
|
|||||||
margin-top: 0.45rem !important;
|
margin-top: 0.45rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-config-card__last-pickup {
|
||||||
|
margin-top: 0.35rem !important;
|
||||||
|
color: #475569 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-config-card__last-pickup strong {
|
||||||
|
color: #334155;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-config-card__store-link {
|
.mobile-config-card__store-link {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { formatDistanceToNowStrict } from 'date-fns';
|
||||||
|
import { de } from 'date-fns/locale';
|
||||||
import './MobileConfigList.css';
|
import './MobileConfigList.css';
|
||||||
|
|
||||||
const FILTER_OPTIONS = [
|
const FILTER_OPTIONS = [
|
||||||
@@ -7,6 +9,17 @@ const FILTER_OPTIONS = [
|
|||||||
{ value: 'inactive', label: 'Inaktiv' }
|
{ value: 'inactive', label: 'Inaktiv' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const formatLastPickup = (value) => {
|
||||||
|
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 = ({
|
const MobileConfigList = ({
|
||||||
entries,
|
entries,
|
||||||
regularPickupMap,
|
regularPickupMap,
|
||||||
@@ -84,6 +97,7 @@ const MobileConfigList = ({
|
|||||||
{filteredEntries.map((entry) => {
|
{filteredEntries.map((entry) => {
|
||||||
const range = entry.desiredDateRange || (entry.desiredDate ? { start: entry.desiredDate, end: entry.desiredDate } : null);
|
const range = entry.desiredDateRange || (entry.desiredDate ? { start: entry.desiredDate, end: entry.desiredDate } : null);
|
||||||
const slots = formatRegularPickup(regularPickupMap?.[entry.id]);
|
const slots = formatRegularPickup(regularPickupMap?.[entry.id]);
|
||||||
|
const lastPickup = formatLastPickup(entry.lastPickupAt);
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
@@ -95,6 +109,9 @@ const MobileConfigList = ({
|
|||||||
<h3 className="mobile-config-card__title">{entry.label || `Store ${entry.id}`}</h3>
|
<h3 className="mobile-config-card__title">{entry.label || `Store ${entry.id}`}</h3>
|
||||||
<p className="mobile-config-card__id">#{entry.id}</p>
|
<p className="mobile-config-card__id">#{entry.id}</p>
|
||||||
{slots && <p className="mobile-config-card__slots">Slots: {slots}</p>}
|
{slots && <p className="mobile-config-card__slots">Slots: {slots}</p>}
|
||||||
|
<p className="mobile-config-card__last-pickup" title={lastPickup.exact || undefined}>
|
||||||
|
Letzte Abholung: <strong>{lastPickup.relative}</strong>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href={`https://foodsharing.de/store/${entry.id}`}
|
href={`https://foodsharing.de/store/${entry.id}`}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const entry = {
|
|||||||
autoDeactivate: true,
|
autoDeactivate: true,
|
||||||
checkProfileId: true,
|
checkProfileId: true,
|
||||||
onlyNotify: false,
|
onlyNotify: false,
|
||||||
skipDormantCheck: false
|
skipDormantCheck: false,
|
||||||
|
lastPickupAt: '2026-08-01T10:00:00.000Z'
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
@@ -48,6 +49,12 @@ describe('MobileConfigList', () => {
|
|||||||
expect(screen.queryByText('Donat')).not.toBeInTheDocument();
|
expect(screen.queryByText('Donat')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the last pickup directly in the card header', () => {
|
||||||
|
render(<MobileConfigList {...props} />);
|
||||||
|
|
||||||
|
expect(screen.getAllByText(/Letzte Abholung:/)[0]).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('uses the existing configuration callbacks', () => {
|
it('uses the existing configuration callbacks', () => {
|
||||||
render(<MobileConfigList {...props} />);
|
render(<MobileConfigList {...props} />);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-navigation--compact {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.app-navigation__mobile-header,
|
.app-navigation__mobile-header,
|
||||||
.app-navigation__mobile-menu {
|
.app-navigation__mobile-menu {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -37,6 +41,10 @@
|
|||||||
margin: 0 -0.25rem 0.75rem;
|
margin: 0 -0.25rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-navigation--compact {
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
.app-navigation__desktop {
|
.app-navigation__desktop {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,16 @@ const NavigationTabs = ({
|
|||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
const tabs = useMemo(() => {
|
const tabs = useMemo(() => {
|
||||||
const items = [
|
const items = [{ to: '/', label: 'Slots buchen' }];
|
||||||
{ to: '/', label: 'Slots buchen' },
|
|
||||||
{ to: '/store-watch', label: 'Betriebs-Monitoring' },
|
|
||||||
{ to: '/journal', label: 'Abhol-Journal' }
|
|
||||||
];
|
|
||||||
if (isAdmin) {
|
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: '/debug', label: 'Debug' });
|
||||||
items.push({ to: '/admin', label: 'Admin' });
|
items.push({ to: '/admin', label: 'Admin' });
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}, [isAdmin]);
|
}, [isAdmin]);
|
||||||
|
const hasNavigation = tabs.length > 1;
|
||||||
|
|
||||||
const handleClick = (event, to) => {
|
const handleClick = (event, to) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -68,13 +67,13 @@ const NavigationTabs = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="app-navigation" aria-label="Navigation">
|
<nav className={`app-navigation ${hasNavigation ? '' : 'app-navigation--compact'}`} aria-label="Navigation">
|
||||||
<div className="app-navigation__mobile-header">
|
<div className="app-navigation__mobile-header">
|
||||||
<span className="app-navigation__product-name">Foodsharing Manager</span>
|
<span className="app-navigation__product-name">Foodsharing Manager</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="app-navigation__menu-toggle"
|
className="app-navigation__menu-toggle"
|
||||||
aria-label={mobileOpen ? 'Navigation schließen' : 'Navigation öffnen'}
|
aria-label={mobileOpen ? 'Menü schließen' : 'Menü öffnen'}
|
||||||
aria-expanded={mobileOpen}
|
aria-expanded={mobileOpen}
|
||||||
aria-controls="mobile-navigation"
|
aria-controls="mobile-navigation"
|
||||||
onClick={() => setMobileOpen((open) => !open)}
|
onClick={() => setMobileOpen((open) => !open)}
|
||||||
@@ -98,9 +97,11 @@ const NavigationTabs = ({
|
|||||||
|
|
||||||
{mobileOpen && (
|
{mobileOpen && (
|
||||||
<div id="mobile-navigation" className="app-navigation__mobile-menu">
|
<div id="mobile-navigation" className="app-navigation__mobile-menu">
|
||||||
<div className="app-navigation__mobile-links">
|
{hasNavigation && (
|
||||||
{tabs.map((tab) => renderLink(tab, 'app-navigation__mobile-link'))}
|
<div className="app-navigation__mobile-links">
|
||||||
</div>
|
{tabs.map((tab) => renderLink(tab, 'app-navigation__mobile-link'))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{location.pathname === '/' && (onRefresh || onToggleNotifications) && (
|
{location.pathname === '/' && (onRefresh || onToggleNotifications) && (
|
||||||
<div className="app-navigation__tools">
|
<div className="app-navigation__tools">
|
||||||
<span>Werkzeuge</span>
|
<span>Werkzeuge</span>
|
||||||
@@ -139,9 +140,11 @@ const NavigationTabs = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="app-navigation__desktop">
|
{hasNavigation && (
|
||||||
{tabs.map((tab) => renderLink(tab, 'app-navigation__desktop-link'))}
|
<div className="app-navigation__desktop">
|
||||||
</div>
|
{tabs.map((tab) => renderLink(tab, 'app-navigation__desktop-link'))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ describe('NavigationTabs', () => {
|
|||||||
const onLogout = jest.fn();
|
const onLogout = jest.fn();
|
||||||
render(<NavigationTabs profileName="Meik" onLogout={onLogout} />);
|
render(<NavigationTabs profileName="Meik" onLogout={onLogout} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Navigation öffnen' }));
|
fireEvent.click(screen.getByRole('button', { name: 'Menü öffnen' }));
|
||||||
|
|
||||||
expect(screen.getByText('Meik')).toBeInTheDocument();
|
expect(screen.getByText('Meik')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Logout' }));
|
fireEvent.click(screen.getByRole('button', { name: 'Logout' }));
|
||||||
@@ -15,9 +15,9 @@ describe('NavigationTabs', () => {
|
|||||||
|
|
||||||
it('uses the existing protected navigation callback', () => {
|
it('uses the existing protected navigation callback', () => {
|
||||||
const onProtectedNavigate = jest.fn();
|
const onProtectedNavigate = jest.fn();
|
||||||
render(<NavigationTabs onProtectedNavigate={onProtectedNavigate} />);
|
render(<NavigationTabs isAdmin onProtectedNavigate={onProtectedNavigate} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Navigation öffnen' }));
|
fireEvent.click(screen.getByRole('button', { name: 'Menü öffnen' }));
|
||||||
fireEvent.click(screen.getAllByText('Betriebs-Monitoring')[0]);
|
fireEvent.click(screen.getAllByText('Betriebs-Monitoring')[0]);
|
||||||
|
|
||||||
expect(onProtectedNavigate).toHaveBeenCalledWith(
|
expect(onProtectedNavigate).toHaveBeenCalledWith(
|
||||||
@@ -31,14 +31,22 @@ describe('NavigationTabs', () => {
|
|||||||
const onToggleNotifications = jest.fn();
|
const onToggleNotifications = jest.fn();
|
||||||
render(<NavigationTabs onRefresh={onRefresh} onToggleNotifications={onToggleNotifications} />);
|
render(<NavigationTabs onRefresh={onRefresh} onToggleNotifications={onToggleNotifications} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Navigation öffnen' }));
|
fireEvent.click(screen.getByRole('button', { name: 'Menü öffnen' }));
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Betriebe aktualisieren' }));
|
fireEvent.click(screen.getByRole('button', { name: 'Betriebe aktualisieren' }));
|
||||||
|
|
||||||
expect(onRefresh).toHaveBeenCalledTimes(1);
|
expect(onRefresh).toHaveBeenCalledTimes(1);
|
||||||
expect(screen.queryByText('Werkzeuge')).not.toBeInTheDocument();
|
expect(screen.queryByText('Werkzeuge')).not.toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Navigation öffnen' }));
|
fireEvent.click(screen.getByRole('button', { name: 'Menü öffnen' }));
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Benachrichtigungen' }));
|
fireEvent.click(screen.getByRole('button', { name: 'Benachrichtigungen' }));
|
||||||
expect(onToggleNotifications).toHaveBeenCalledTimes(1);
|
expect(onToggleNotifications).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides admin-only sections and the desktop navigation for regular users', () => {
|
||||||
|
render(<NavigationTabs />);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Betriebs-Monitoring')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Abhol-Journal')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Slots buchen')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user