feat(admin): show paused sessions in live view; reset to live on logout
- Live cards freeze the worked timer at paused_at and show an amber "Gepauzeerd" badge plus a "Pauze H:MM:SS" total when paused. - AuthContext.signOut resets the path to / so the next admin login lands on Live rather than the tab it logged out from.
This commit is contained in:
@@ -92,4 +92,23 @@ describe('AuthContext admin gate', () => {
|
|||||||
expect(getToken()).toBeNull();
|
expect(getToken()).toBeNull();
|
||||||
expect(screen.getByTestId('authed')).toHaveTextContent('false');
|
expect(screen.getByTestId('authed')).toHaveTextContent('false');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('signOut resets the path to / so the next login lands on Live', async () => {
|
||||||
|
mockedFetchMe.mockResolvedValue({
|
||||||
|
user: { id: 'a1', email: 'admin@solelog.local', name: 'Admin', role: 'admin' },
|
||||||
|
});
|
||||||
|
window.history.replaceState(null, '', '/activities');
|
||||||
|
expect(window.location.pathname).toBe('/activities');
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderHarness();
|
||||||
|
await user.click(screen.getByRole('button', { name: 'go' }));
|
||||||
|
await waitFor(() => expect(screen.getByTestId('authed')).toHaveTextContent('true'));
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'uit' }));
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByTestId('authed')).toHaveTextContent('false'));
|
||||||
|
expect(window.location.pathname).toBe('/');
|
||||||
|
expect(getToken()).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const signOut = useCallback(() => {
|
const signOut = useCallback(() => {
|
||||||
|
// Reset the path to / so the next authed mount lands on Live, not whatever tab
|
||||||
|
// (e.g. Account/Activities) the admin happened to log out from.
|
||||||
|
window.history.replaceState(null, '', '/');
|
||||||
clearToken();
|
clearToken();
|
||||||
setIsAuthed(false);
|
setIsAuthed(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ function makeSession(over: Partial<WorkSession>): WorkSession {
|
|||||||
start_time: new Date(Date.now() - 65_000).toISOString(),
|
start_time: new Date(Date.now() - 65_000).toISOString(),
|
||||||
end_time: null,
|
end_time: null,
|
||||||
duration_seconds: null,
|
duration_seconds: null,
|
||||||
|
paused_seconds: 0,
|
||||||
|
paused_at: null,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
source: 'app',
|
source: 'app',
|
||||||
notes: null,
|
notes: null,
|
||||||
|
|||||||
@@ -49,7 +49,13 @@ export default function Live() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LiveCard({ session, now }: { session: WorkSession; now: number }) {
|
function LiveCard({ session, now }: { session: WorkSession; now: number }) {
|
||||||
const elapsed = formatTime((now - Date.parse(session.start_time)) / 1000);
|
// While paused the worked timer freezes at the pause moment; otherwise it counts to now.
|
||||||
|
// Worked = (base - start) - paused_seconds, where base is the pause moment when paused.
|
||||||
|
const base = session.paused_at ? Date.parse(session.paused_at) : now;
|
||||||
|
const worked = Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor((base - Date.parse(session.start_time)) / 1000) - session.paused_seconds
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<article className="live-card">
|
<article className="live-card">
|
||||||
<div className="live-card-head">
|
<div className="live-card-head">
|
||||||
@@ -58,7 +64,11 @@ function LiveCard({ session, now }: { session: WorkSession; now: number }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="live-activity">{session.activity_name ?? 'Onbekende handeling'}</div>
|
<div className="live-activity">{session.activity_name ?? 'Onbekende handeling'}</div>
|
||||||
<div className="live-meta">{session.pair_count} zolen</div>
|
<div className="live-meta">{session.pair_count} zolen</div>
|
||||||
<div className="live-timer">{elapsed}</div>
|
<div className="live-timer">{formatTime(worked)}</div>
|
||||||
|
{session.paused_at && <span className="live-badge-paused">Gepauzeerd</span>}
|
||||||
|
{session.paused_seconds > 0 && (
|
||||||
|
<span className="live-paused-total">Pauze {formatTime(session.paused_seconds)}</span>
|
||||||
|
)}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,3 +404,19 @@ body {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.live-badge-paused {
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--amber);
|
||||||
|
background: #fef3c7;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-paused-total {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user