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 { Scale, Plus, Search, Filter, RefreshCw, Edit2, Eye, Layers, Calendar, User as UserIcon, Tag, CheckCircle2, AlertCircle, GitBranch, } from 'lucide-react'; import { Modal } from '../ui/Modal'; import { HelpTooltip } from '../ui/HelpTooltip'; import { useToast } from '../ui/Toast'; import { ExportButton } from '../ui/ExportButton'; import { ExportColumn } from '../../utils/exportUtils'; import { useTableSort, SortableTh } from '../../utils/tableSort'; import { Rule, Programme, ActivityStatus, } from '../../types'; import { getRules, createRule, updateRule, } from '../../services/api'; import { getActivityStatusBadge, formatDate, getUserEmail, ACTIVITY_STATUS_OPTIONS, } from '../../lib/formatters'; const ruleSchema = z.object({ programmeId: z.coerce.number().min(1, 'Please select an associated programme from the dropdown'), name: z.string().min(3, 'Rule name must be at least 3 characters'), description: z.string().min(5, 'Description is required (minimum 5 characters)'), version: z.coerce.number().min(1, 'Version must be at least 1').default(1), status: z.coerce.number().default(ActivityStatus.Active), }); type RuleFormValues = z.infer; interface RulesSectionProps { programmes: Programme[]; currentUserId: string; preselectedProgrammeId?: string | number | null; onClearPreselectedProgramme?: () => void; isCreateModalOpenExternal?: boolean; onCloseCreateModalExternal?: () => void; } export const RulesSection: React.FC = ({ programmes, currentUserId, preselectedProgrammeId, onClearPreselectedProgramme, isCreateModalOpenExternal, onCloseCreateModalExternal, }) => { const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const [searchTerm, setSearchTerm] = useState(''); const [selectedProgrammeFilter, setSelectedProgrammeFilter] = useState( preselectedProgrammeId ? String(preselectedProgrammeId) : 'all' ); const [statusFilter, setStatusFilter] = useState('all'); // Modals const [isModalOpen, setIsModalOpen] = useState(false); const [editingRule, setEditingRule] = useState(null); const [inspectingRule, setInspectingRule] = useState(null); // Sync external create trigger React.useEffect(() => { if (isCreateModalOpenExternal) { setEditingRule(null); ruleForm.reset({ programmeId: preselectedProgrammeId ? Number(preselectedProgrammeId) : (programmes[0]?.id ? Number(programmes[0].id) : 0), name: '', description: '', version: 1, status: ActivityStatus.Active, }); setIsModalOpen(true); } }, [isCreateModalOpenExternal, preselectedProgrammeId, programmes]); // Form setup const ruleForm = useForm({ resolver: zodResolver(ruleSchema), defaultValues: { programmeId: programmes[0]?.id ? Number(programmes[0].id) : 0, name: '', description: '', version: 1, status: ActivityStatus.Active, }, }); // Query: Get all rules (with programme filter) const { data: rules = [], isLoading, isFetching, refetch, } = useQuery({ queryKey: ['rules', selectedProgrammeFilter], queryFn: () => getRules( { page: 1, pageSize: 200 }, selectedProgrammeFilter !== 'all' ? selectedProgrammeFilter : undefined ), }); // Programme lookup helper const programmeMap = useMemo(() => { const map = new Map(); programmes.forEach((p) => map.set(String(p.id), p)); return map; }, [programmes]); // Mutation: Create or Update Rule const mutation = useMutation({ mutationFn: async (data: RuleFormValues) => { if (editingRule) { await updateRule({ ruleId: editingRule.id, programmeId: data.programmeId, name: data.name, description: data.description, status: data.status !== undefined ? Number(data.status) : ActivityStatus.Active, version: Number(data.version) || 1, userId: currentUserId, }); } else { await createRule({ programmeId: data.programmeId, name: data.name, description: data.description, userId: currentUserId, }); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['rules'] }); success( editingRule ? 'Rule Mutated' : 'Rule Created', editingRule ? 'Programme rule record mutated successfully.' : 'New programme rule registered and linked to programme.' ); setIsModalOpen(false); setEditingRule(null); if (onCloseCreateModalExternal) onCloseCreateModalExternal(); }, onError: (err: unknown) => { toastError('Save Failed', err); }, }); // Handlers const openCreateModal = (programmeId?: number | string) => { setEditingRule(null); ruleForm.reset({ programmeId: programmeId ? Number(programmeId) : selectedProgrammeFilter !== 'all' ? Number(selectedProgrammeFilter) : programmes[0]?.id ? Number(programmes[0].id) : 0, name: '', description: '', version: 1, status: ActivityStatus.Active, }); setIsModalOpen(true); }; const openEditModal = (rule: Rule) => { setEditingRule(rule); ruleForm.reset({ programmeId: Number(rule.programmeId), name: rule.name || '', description: rule.description || '', version: Number(rule.version) > 0 ? Number(rule.version) : 1, status: Number(rule.status), }); setIsModalOpen(true); }; const handleCloseModal = () => { setIsModalOpen(false); setEditingRule(null); if (onCloseCreateModalExternal) onCloseCreateModalExternal(); }; // Filter rules const filteredRules = useMemo(() => { return rules.filter((r) => { // Programme filter if (selectedProgrammeFilter !== 'all' && String(r.programmeId) !== String(selectedProgrammeFilter)) { return false; } // Status filter if (statusFilter !== 'all' && String(r.status) !== String(statusFilter)) { return false; } // Search term if (searchTerm.trim()) { const query = searchTerm.toLowerCase(); const pName = programmeMap.get(String(r.programmeId))?.name?.toLowerCase() || ''; const matchName = (r.name || '').toLowerCase().includes(query); const matchDesc = (r.description || '').toLowerCase().includes(query); const matchProg = pName.includes(query); if (!matchName && !matchDesc && !matchProg) { return false; } } return true; }); }, [rules, selectedProgrammeFilter, statusFilter, searchTerm, programmeMap]); // Sorting hook const { sortedData: sortedRules, sortColumn: ruleSortCol, sortDirection: ruleSortDir, handleSort: handleRuleSort, } = useTableSort(filteredRules, null, null, { name: (r: Rule) => r.name, programmeId: (r: Rule) => programmeMap.get(String(r.programmeId))?.name || `Programme #${r.programmeId}`, version: (r: Rule) => r.version || 1, status: (r: Rule) => getActivityStatusBadge(r.status).label, createdAt: (r: Rule) => r.createdAt, }); // Export columns definition const ruleExportColumns: ExportColumn[] = useMemo( () => [ { header: 'Rule ID', key: 'id' }, { header: 'Rule Name', key: 'name', format: (val) => val || 'Untitled Rule' }, { header: 'Strategic Programme', key: 'programmeId', format: (val) => programmeMap.get(String(val))?.name || `Programme #${val}`, }, { header: 'Version', key: 'version', format: (val) => `v${val || 1}` }, { header: 'Status', key: 'status', format: (val) => (Number(val) === ActivityStatus.Active ? 'Active' : 'Inactive'), }, { header: 'Rule Description', key: 'description', format: (val) => val || '—' }, { header: 'Created Date', key: 'createdAt', format: (val) => (val ? formatDate(val) : '—') }, ], [programmeMap] ); return (
{/* Top Filter and Actions Bar */}
{/* Search Input */}
setSearchTerm(e.target.value)} className="w-full pl-9 pr-4 py-2.5 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 text-slate-200 placeholder:text-slate-500 transition shadow-xs" />
{/* Programme Filter Dropdown */}
{/* Status Filter Dropdown */}
{/* Action Buttons: Refresh, Export & Add */}
{/* Rules Table & Cards */}

Programme Rules & Governance Directives ({filteredRules.length})

{filteredRules.length} of {rules.length} recorded
{isLoading ? (
Loading programme rules directory...
) : filteredRules.length === 0 ? (
{searchTerm || selectedProgrammeFilter !== 'all' || statusFilter !== 'all' ? 'No programme rules match the active filters.' : 'No programme rules registered yet. Link rules to programmes using the button below.'}
) : (
Rule Title & Directives Associated Programme Version Status Audit Info {sortedRules.map((r) => { const statusBadge = getActivityStatusBadge(r.status); const parentProg = programmeMap.get(String(r.programmeId)); return ( ); })}
Actions
{r.name}
{r.description && (
{r.description}
)}
{parentProg ? ( {parentProg.name} ) : ( Programme #{r.programmeId} )} v{r.version || 1} {statusBadge.label}
{formatDate(r.createdAt)}
{r.createdBy && (
{getUserEmail(r.createdBy)}
)}
)}
{/* Modal: Create / Edit Rule */}
mutation.mutate(data))} className="space-y-4" > {/* Associated Programme Dropdown (User must choose from dropdown!) */}
{ruleForm.formState.errors.programmeId && (

{ruleForm.formState.errors.programmeId.message}

)}
{/* Rule Name */}
{ruleForm.formState.errors.name && (

{ruleForm.formState.errors.name.message}

)}
{/* Description */}