feat(admin): stop/cancel an active session from the live view
This commit is contained in:
@@ -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([
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user