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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user