feat(admin): stop/cancel an active session from the live view

This commit is contained in:
Bas van Rossem
2026-06-17 22:47:05 +02:00
parent 69b46bee04
commit f1ec249ee7
3 changed files with 91 additions and 2 deletions

View File

@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { WorkSession } from '@solelog/shared';
@@ -120,6 +121,58 @@ describe('Live', () => {
expect(await screen.findByText('Pauze 00:02:05')).toBeInTheDocument();
});
it('shows Stop and Annuleer buttons on an active card', async () => {
mockApiFetch.mockResolvedValue([makeSession({ id: 7, user_name: 'Jan' })]);
renderLive();
expect(await screen.findByText('Jan')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Stop' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Annuleer' })).toBeInTheDocument();
});
it('clicking Stop calls the stop endpoint for that session', async () => {
const user = userEvent.setup();
mockApiFetch.mockImplementation((path: string) => {
if (path === '/api/admin/sessions/active') {
return Promise.resolve([makeSession({ id: 7, user_name: 'Jan' })]);
}
return Promise.resolve({});
});
renderLive();
await user.click(await screen.findByRole('button', { name: 'Stop' }));
await waitFor(() => {
expect(mockApiFetch).toHaveBeenCalledWith(
'/api/admin/sessions/7/stop',
expect.objectContaining({ method: 'POST' }),
);
});
});
it('clicking Annuleer calls the discard endpoint for that session', async () => {
const user = userEvent.setup();
mockApiFetch.mockImplementation((path: string) => {
if (path === '/api/admin/sessions/active') {
return Promise.resolve([makeSession({ id: 7, user_name: 'Jan' })]);
}
return Promise.resolve({});
});
renderLive();
await user.click(await screen.findByRole('button', { name: 'Annuleer' }));
await waitFor(() => {
expect(mockApiFetch).toHaveBeenCalledWith(
'/api/admin/sessions/7/discard',
expect.objectContaining({ method: 'POST' }),
);
});
});
it('keeps the timer counting (no Gepauzeerd badge) when not paused', async () => {
vi.useFakeTimers();
mockApiFetch.mockResolvedValue([

View File

@@ -1,6 +1,10 @@
import { useEffect, useState } from 'react';
import type { WorkSession } from '@solelog/shared';
import { useActiveSessions } from '../api/admin-sessions';
import {
useActiveSessions,
useAdminDiscardSession,
useAdminStopSession,
} from '../api/admin-sessions';
import { formatTime } from '../lib/elapsed';
export default function Live() {
@@ -56,6 +60,14 @@ function LiveCard({ session, now }: { session: WorkSession; now: number }) {
0,
Math.floor((base - Date.parse(session.start_time)) / 1000) - session.paused_seconds,
);
// Stop closes the session (server computes the duration); Annuleer discards it.
// Both hooks invalidate ['admin','sessions'], so the active query refetches and the
// card drops off the Live grid once the session is no longer active.
const stopSession = useAdminStopSession();
const discardSession = useAdminDiscardSession();
const busy = stopSession.isPending || discardSession.isPending;
return (
<article className="live-card">
<div className="live-card-head">
@@ -69,6 +81,24 @@ function LiveCard({ session, now }: { session: WorkSession; now: number }) {
{session.paused_seconds > 0 && (
<span className="live-paused-total">Pauze {formatTime(session.paused_seconds)}</span>
)}
<div className="live-card-actions">
<button
type="button"
className="btn-row-stop"
disabled={busy}
onClick={() => stopSession.mutate(session.id)}
>
Stop
</button>
<button
type="button"
className="btn-row-cancel"
disabled={busy}
onClick={() => discardSession.mutate(session.id)}
>
Annuleer
</button>
</div>
</article>
);
}

View File

@@ -421,6 +421,12 @@ body {
font-variant-numeric: tabular-nums;
}
.live-card-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
/* ---- Sessions management (Sessies) ---- */
.sessions-head {
display: flex;