From e48df48376da3043279962abe3aaf34a3626db9b Mon Sep 17 00:00:00 2001 From: Bas van Rossem Date: Wed, 17 Jun 2026 21:18:07 +0200 Subject: [PATCH] feat(admin): reorder handelingen with up/down arrows --- apps/admin/src/api/activities.ts | 14 +++++ apps/admin/src/screens/Activities.test.tsx | 64 +++++++++++++++++++++- apps/admin/src/screens/Activities.tsx | 30 +++++++++- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/apps/admin/src/api/activities.ts b/apps/admin/src/api/activities.ts index 6c9a4e0..600ae8e 100644 --- a/apps/admin/src/api/activities.ts +++ b/apps/admin/src/api/activities.ts @@ -47,3 +47,17 @@ export function useDeleteActivity() { }, }); } + +export function useReorderActivities() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (ids: number[]) => + apiFetch('/api/activities/reorder', { + method: 'PUT', + body: JSON.stringify({ ids }), + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['activities'] }); + }, + }); +} diff --git a/apps/admin/src/screens/Activities.test.tsx b/apps/admin/src/screens/Activities.test.tsx index 756d1f4..5a6a3bc 100644 --- a/apps/admin/src/screens/Activities.test.tsx +++ b/apps/admin/src/screens/Activities.test.tsx @@ -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(); + }); }); diff --git a/apps/admin/src/screens/Activities.tsx b/apps/admin/src/screens/Activities.tsx index a496e3a..e3d39ae 100644 --- a/apps/admin/src/screens/Activities.tsx +++ b/apps/admin/src/screens/Activities.tsx @@ -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 (

Handelingen

@@ -164,7 +174,7 @@ export default function Activities() {

Nog geen stappen. Voeg er een toe hierboven.

) : (
    - {activities.map((activity) => ( + {activities.map((activity, index) => (
  • {editingId === activity.id ? ( <> @@ -202,6 +212,24 @@ export default function Activities() {
    {activity.name}
    + +