M2-T10: build records management UI (paginated lists + single-record CRUD)

- reusable src/records/ module: useUpdate/useDelete Poo+Location hooks
  (encodeURIComponent PK, prefix-based query invalidation), EditPooModal,
  EditLocationModal, ConfirmDeleteModal — exported for the map (T09) to reuse
- RecordsPage (/records): paginated poo + location tables (page size 100),
  edit + delete-with-confirm, refresh on success
- query keys ['poo']/['locations'] so map and list invalidations cross-cut
- typed client only; vitest tests
This commit is contained in:
2026-06-13 10:32:02 +02:00
parent 6cc6382515
commit ef7ea6b971
9 changed files with 1442 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
/**
* EditPooModal — edit non-PK fields of a poo record (M2-T10, reused by T09).
*
* Editable fields: status, latitude, longitude.
* Read-only: timestamp (PK).
*/
import { useState } from 'react'
import {
Modal,
Stack,
TextInput,
NumberInput,
Button,
Group,
Text,
Alert,
} from '@mantine/core'
import { useUpdatePoo } from './hooks'
import type { PooRecord, PooUpdateBody } from './hooks'
export interface EditPooModalProps {
record: PooRecord
onClose: () => void
onSaved: () => void
}
export function EditPooModal({ record, onClose, onSaved }: EditPooModalProps) {
const [status, setStatus] = useState(record.status)
const [latitude, setLatitude] = useState<number | string>(record.latitude)
const [longitude, setLongitude] = useState<number | string>(record.longitude)
const [error, setError] = useState<string | null>(null)
const updateMutation = useUpdatePoo()
function validate(): string | null {
const lat = Number(latitude)
const lng = Number(longitude)
if (isNaN(lat) || lat < -90 || lat > 90) return 'Latitude must be a number between -90 and 90.'
if (isNaN(lng) || lng < -180 || lng > 180)
return 'Longitude must be a number between -180 and 180.'
return null
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError(null)
const validationError = validate()
if (validationError) {
setError(validationError)
return
}
const body: PooUpdateBody = {
status: status || undefined,
latitude: Number(latitude),
longitude: Number(longitude),
}
try {
await updateMutation.mutateAsync({ timestamp: record.timestamp, body })
onSaved()
onClose()
} catch {
setError('Failed to save. Please try again.')
}
}
return (
<Modal
opened
onClose={onClose}
title="Edit Poo Record"
size="sm"
data-testid="edit-poo-modal"
>
<form onSubmit={handleSubmit} data-testid="edit-poo-form">
<Stack gap="sm">
{/* PK — read-only */}
<Text size="sm" c="dimmed">
<strong>Timestamp (PK):</strong> {record.timestamp}
</Text>
<TextInput
label="Status"
value={status}
onChange={(e) => setStatus(e.currentTarget.value)}
data-testid="poo-status-input"
/>
<NumberInput
label="Latitude"
value={latitude}
onChange={(val) => setLatitude(val)}
decimalScale={6}
data-testid="poo-latitude-input"
/>
<NumberInput
label="Longitude"
value={longitude}
onChange={(val) => setLongitude(val)}
decimalScale={6}
data-testid="poo-longitude-input"
/>
{error && (
<Alert color="red" data-testid="edit-poo-error">
{error}
</Alert>
)}
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={onClose} data-testid="edit-poo-cancel">
Cancel
</Button>
<Button
type="submit"
loading={updateMutation.isPending}
data-testid="edit-poo-submit"
>
Save
</Button>
</Group>
</Stack>
</form>
</Modal>
)
}