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 { ShieldAlert, Plus, Edit2, AlertTriangle, X, RotateCcw, CheckCircle2, ShieldCheck, } from 'lucide-react'; import { Modal } from '../ui/Modal'; import { HelpTooltip } from '../ui/HelpTooltip'; import { useToast } from '../ui/Toast'; import { getProjectRisks, createRisk, updateRisk, } from '../../services/api'; import { Project, Risk, RiskLikelihood, RiskImpact } from '../../types'; import { RISK_LIKELIHOOD_OPTIONS, RISK_IMPACT_OPTIONS, getRiskScoreColor, } from '../../lib/formatters'; const riskSchema = z.object({ name: z.string().min(5, 'Risk title required (min 5 chars)'), description: z.string().min(10, 'Risk description required (min 10 chars)'), likelihood: z.coerce.number().min(1).max(5), impact: z.coerce.number().min(1).max(5), }); type RiskFormValues = z.infer; interface ProjectRisksModalProps { isOpen: boolean; onClose: () => void; project: Project | null; } export const ProjectRisksModal: React.FC = ({ isOpen, onClose, project, }) => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const [isFormOpen, setIsFormOpen] = useState(false); const [editingRisk, setEditingRisk] = useState(null); const projectId = project?.id; const { data: risks = [], isLoading, refetch, } = useQuery({ queryKey: ['risks', projectId], queryFn: () => (projectId ? getProjectRisks(projectId) : Promise.resolve([])), enabled: isOpen && !!projectId, }); const form = useForm({ resolver: zodResolver(riskSchema) as any, defaultValues: { name: '', description: '', likelihood: RiskLikelihood.Possible, impact: RiskImpact.Moderate, }, }); const openCreateForm = () => { setEditingRisk(null); form.reset({ name: '', description: '', likelihood: RiskLikelihood.Possible, impact: RiskImpact.Moderate, }); setIsFormOpen(true); }; const openEditForm = (r: Risk) => { setEditingRisk(r); form.reset({ name: r.name || '', description: r.description || '', likelihood: r.likelihood || RiskLikelihood.Possible, impact: r.impact || RiskImpact.Moderate, }); setIsFormOpen(true); }; const mutation = useMutation({ mutationFn: async (data: RiskFormValues): Promise => { if (!projectId) return; if (editingRisk) { await updateRisk({ riskId: editingRisk.id, name: data.name, description: data.description, likelihood: Number(data.likelihood), impact: Number(data.impact), }); } else { await createRisk({ projectId: projectId, name: data.name, description: data.description, likelihood: Number(data.likelihood), impact: Number(data.impact), }); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['risks', projectId] }); queryClient.invalidateQueries({ queryKey: ['risks'] }); success( editingRisk ? 'Risk Updated' : 'Risk Registered', editingRisk ? 'Risk register item updated' : 'Risk registered to ISO 31000 risk matrix' ); setIsFormOpen(false); setEditingRisk(null); form.reset(); }, onError: (err: unknown) => { toastError(editingRisk ? 'Failed to update risk' : 'Failed to register risk', err); }, }); if (!project) return null; const highRisksCount = risks.filter((r) => r.likelihood * r.impact >= 12).length; return ( { setIsFormOpen(false); setEditingRisk(null); onClose(); }} title={`Risk Register: ${project.name}`} subtitle={`Project #${project.id} • ISO 31000 Risk & Mitigation Controls`} helpText="Assesses project operational, financial, and technical hazards by Likelihood (1-5) and Consequence/Impact (1-5), establishing proactive mitigation commitments." maxWidth="xl" >
{/* KPI Summary Card */}
Project Risk Profile
{risks.length} ({highRisksCount} critical/high exposure items)
{!isFormOpen && ( )}
{/* Create / Edit Risk Form */} {isFormOpen && (
{editingRisk ? `Edit Risk #${editingRisk.id}` : 'Log New Project Hazard & Risk'}
mutation.mutate(data as any))} className="space-y-3">
{form.formState.errors.name && (

{form.formState.errors.name.message}

)}