import { useState, useEffect, useMemo } from 'react' import { useParams, useNavigate } from 'react-router-dom' import api from '@/services/api' import type { Milestone, MilestoneProgress, Task, Project, ProjectMember } from '@/types' import dayjs from 'dayjs' import CreateTaskModal from '@/components/CreateTaskModal' import MilestoneFormModal from '@/components/MilestoneFormModal' import { useAuth } from '@/hooks/useAuth' interface MilestoneTask { id: number title: string description?: string status: string task_code?: string task_status?: string estimated_effort?: number estimated_working_time?: string started_on?: string finished_on?: string assignee_id?: number created_at: string } export default function MilestoneDetailPage() { const { id } = useParams() const navigate = useNavigate() const { user } = useAuth() const [milestone, setMilestone] = useState(null) const [project, setProject] = useState(null) const [members, setMembers] = useState([]) const [progress, setProgress] = useState(null) const [tasks, setTasks] = useState([]) const [supports, setSupports] = useState([]) const [meetings, setMeetings] = useState([]) const [activeTab, setActiveTab] = useState<'tasks' | 'supports' | 'meetings'>('tasks') const [showCreateTask, setShowCreateTask] = useState(false) const [showEditMilestone, setShowEditMilestone] = useState(false) const [showCreateSupport, setShowCreateSupport] = useState(false) const [showCreateMeeting, setShowCreateMeeting] = useState(false) const [newTitle, setNewTitle] = useState('') const [newDesc, setNewDesc] = useState('') const [projectCode, setProjectCode] = useState('') const [actionLoading, setActionLoading] = useState(null) const [actionError, setActionError] = useState(null) const [showCloseConfirm, setShowCloseConfirm] = useState(false) const [closeReason, setCloseReason] = useState('') const fetchMilestone = () => { api.get(`/milestones/${id}`).then(({ data }) => { setMilestone(data) if (data.project_id) { api.get(`/projects/${data.project_id}`).then(({ data: proj }) => { setProject(proj) setProjectCode(proj.project_code || '') }) api.get(`/projects/${data.project_id}/members`).then(({ data }) => setMembers(data)).catch(() => {}) } }) api.get(`/milestones/${id}/progress`).then(({ data }) => setProgress(data)).catch(() => {}) } useEffect(() => { fetchMilestone() }, [id]) const refreshMilestoneItems = () => { if (!projectCode || !id) return api.get(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data)).catch(() => {}) api.get(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data)).catch(() => {}) api.get(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data)).catch(() => {}) } useEffect(() => { refreshMilestoneItems() }, [projectCode, id]) const createItem = async (type: 'supports' | 'meetings') => { if (!newTitle.trim() || !projectCode) return const payload = { title: newTitle, description: newDesc || null, } await api.post(`/${type}/${projectCode}/${id}`, payload) setNewTitle('') setNewDesc('') setShowCreateSupport(false) setShowCreateMeeting(false) refreshMilestoneItems() } const currentMemberRole = useMemo( () => members.find((m) => m.user_id === user?.id)?.role, [members, user?.id] ) const canEditMilestone = Boolean(milestone && project && user && ( user.is_admin || user.id === project.owner_id || user.id === milestone.created_by_id || currentMemberRole === 'admin' )) 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) => { 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
Loading...
const renderTaskRow = (t: MilestoneTask) => ( navigate(`/tasks/${t.id}`)}> {t.task_code || t.id} {t.title} {t.task_status || t.status} {t.estimated_effort || '-'} {t.estimated_working_time || '-'} ) return (

🏁 {milestone.title}

{milestone.status} {milestone.due_date && Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}} {milestone.planned_release_date && Planned Release: {dayjs(milestone.planned_release_date).format('YYYY-MM-DD')}} {milestone.started_at && Started: {dayjs(milestone.started_at).format('YYYY-MM-DD HH:mm')}}
{canEditMilestone && msStatus === 'open' && } {canEditMilestone && (msStatus === 'freeze' || msStatus === 'undergoing') && ( ⚠ Milestone is {msStatus} — scope fields are locked )} {/* Milestone status action buttons (P8.2) */} {!isTerminal && (
{msStatus === 'open' && ( )} {msStatus === 'freeze' && ( )} {(msStatus === 'open' || msStatus === 'freeze' || msStatus === 'undergoing') && ( <> {!showCloseConfirm ? ( ) : (
setCloseReason(e.target.value)} style={{ minWidth: 180 }} />
)} )}
)} {actionError &&

⚠️ {actionError}

}
{milestone.description && (

Description

{milestone.description}

)} {progress && (

Progress (Tasks: {progress.completed}/{progress.total})

{progress.progress_pct.toFixed(0)}%
{progress.time_progress_pct !== null && ( <>

Time Progress

{progress.time_progress_pct.toFixed(0)}%
)}
)}
{!isTerminal && !isUndergoing && canEditMilestone && ( <> )} {(isUndergoing || isTerminal) && {isTerminal ? `Milestone is ${msStatus}` : 'Milestone is undergoing'} — cannot add new items}
setShowEditMilestone(false)} milestone={milestone} lockProject onSaved={(data) => { setMilestone(data) fetchMilestone() }} /> setShowCreateTask(false)} initialProjectId={milestone.project_id} initialMilestoneId={milestone.id} lockProject lockMilestone onCreated={() => { setActiveTab('tasks') refreshMilestoneItems() }} /> {(showCreateSupport || showCreateMeeting) && (
setNewTitle(e.target.value)} style={{ marginBottom: 8 }} />