Files
HarborForge.Frontend/src/components/MilestoneFormModal.tsx

155 lines
5.0 KiB
TypeScript

import { useEffect, useState } from 'react'
import api from '@/services/api'
import type { Milestone, Project } from '@/types'
type Props = {
isOpen: boolean
onClose: () => void
onSaved?: (milestone: Milestone) => void | Promise<void>
milestone?: Milestone | null
initialProjectId?: number
lockProject?: boolean
}
type FormState = {
title: string
description: string
project_id: number
status: string
due_date: string
planned_release_date: string
}
const emptyForm: FormState = {
title: '',
description: '',
project_id: 0,
status: 'open',
due_date: '',
planned_release_date: '',
}
const toDateInput = (value?: string | null) => (value ? String(value).slice(0, 10) : '')
export default function MilestoneFormModal({ isOpen, onClose, onSaved, milestone, initialProjectId, lockProject = false }: Props) {
const [projects, setProjects] = useState<Project[]>([])
const [saving, setSaving] = useState(false)
const [form, setForm] = useState<FormState>(emptyForm)
useEffect(() => {
if (!isOpen) return
const init = async () => {
const { data } = await api.get<Project[]>('/projects')
setProjects(data)
const defaultProjectId = milestone?.project_id || initialProjectId || data[0]?.id || 0
setForm({
title: milestone?.title || '',
description: milestone?.description || '',
project_id: defaultProjectId,
status: milestone?.status || 'open',
due_date: toDateInput(milestone?.due_date),
planned_release_date: toDateInput(milestone?.planned_release_date),
})
}
init().catch(console.error)
}, [isOpen, milestone, initialProjectId])
const submit = async (e: React.FormEvent) => {
e.preventDefault()
setSaving(true)
try {
const payload = {
title: form.title,
description: form.description || null,
project_id: form.project_id,
status: form.status,
due_date: form.due_date || null,
planned_release_date: form.planned_release_date || null,
}
if (milestone) {
const { data } = await api.patch<Milestone>(`/milestones/${milestone.id}`, payload)
await onSaved?.(data)
} else {
const { data } = await api.post<Milestone>('/milestones', payload)
await onSaved?.(data)
}
onClose()
} finally {
setSaving(false)
}
}
if (!isOpen) return null
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>{milestone ? 'Edit Milestone' : 'Create Milestone'}</h3>
<button type="button" className="btn-secondary" onClick={onClose}></button>
</div>
<form className="task-create-form" onSubmit={submit}>
<label>
Title
<input
data-testid="milestone-title-input"
required
placeholder="Milestone title"
value={form.title}
onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}
/>
</label>
<label>
Description
<textarea
data-testid="milestone-description-input"
placeholder="Description (optional)"
value={form.description}
onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))}
/>
</label>
<div className="task-create-grid">
<label>
Project
<select value={form.project_id} onChange={(e) => setForm((f) => ({ ...f, project_id: Number(e.target.value) }))} disabled={lockProject}>
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</label>
<label>
Status
<select value={form.status} onChange={(e) => setForm((f) => ({ ...f, status: e.target.value }))}>
<option value="open">Open</option>
<option value="pending">Pending</option>
<option value="deferred">Deferred</option>
<option value="progressing">Progressing</option>
<option value="closed">Closed</option>
</select>
</label>
<label>
Due date
<input type="date" value={form.due_date} onChange={(e) => setForm((f) => ({ ...f, due_date: e.target.value }))} />
</label>
<label>
Planned release date
<input type="date" value={form.planned_release_date} onChange={(e) => setForm((f) => ({ ...f, planned_release_date: e.target.value }))} />
</label>
</div>
<div className="modal-actions">
<button type="submit" className="btn-primary" disabled={saving}>{saving ? 'Saving...' : (milestone ? 'Save' : 'Create')}</button>
<button type="button" className="btn-secondary" onClick={onClose} disabled={saving}>Cancel</button>
</div>
</form>
</div>
</div>
)
}