131 lines
3.3 KiB
TypeScript
131 lines
3.3 KiB
TypeScript
|
|
/**
|
||
|
|
* 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>
|
||
|
|
)
|
||
|
|
}
|