feat(admin): reorder handelingen with up/down arrows

This commit is contained in:
Bas van Rossem
2026-06-17 21:18:07 +02:00
parent 0b0a6bd073
commit e48df48376
3 changed files with 105 additions and 3 deletions

View File

@@ -47,3 +47,17 @@ export function useDeleteActivity() {
},
});
}
export function useReorderActivities() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (ids: number[]) =>
apiFetch<Activity[]>('/api/activities/reorder', {
method: 'PUT',
body: JSON.stringify({ ids }),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['activities'] });
},
});
}

View File

@@ -13,8 +13,27 @@ vi.mock('../lib/api', () => ({
const mockApiFetch = vi.mocked(apiFetch);
const existing: Activity[] = [
{ id: 1, name: 'Frezen', insole_types: ['Kurk', 'Berk'], created_at: '2026-06-17T00:00:00.000Z' },
{ id: 2, name: 'Lijmen', insole_types: ['3D'], created_at: '2026-06-17T00:00:00.000Z' },
{
id: 1,
name: 'Frezen',
insole_types: ['Kurk', 'Berk'],
created_at: '2026-06-17T00:00:00.000Z',
sort_order: 0,
},
{
id: 2,
name: 'Lijmen',
insole_types: ['3D'],
created_at: '2026-06-17T00:00:00.000Z',
sort_order: 1,
},
{
id: 3,
name: 'Polijsten',
insole_types: ['Kurk'],
created_at: '2026-06-17T00:00:00.000Z',
sort_order: 2,
},
];
function renderActivities() {
@@ -103,4 +122,45 @@ describe('Activities', () => {
expect(mockApiFetch).not.toHaveBeenCalledWith('/api/activities/1', { method: 'DELETE' });
});
it('moving the second row up PUTs /api/activities/reorder with the swapped ids', async () => {
const user = userEvent.setup();
renderActivities();
await screen.findByText('Lijmen');
await user.click(screen.getByRole('button', { name: 'Verplaats Lijmen omhoog' }));
await waitFor(() => {
expect(mockApiFetch).toHaveBeenCalledWith('/api/activities/reorder', {
method: 'PUT',
body: JSON.stringify({ ids: [2, 1, 3] }),
});
});
});
it('moving the second row down PUTs /api/activities/reorder with the swapped ids', async () => {
const user = userEvent.setup();
renderActivities();
await screen.findByText('Lijmen');
await user.click(screen.getByRole('button', { name: 'Verplaats Lijmen omlaag' }));
await waitFor(() => {
expect(mockApiFetch).toHaveBeenCalledWith('/api/activities/reorder', {
method: 'PUT',
body: JSON.stringify({ ids: [1, 3, 2] }),
});
});
});
it('disables up on the first row and down on the last row', async () => {
renderActivities();
await screen.findByText('Frezen');
expect(screen.getByRole('button', { name: 'Verplaats Frezen omhoog' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Verplaats Polijsten omlaag' })).toBeDisabled();
// The opposite ends are enabled.
expect(screen.getByRole('button', { name: 'Verplaats Frezen omlaag' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'Verplaats Polijsten omhoog' })).toBeEnabled();
});
});

View File

@@ -4,6 +4,7 @@ import {
useActivities,
useCreateActivity,
useDeleteActivity,
useReorderActivities,
useUpdateActivity,
} from '../api/activities';
@@ -71,6 +72,7 @@ export default function Activities() {
const createActivity = useCreateActivity();
const updateActivity = useUpdateActivity();
const deleteActivity = useDeleteActivity();
const reorderActivities = useReorderActivities();
// Add form state.
const [newName, setNewName] = useState('');
@@ -127,6 +129,14 @@ export default function Activities() {
if (ok) deleteActivity.mutate(id);
}
function handleMove(index: number, direction: -1 | 1) {
const target = index + direction;
if (target < 0 || target >= activities.length) return;
const ids = activities.map((a) => a.id);
[ids[index], ids[target]] = [ids[target], ids[index]];
reorderActivities.mutate(ids);
}
return (
<div className="screen">
<h1 className="screen-title">Handelingen</h1>
@@ -164,7 +174,7 @@ export default function Activities() {
<p className="muted">Nog geen stappen. Voeg er een toe hierboven.</p>
) : (
<ul className="activity-list">
{activities.map((activity) => (
{activities.map((activity, index) => (
<li key={activity.id} className="activity-card">
{editingId === activity.id ? (
<>
@@ -202,6 +212,24 @@ export default function Activities() {
<div className="activity-row">
<span className="activity-name">{activity.name}</span>
<div className="row-actions">
<button
type="button"
className="icon-btn icon-move"
aria-label={`Verplaats ${activity.name} omhoog`}
disabled={index === 0 || reorderActivities.isPending}
onClick={() => handleMove(index, -1)}
>
</button>
<button
type="button"
className="icon-btn icon-move"
aria-label={`Verplaats ${activity.name} omlaag`}
disabled={index === activities.length - 1 || reorderActivities.isPending}
onClick={() => handleMove(index, 1)}
>
</button>
<button
type="button"
className="icon-btn icon-edit"