feat(P8.1-P8.2): milestone status action buttons + badge styles + started_at display

- Add freeze/start/close action buttons on MilestoneDetailPage
- Freeze: visible in open status, calls POST .../actions/freeze
- Start: visible in freeze status, calls POST .../actions/start
- Close: visible in open/freeze/undergoing, with reason input + confirmation
- Display started_at in milestone meta when present
- Hide edit button and create-item buttons in terminal states
- Add CSS badge styles for freeze (purple), undergoing (amber), completed (green)
- All actions show loading state and error feedback
This commit is contained in:
zhi
2026-03-17 06:05:09 +00:00
parent 35e7d3a141
commit 18703d98f8
2 changed files with 95 additions and 4 deletions

View File

@@ -72,6 +72,9 @@ tr.clickable:hover { background: var(--bg-hover); }
.status-resolved { background: #10b981; }
.status-closed { background: #6b7280; }
.status-blocked { background: #ef4444; }
.status-freeze { background: #8b5cf6; }
.status-undergoing { background: #f59e0b; }
.status-completed { background: #10b981; }
.priority-low { background: #6b7280; }
.priority-medium { background: #3b82f6; }
.priority-high { background: #f59e0b; }

View File

@@ -41,6 +41,10 @@ export default function MilestoneDetailPage() {
const [newTitle, setNewTitle] = useState('')
const [newDesc, setNewDesc] = useState('')
const [projectCode, setProjectCode] = useState('')
const [actionLoading, setActionLoading] = useState<string | null>(null)
const [actionError, setActionError] = useState<string | null>(null)
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
const [closeReason, setCloseReason] = useState('')
const fetchMilestone = () => {
api.get<Milestone>(`/milestones/${id}`).then(({ data }) => {
@@ -96,7 +100,34 @@ export default function MilestoneDetailPage() {
currentMemberRole === 'admin'
))
const isUndergoing = milestone?.status === 'undergoing'
const msStatus = milestone?.status
const isUndergoing = msStatus === 'undergoing'
const isTerminal = msStatus === 'completed' || msStatus === 'closed'
// --- Milestone action handlers (P8.2) ---
const performAction = async (action: string, body?: Record<string, unknown>) => {
if (!milestone || !project) return
setActionLoading(action)
setActionError(null)
try {
await api.post(`/projects/${project.id}/milestones/${milestone.id}/actions/${action}`, body ?? {})
fetchMilestone()
refreshMilestoneItems()
} catch (err: any) {
const detail = err?.response?.data?.detail
setActionError(typeof detail === 'string' ? detail : `${action} failed`)
} finally {
setActionLoading(null)
}
}
const handleFreeze = () => performAction('freeze')
const handleStart = () => performAction('start')
const handleClose = () => {
performAction('close', closeReason ? { reason: closeReason } : {})
setShowCloseConfirm(false)
setCloseReason('')
}
if (!milestone) return <div className="loading">Loading...</div>
@@ -120,8 +151,65 @@ export default function MilestoneDetailPage() {
<span className={`badge status-${milestone.status}`}>{milestone.status}</span>
{milestone.due_date && <span className="text-dim">Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}
{milestone.planned_release_date && <span className="text-dim">Planned Release: {dayjs(milestone.planned_release_date).format('YYYY-MM-DD')}</span>}
{milestone.started_at && <span className="text-dim">Started: {dayjs(milestone.started_at).format('YYYY-MM-DD HH:mm')}</span>}
</div>
{canEditMilestone && <button className="btn-transition" style={{ marginTop: 8 }} onClick={() => setShowEditMilestone(true)}>Edit Milestone</button>}
{canEditMilestone && !isTerminal && <button className="btn-transition" style={{ marginTop: 8 }} onClick={() => setShowEditMilestone(true)}>Edit Milestone</button>}
{/* Milestone status action buttons (P8.2) */}
{!isTerminal && (
<div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap', alignItems: 'center' }}>
{msStatus === 'open' && (
<button
className="btn-primary"
disabled={actionLoading === 'freeze'}
onClick={handleFreeze}
>
{actionLoading === 'freeze' ? '⏳ Freezing...' : '🧊 Freeze'}
</button>
)}
{msStatus === 'freeze' && (
<button
className="btn-primary"
disabled={actionLoading === 'start'}
onClick={handleStart}
>
{actionLoading === 'start' ? '⏳ Starting...' : '▶️ Start'}
</button>
)}
{(msStatus === 'open' || msStatus === 'freeze' || msStatus === 'undergoing') && (
<>
{!showCloseConfirm ? (
<button
className="btn-transition"
style={{ color: 'var(--color-danger, #e74c3c)' }}
onClick={() => setShowCloseConfirm(true)}
>
Close
</button>
) : (
<div className="card" style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '8px 12px' }}>
<input
placeholder="Reason (optional)"
value={closeReason}
onChange={(e) => setCloseReason(e.target.value)}
style={{ minWidth: 180 }}
/>
<button
className="btn-primary"
style={{ backgroundColor: 'var(--color-danger, #e74c3c)' }}
disabled={actionLoading === 'close'}
onClick={handleClose}
>
{actionLoading === 'close' ? 'Closing...' : 'Confirm Close'}
</button>
<button className="btn-back" onClick={() => { setShowCloseConfirm(false); setCloseReason('') }}>Cancel</button>
</div>
)}
</>
)}
</div>
)}
{actionError && <p style={{ color: 'var(--color-danger, #e74c3c)', marginTop: 4 }}> {actionError}</p>}
</div>
{milestone.description && (
@@ -154,14 +242,14 @@ export default function MilestoneDetailPage() {
<div className="section">
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
{!isUndergoing && canEditMilestone && (
{!isTerminal && !isUndergoing && canEditMilestone && (
<>
<button className="btn-primary" onClick={() => { setActiveTab('tasks'); setShowCreateTask(true) }}>+ Create Task</button>
<button className="btn-primary" onClick={() => { setActiveTab('supports'); setShowCreateSupport(true) }}>+ Create Support</button>
<button className="btn-primary" onClick={() => { setActiveTab('meetings'); setShowCreateMeeting(true) }}>+ Schedule Meeting</button>
</>
)}
{isUndergoing && <span className="text-dim">Milestone is undergoing - cannot add new items</span>}
{(isUndergoing || isTerminal) && <span className="text-dim">{isTerminal ? `Milestone is ${msStatus}` : 'Milestone is undergoing'} cannot add new items</span>}
</div>
<MilestoneFormModal