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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import type { WorkSession } from '@solelog/shared';
|
import type { WorkSession } from '@solelog/shared';
|
||||||
@@ -120,6 +121,58 @@ describe('Live', () => {
|
|||||||
expect(await screen.findByText('Pauze 00:02:05')).toBeInTheDocument();
|
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 () => {
|
it('keeps the timer counting (no Gepauzeerd badge) when not paused', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
mockApiFetch.mockResolvedValue([
|
mockApiFetch.mockResolvedValue([
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { WorkSession } from '@solelog/shared';
|
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';
|
import { formatTime } from '../lib/elapsed';
|
||||||
|
|
||||||
export default function Live() {
|
export default function Live() {
|
||||||
@@ -56,6 +60,14 @@ function LiveCard({ session, now }: { session: WorkSession; now: number }) {
|
|||||||
0,
|
0,
|
||||||
Math.floor((base - Date.parse(session.start_time)) / 1000) - session.paused_seconds,
|
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 (
|
return (
|
||||||
<article className="live-card">
|
<article className="live-card">
|
||||||
<div className="live-card-head">
|
<div className="live-card-head">
|
||||||
@@ -69,6 +81,24 @@ function LiveCard({ session, now }: { session: WorkSession; now: number }) {
|
|||||||
{session.paused_seconds > 0 && (
|
{session.paused_seconds > 0 && (
|
||||||
<span className="live-paused-total">Pauze {formatTime(session.paused_seconds)}</span>
|
<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>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -421,6 +421,12 @@ body {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.live-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Sessions management (Sessies) ---- */
|
/* ---- Sessions management (Sessies) ---- */
|
||||||
.sessions-head {
|
.sessions-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user