import React, { useState, useMemo } 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 { Briefcase, Plus, Edit2, Clock, Coins, FileCheck, Flag, X, RotateCcw, Target, AlertTriangle, ChevronRight, ShieldAlert, } from 'lucide-react'; import { Modal } from '../ui/Modal'; import { HelpTooltip } from '../ui/HelpTooltip'; import { useToast } from '../ui/Toast'; import { getActivities, createActivity, updateActivity, getMilestones, getProjectIndicators, getBudgets, } from '../../services/api'; import { Project, Activity, Milestone, Indicator, Budget } from '../../types'; import { formatDate, formatZAR } from '../../lib/formatters'; const activitySchema = z.object({ title: z.string().min(3, 'Activity title required'), description: z.string().min(5, 'Description required'), category: z.string().min(2, 'Category required'), amount: z.coerce.number().min(1, 'Amount must be positive'), milestoneId: z.string().optional().nullable(), performedAt: z.string().optional().nullable(), }); type ActivityFormValues = z.infer; interface ProjectActivitiesModalProps { isOpen: boolean; onClose: () => void; project: Project | null; onOpenIndicatorsForActivity?: (activityId: string | number) => void; } export const ProjectActivitiesModal: React.FC = ({ isOpen, onClose, project, onOpenIndicatorsForActivity, }) => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const [isFormOpen, setIsFormOpen] = useState(false); const [editingActivity, setEditingActivity] = useState(null); const projectId = project?.id; const { data: activities = [], isLoading, refetch, } = useQuery({ queryKey: ['activities', projectId], queryFn: () => (projectId ? getActivities(projectId) : Promise.resolve([])), enabled: isOpen && !!projectId, }); const { data: milestones = [] } = useQuery({ queryKey: ['milestones', projectId], queryFn: () => (projectId ? getMilestones(projectId) : Promise.resolve([])), enabled: isOpen && !!projectId, }); const { data: indicators = [] } = useQuery({ queryKey: ['indicators', projectId], queryFn: () => (projectId ? getProjectIndicators(projectId) : Promise.resolve([])), enabled: isOpen && !!projectId, }); const { data: allBudgets = [] } = useQuery({ queryKey: ['budgets'], queryFn: () => getBudgets(1, 100), enabled: isOpen, }); // Approved project budget calculations const approvedBudgetTotal = useMemo(() => { const projectBudgets = allBudgets.filter((b) => String(b.projectId) === String(projectId)); return projectBudgets .filter((b) => b.status === undefined || b.status === null || Number(b.status) === 3) .reduce((sum, b) => sum + (Number(b.amount) || 0), 0); }, [allBudgets, projectId]); const totalAllocated = useMemo( () => activities.reduce((acc, a) => acc + (Number(a.amount) || 0), 0), [activities] ); const maxAllowedForForm = useMemo(() => { if (approvedBudgetTotal <= 0) return Infinity; const baseRemaining = approvedBudgetTotal - totalAllocated; if (editingActivity) { return baseRemaining + (Number(editingActivity.amount) || 0); } return Math.max(0, baseRemaining); }, [approvedBudgetTotal, totalAllocated, editingActivity]); const remainingBudget = useMemo(() => { if (approvedBudgetTotal <= 0) return 0; return Math.max(0, approvedBudgetTotal - totalAllocated); }, [approvedBudgetTotal, totalAllocated]); const form = useForm({ resolver: zodResolver(activitySchema), defaultValues: { title: '', description: '', category: 'Operations', amount: 0, milestoneId: '', performedAt: new Date().toISOString().slice(0, 10), }, }); const openCreateForm = () => { setEditingActivity(null); form.reset({ title: '', description: '', category: 'Operations', amount: 0, milestoneId: '', performedAt: new Date().toISOString().slice(0, 10), }); setIsFormOpen(true); }; const openEditForm = (act: Activity) => { setEditingActivity(act); form.reset({ title: act.title || '', description: act.description || '', category: act.category || 'Operations', amount: act.amount || 0, milestoneId: act.milestoneId ? String(act.milestoneId) : '', performedAt: act.performedAt ? act.performedAt.slice(0, 10) : '', }); setIsFormOpen(true); }; const mutation = useMutation({ mutationFn: async (data: ActivityFormValues): Promise => { if (!projectId) return; const requestedAmount = Number(data.amount); if (approvedBudgetTotal > 0 && requestedAmount > maxAllowedForForm) { throw new Error( `Activity amount (${formatZAR(requestedAmount)}) exceeds the available remaining project budget (${formatZAR(maxAllowedForForm)}).` ); } const parsedMilestoneId = data.milestoneId && String(data.milestoneId).trim() !== '' ? Number(data.milestoneId) : null; const performedAtIso = data.performedAt ? new Date(data.performedAt).toISOString() : null; if (editingActivity) { await updateActivity({ activityId: editingActivity.id, projectId: projectId, title: data.title, description: data.description, category: data.category, amount: requestedAmount, milestoneId: parsedMilestoneId, performedAt: performedAtIso, }); } else { await createActivity({ projectId: projectId, title: data.title, description: data.description, category: data.category, amount: requestedAmount, milestoneId: parsedMilestoneId, performedAt: performedAtIso, }); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['activities', projectId] }); queryClient.invalidateQueries({ queryKey: ['milestones', projectId] }); queryClient.invalidateQueries({ queryKey: ['projects'] }); success( editingActivity ? 'Activity Updated' : 'Activity Created', editingActivity ? 'Work breakdown task updated successfully' : 'Task added to project work breakdown structure' ); setIsFormOpen(false); setEditingActivity(null); form.reset(); }, onError: (err: unknown) => { toastError(editingActivity ? 'Failed to update activity' : 'Failed to add activity', err); }, }); if (!project) return null; return ( { setIsFormOpen(false); setEditingActivity(null); onClose(); }} title={`Work Breakdown Activities: ${project.name}`} subtitle={`Project #${project.id} • Operational Work Breakdown Structure (WBS)`} helpText="Under each milestone, operational activities are scheduled with budgeted amounts. The sum of all activity budgets across the project cannot exceed the approved project budget." maxWidth="xl" >
{/* KPI Summary Cards */}
Total Allocated Activities
{formatZAR(totalAllocated)} ({activities.length} tasks)
Approved Project Budget
{formatZAR(approvedBudgetTotal)}
Remaining Budget Ceiling
0 ? 'text-rose-400' : 'text-blue-400'}`}> {formatZAR(remainingBudget)}
{/* Action Header */}
Scheduled Tasks: {activities.length}
{!isFormOpen && ( )}
{/* Create/Edit Activity Form */} {isFormOpen && (
{editingActivity ? `Edit Activity #${editingActivity.id}` : 'Add Work Breakdown Activity'}
{approvedBudgetTotal > 0 && (
Maximum activity budget allowance: {formatZAR(maxAllowedForForm)}
)}
mutation.mutate(data))} className="space-y-3">
{form.formState.errors.title && (

{form.formState.errors.title.message}

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

{form.formState.errors.amount.message}

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

{form.formState.errors.category.message}

)}