import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Link } from 'react-router-dom'; import { Flag, Plus, Edit2, Clock, FileCheck, CheckCircle2, AlertCircle, TrendingUp, X, RotateCcw, } from 'lucide-react'; import { Modal } from '../ui/Modal'; import { HelpTooltip } from '../ui/HelpTooltip'; import { useToast } from '../ui/Toast'; import { getMilestones, createMilestone, updateMilestone, } from '../../services/api'; import { Project, Milestone, Priority, ApprovalStatus } from '../../types'; import { formatDate, getApprovalStatusBadge, getPriorityBadge, PRIORITY_OPTIONS, APPROVAL_STATUS_OPTIONS, } from '../../lib/formatters'; const milestoneSchema = z.object({ name: z.string().min(4, 'Deliverable title must be at least 4 characters'), dueAt: z.string().min(4, 'Due date is mandatory'), priority: z.coerce.number().min(1).max(4), status: z.coerce.number().optional(), }); type MilestoneFormValues = z.infer; interface ProjectMilestonesModalProps { isOpen: boolean; onClose: () => void; project: Project | null; } export const ProjectMilestonesModal: React.FC = ({ isOpen, onClose, project, }) => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const [isFormOpen, setIsFormOpen] = useState(false); const [editingMilestone, setEditingMilestone] = useState(null); const projectId = project?.id; const { data: milestones = [], isLoading, refetch, } = useQuery({ queryKey: ['milestones', projectId], queryFn: () => (projectId ? getMilestones(projectId) : Promise.resolve([])), enabled: isOpen && !!projectId, }); const form = useForm({ resolver: zodResolver(milestoneSchema) as any, defaultValues: { name: '', dueAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10), priority: Priority.High, status: ApprovalStatus.Pending, }, }); const openCreateForm = () => { setEditingMilestone(null); form.reset({ name: '', dueAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10), priority: Priority.High, status: ApprovalStatus.Pending, }); setIsFormOpen(true); }; const openEditForm = (m: Milestone) => { setEditingMilestone(m); form.reset({ name: m.name || '', dueAt: m.dueAt ? m.dueAt.slice(0, 10) : new Date().toISOString().slice(0, 10), priority: m.priority || Priority.High, status: m.status || ApprovalStatus.Pending, }); setIsFormOpen(true); }; const mutation = useMutation({ mutationFn: async (data: MilestoneFormValues): Promise => { if (!projectId) return; if (editingMilestone) { await updateMilestone({ milestoneId: editingMilestone.id, name: data.name, dueAt: data.dueAt, priority: Number(data.priority), status: Number(data.status ?? editingMilestone.status), }); } else { await createMilestone({ projectId: projectId, name: data.name, dueAt: data.dueAt, priority: Number(data.priority), }); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['milestones', projectId] }); queryClient.invalidateQueries({ queryKey: ['projects'] }); queryClient.invalidateQueries({ queryKey: ['milestones-all'] }); success( editingMilestone ? 'Milestone Updated' : 'Milestone Created', editingMilestone ? 'Contractual deliverable updated successfully' : 'Deliverable added to project implementation schedule' ); setIsFormOpen(false); setEditingMilestone(null); form.reset(); }, onError: (err: unknown) => { toastError(editingMilestone ? 'Failed to update milestone' : 'Failed to create milestone', err); }, }); if (!project) return null; const verifiedCount = milestones.filter((m) => m.status === ApprovalStatus.Approved).length; const progressPct = milestones.length > 0 ? Math.round((verifiedCount / milestones.length) * 100) : 0; return ( { setIsFormOpen(false); setEditingMilestone(null); onClose(); }} title={`Milestones & Deliverables: ${project.name}`} subtitle={`Project #${project.id} • Statutory Work Breakdown Schedule`} helpText="Contractual milestones represent formal deliverables specified in the grant contract. Upon verification and approval, they increment the calculated project progress score and unblock disbursement releases." maxWidth="xl" >
{/* Progress & Quick Stats Card */}
Calculated Completion Score
{progressPct}% ({verifiedCount} of {milestones.length} deliverables verified)
{!isFormOpen && ( )}
{/* Milestone Create/Edit Form */} {isFormOpen && (
{editingMilestone ? `Edit Milestone #${editingMilestone.id}` : 'Define New Contractual Milestone'}
mutation.mutate(data as any))} className="space-y-3">
{form.formState.errors.name && (

{form.formState.errors.name.message}

)}
{form.formState.errors.dueAt && (

{form.formState.errors.dueAt.message}

)}
{editingMilestone && (
)}
)} {/* Milestones List */}
Project Deliverables Schedule ({milestones.length})
{isLoading ? (
Loading milestones...
) : milestones.length === 0 ? (

No milestones registered yet

Define contractual deliverables to monitor implementation progress.

) : (
{milestones.map((m) => { const statusBadge = getApprovalStatusBadge(m.status); const priBadge = getPriorityBadge(m.priority); const isApproved = m.status === ApprovalStatus.Approved; return (
{m.name} {statusBadge.label} {priBadge.label}
Due: {formatDate(m.dueAt)} • ID #{m.id}
Evidence
); })}
)}
); };