import React, { useState, useMemo, useRef } from 'react'; import { Clock, Plus, FileText, Calendar, Users, Award, Gavel, ShieldAlert, Search, Filter, Paperclip, MapPin, User, Edit2, Trash2, Download, X, Tag, ArrowUpDown, Sparkles, ExternalLink, Zap, ChevronLeft, ChevronRight, Maximize2, Minimize2, Info, List, GitCommit, CheckCircle2, RotateCcw } from 'lucide-react'; import { ProjectStore, TimelineEvent, TimelineEventType, UserRole, Attachment } from '../types'; import { AttachmentUploader } from './AttachmentUploader'; import { generateDynamicTimeline } from '../utils/timelineGenerator'; interface TimelineViewProps { store: ProjectStore; onUpdateStore: (newStore: ProjectStore) => void; userRole?: UserRole; currentUserName?: string; onNavigateTab?: (tab: string) => void; } export const TimelineView: React.FC = ({ store, onUpdateStore, userRole = 'viewer', currentUserName = 'System User', onNavigateTab }) => { const canEdit = userRole === 'admin' || userRole === 'editor'; // Automatically and dynamically generate the unified timeline & circular registry from all portal updates const dynamicTimelineEvents = useMemo(() => { return generateDynamicTimeline(store); }, [store]); // View Mode: 'flow' (Horizontal Infographic Flow like Slide Deck), 'feed' (Vertical Detailed Feed), or 'circulars' (Table) const [activeViewMode, setActiveViewMode] = useState<'flow' | 'feed' | 'circulars'>('flow'); // Selected Event Modal Window State (Replaces confinement-prone hover balloons) const [selectedDetailEvent, setSelectedDetailEvent] = useState(null); // Search, Type, Source & Date Skip Filters const [searchQuery, setSearchQuery] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [sourceFilter, setSourceFilter] = useState('all'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); // Chronological order const [fromDateFilter, setFromDateFilter] = useState(''); // Date skip onwards (e.g. "2026-05-01") const [selectedQuickMonth, setSelectedQuickMonth] = useState('all'); // Manual Event Creation/Editing Modal State const [isModalOpen, setIsModalOpen] = useState(false); const [editingEvent, setEditingEvent] = useState | null>(null); // Scroll Container Ref for Horizontal Flow const scrollContainerRef = useRef(null); // Modal Form Inputs const [title, setTitle] = useState(''); const [eventType, setEventType] = useState('circular'); const [category, setCategory] = useState(''); const [date, setDate] = useState(new Date().toISOString().split('T')[0]); const [time, setTime] = useState('10:00'); const [referenceNumber, setReferenceNumber] = useState(''); const [location, setLocation] = useState(''); const [author, setAuthor] = useState(currentUserName); const [description, setDescription] = useState(''); const [attachments, setAttachments] = useState([]); // Open modal to create a new manual event or circular const handleOpenCreateModal = () => { setEditingEvent(null); setTitle(''); setEventType('circular'); setCategory('Official Governance Circular'); setDate(new Date().toISOString().split('T')[0]); setTime('10:00'); setReferenceNumber(`PIN-${new Date().getFullYear()}-${Math.floor(100 + Math.random() * 900)}`); setLocation('Executive Office'); setAuthor(currentUserName); setDescription(''); setAttachments([]); setIsModalOpen(true); }; // Open modal to edit existing manual event const handleOpenEditModal = (event: TimelineEvent) => { setEditingEvent(event); setTitle(event.title); setEventType(event.type); setCategory(event.category || ''); setDate(event.date); setTime(event.time || ''); setReferenceNumber(event.referenceNumber || ''); setLocation(event.location || ''); setAuthor(event.author || currentUserName); setDescription(event.description); setAttachments(event.attachments || []); setIsModalOpen(true); }; // Save manual event const handleSaveEvent = (e: React.FormEvent) => { e.preventDefault(); if (!title.trim() || !date) return; const currentManualEvents = store.timelineEvents || []; let updatedManualEvents = [...currentManualEvents]; if (editingEvent && editingEvent.id) { updatedManualEvents = updatedManualEvents.map((evt) => String(evt.id) === String(editingEvent.id) ? { ...evt, title: title.trim(), type: eventType, category: category.trim() || undefined, date, time: time.trim() || undefined, referenceNumber: referenceNumber.trim() || undefined, location: location.trim() || undefined, author: author.trim() || currentUserName, description: description.trim(), attachments } : evt ); } else { const nextId = (store.ids.timelineEvent || currentManualEvents.length + 1) + 1; const newEvt: TimelineEvent = { id: nextId, title: title.trim(), type: eventType, category: category.trim() || getTypeLabel(eventType), date, time: time.trim() || undefined, referenceNumber: referenceNumber.trim() || undefined, location: location.trim() || undefined, author: author.trim() || currentUserName, description: description.trim(), attachments, createdAt: new Date().toISOString(), isAutoGenerated: false, sourceType: 'manual' }; updatedManualEvents.push(newEvt); } onUpdateStore({ ...store, timelineEvents: updatedManualEvents, ids: { ...store.ids, timelineEvent: (store.ids.timelineEvent || currentManualEvents.length + 1) + 1 }, logs: [ { id: store.ids.log || Date.now(), action: editingEvent ? 'TIMELINE_EVENT_UPDATED' : 'TIMELINE_EVENT_CREATED', type: 'timeline', details: `${editingEvent ? 'Updated' : 'Created'} timeline publication: ${title.trim()} (${referenceNumber || eventType})`, user: currentUserName, timestamp: new Date().toISOString() }, ...store.logs ] }); setIsModalOpen(false); }; // Delete manual event const handleDeleteManualEvent = (id: string | number) => { if (!window.confirm('Are you sure you want to delete this custom timeline entry?')) return; const currentManualEvents = store.timelineEvents || []; const updatedManualEvents = currentManualEvents.filter((e) => String(e.id) !== String(id)); onUpdateStore({ ...store, timelineEvents: updatedManualEvents, logs: [ { id: store.ids.log || Date.now(), action: 'TIMELINE_EVENT_DELETED', type: 'timeline', details: `Deleted manual timeline entry ID #${id}`, user: currentUserName, timestamp: new Date().toISOString() }, ...store.logs ] }); }; // Unique list of available Months for Quick-Jump Picker const availableMonths = useMemo(() => { const monthSet = new Map(); // key YYYY-MM, label e.g. "May 2026" dynamicTimelineEvents.forEach((e) => { if (e.date && e.date.length >= 7) { const ym = e.date.substring(0, 7); const [y, m] = ym.split('-'); if (y && m) { const dateObj = new Date(parseInt(y), parseInt(m) - 1, 1); const label = dateObj.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); monthSet.set(ym, label); } } }); return Array.from(monthSet.entries()).sort((a, b) => a[0].localeCompare(b[0])); }, [dynamicTimelineEvents]); // Filter & Sort Dynamic Timeline Events chronologically based on set date & skip filters const filteredEvents = useMemo(() => { return dynamicTimelineEvents .filter((evt) => { if (activeViewMode === 'circulars') { const isCirc = evt.type === 'circular' || (evt.category && evt.category.toLowerCase().includes('circular')) || (evt.referenceNumber && evt.referenceNumber.toLowerCase().includes('pin-')) || evt.title.toLowerCase().includes('circular'); if (!isCirc) return false; } if (typeFilter !== 'all') { if (typeFilter === 'circular' && evt.type !== 'circular') return false; if (typeFilter === 'meeting' && evt.type !== 'meeting') return false; if (typeFilter === 'decision' && evt.type !== 'decision') return false; if (typeFilter === 'milestone' && evt.type !== 'milestone') return false; if (typeFilter === 'audit' && evt.type !== 'audit') return false; } if (sourceFilter !== 'all') { if (sourceFilter === 'manual' && evt.isAutoGenerated) return false; if (sourceFilter === 'auto' && !evt.isAutoGenerated) return false; if (evt.sourceTab && evt.sourceTab !== sourceFilter && sourceFilter !== 'manual' && sourceFilter !== 'auto') { return false; } } // Date Skip Onwards Filter if (fromDateFilter && evt.date < fromDateFilter) { return false; } // Quick Month Filter if (selectedQuickMonth !== 'all') { if (!evt.date.startsWith(selectedQuickMonth)) { return false; } } if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); const matchTitle = evt.title.toLowerCase().includes(q); const matchDesc = evt.description.toLowerCase().includes(q); const matchRef = evt.referenceNumber ? evt.referenceNumber.toLowerCase().includes(q) : false; const matchCat = evt.category ? evt.category.toLowerCase().includes(q) : false; const matchAuthor = evt.author ? evt.author.toLowerCase().includes(q) : false; const matchLoc = evt.location ? evt.location.toLowerCase().includes(q) : false; const matchAtt = evt.attachments ? evt.attachments.some((a) => a.name.toLowerCase().includes(q)) : false; return matchTitle || matchDesc || matchRef || matchCat || matchAuthor || matchLoc || matchAtt; } return true; }) .sort((a, b) => { const timeA = a.time || '00:00'; const timeB = b.time || '00:00'; const dateA = new Date(`${a.date}T${timeA}`).getTime(); const dateB = new Date(`${b.date}T${timeB}`).getTime(); if (isNaN(dateA) || isNaN(dateB)) return 0; return sortOrder === 'asc' ? dateA - dateB : dateB - dateA; }); }, [ dynamicTimelineEvents, activeViewMode, typeFilter, sourceFilter, fromDateFilter, selectedQuickMonth, searchQuery, sortOrder ]); // Scroll to Specific Event Node in Horizontal Flow const handleJumpToEvent = (eventId: string | number) => { const nodeElem = document.getElementById(`timeline-node-${eventId}`); if (nodeElem) { nodeElem.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); setPinnedEventId(eventId); } }; // Horizontal Scroll Controls const handleScrollLeft = () => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollBy({ left: -360, behavior: 'smooth' }); } }; const handleScrollRight = () => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollBy({ left: 360, behavior: 'smooth' }); } }; // Analytics Metrics const autoGeneratedCount = dynamicTimelineEvents.filter((e) => e.isAutoGenerated).length; const manualCount = dynamicTimelineEvents.filter((e) => !e.isAutoGenerated).length; const circularsCount = dynamicTimelineEvents.filter( (e) => e.type === 'circular' || (e.category && e.category.toLowerCase().includes('circular')) || (e.referenceNumber && e.referenceNumber.toLowerCase().includes('pin-')) ).length; const meetingsCount = dynamicTimelineEvents.filter((e) => e.type === 'meeting').length; const decisionsCount = dynamicTimelineEvents.filter((e) => e.type === 'decision').length; const docsCount = dynamicTimelineEvents.reduce((acc, e) => acc + (e.attachments ? e.attachments.length : 0), 0); // Helper Badge Colors & Formatting function getTypeBadge(type: TimelineEventType) { switch (type) { case 'circular': return 'bg-cyan-500/10 text-cyan-400 border-cyan-500/30'; case 'meeting': return 'bg-amber-500/10 text-amber-400 border-amber-500/30'; case 'milestone': return 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30'; case 'decision': return 'bg-purple-500/10 text-purple-400 border-purple-500/30'; case 'release': case 'audit': return 'bg-rose-500/10 text-rose-400 border-rose-500/30'; default: return 'bg-slate-800 text-slate-300 border-slate-700'; } } function getTypeIcon(type: TimelineEventType) { switch (type) { case 'circular': return ; case 'meeting': return ; case 'milestone': return ; case 'decision': return ; case 'release': case 'audit': return ; default: return ; } } function getTypeLabel(type: TimelineEventType) { switch (type) { case 'circular': return 'Governance Circular'; case 'meeting': return 'Committee Session'; case 'milestone': return 'Project Milestone'; case 'decision': return 'Executive Decision'; case 'release': return 'Official Release'; case 'audit': return 'Risk & Audit Alert'; default: return 'Portal Update'; } } function getSourceLabel(sourceTab?: string, isAuto?: boolean) { if (!isAuto) return 'Manual Publication'; switch (sourceTab) { case 'meetings': return 'Meetings Register'; case 'decisions': return 'Executive Decisions'; case 'phases': return 'Phase Deliverables'; case 'risks': return 'Risk Register'; default: return 'Portal Activity'; } } // Helper to format date string nicely (e.g. "May 13, 2026") function formatDateNice(dateStr: string) { try { const [y, m, d] = dateStr.split('-'); if (!y || !m || !d) return dateStr; const dateObj = new Date(parseInt(y), parseInt(m) - 1, parseInt(d)); return dateObj.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } catch { return dateStr; } } // Extract caption for node display (truncated only after 50 words) function getTruncatedCaption(description: string, maxWords: number = 50) { if (!description) return ''; const words = description.trim().split(/\s+/); if (words.length <= maxWords) return description; return words.slice(0, maxWords).join(' ') + '...'; } return (
{/* Page Header */}

Project Flow – Interactive Timeline

Auto-Generated

Compact presentation-grade timeline flow. Hover or click any node for details.

{/* View Mode Switcher */}
{canEdit && ( )}
{/* Analytics Summary Cards */}
Total Timeline Events
{dynamicTimelineEvents.length}
{autoGeneratedCount} auto {manualCount} manual
Circulars Issued
{circularsCount}
Governance & directive publications
Committee Sessions
{meetingsCount}
Meetings & committee minutes
Executive Decisions
{decisionsCount}
Board & PMT resolutions
Attachments & PDFs
{docsCount}
Attached files & documents
{/* FILTER & SKIP-TO-DATE CONTROL PANEL */}
{/* Row 1: Search, Type Badges & Source Dropdown */}
setSearchQuery(e.target.value)} placeholder="Search reference, title, author, attachments..." className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-3 py-2 text-xs text-slate-200 focus:outline-none focus:border-amber-500 transition" /> {searchQuery && ( )}
{[ { id: 'all', label: 'All Updates' }, { id: 'circular', label: 'Circulars' }, { id: 'meeting', label: 'Meetings' }, { id: 'decision', label: 'Decisions' }, { id: 'milestone', label: 'Milestones' }, { id: 'audit', label: 'Risks / Audits' } ].map((tab) => ( ))}
{/* Row 2: SKIP TO DATE & QUICK MONTH JUMP PANEL */}
Skip / Onwards Date:
{/* Skip Date Onwards Input */} setFromDateFilter(e.target.value)} className="bg-slate-950 border border-slate-800 text-slate-200 rounded-lg px-2.5 py-1 text-xs focus:outline-none focus:border-amber-500 cursor-pointer" title="Filter events on or after this date" /> {/* Quick Month Filter Chips */}
{availableMonths.map(([ym, label]) => ( ))}
{(fromDateFilter || selectedQuickMonth !== 'all') && ( )}
{/* VIEW 1: INFOGRAPHIC PROCESS FLOW (BOXED LAYOUT WITH MODAL DETAILS) */} {activeViewMode === 'flow' && (
{/* Controls Bar for Horizontal Timeline */}
Interactive Timeline Flow ({filteredEvents.length} chronological milestones • Click any node or card to view modal)
{filteredEvents.length === 0 ? (
No events match your selected date or filters
{(fromDateFilter || selectedQuickMonth !== 'all') && ( )}
) : (
{/* Central Vector Arrow Line spanning full scroll width */}
{/* Arrow Head */}
{filteredEvents.map((evt, idx) => { const isTop = idx % 2 === 0; const formattedDate = formatDateNice(evt.date); const caption = getTruncatedCaption(evt.description, 50); return (
setSelectedDetailEvent(evt)} > {/* TOP CALLOUT CARD (For Even Index) */} {isTop && (
{formattedDate}
{evt.title}
{caption &&
{caption}
}
Click for Details
)} {/* NODE DOT ON THE TIMELINE VECTOR */}
{/* BOTTOM CALLOUT CARD (For Odd Index) */} {!isTop && (
{formattedDate}
{evt.title}
{caption &&
{caption}
}
Click for Details
)}
); })}
)}
)} {/* VIEW 2: VERTICAL DETAILED FEED */} {activeViewMode === 'feed' && (
{filteredEvents.map((evt) => (
setSelectedDetailEvent(evt)} >
{getTypeLabel(evt.type)} {evt.referenceNumber && ( Ref: {evt.referenceNumber} )} {getSourceLabel(evt.sourceTab, evt.isAutoGenerated)} {evt.date}

{evt.title}

{evt.description}
))}
)} {/* VIEW 3: OFFICIAL CIRCULARS REGISTRY */} {activeViewMode === 'circulars' && (
{filteredEvents.map((circ) => ( setSelectedDetailEvent(circ)} > ))}
Reference PIN Set Date Circular Title & Subject Issuing Authority Actions
{circ.referenceNumber || 'PIN-OFFICIAL'} {circ.date} {circ.title} {circ.author || 'Executive Office'}
)} {/* MODAL WINDOW FOR EVENT DETAILS (Replaces Confinement-Prone Hover Balloons) */} {selectedDetailEvent && (
setSelectedDetailEvent(null)} >
e.stopPropagation()} > {/* Header */}
{getTypeLabel(selectedDetailEvent.type)} {selectedDetailEvent.referenceNumber && ( Ref: {selectedDetailEvent.referenceNumber} )} {selectedDetailEvent.date}

{selectedDetailEvent.title}

{/* Metadata Grid */}
Publication Date {selectedDetailEvent.date}
Authority / Author {selectedDetailEvent.author || 'Governance Office'}
Source Classification {getSourceLabel(selectedDetailEvent.sourceTab, selectedDetailEvent.isAutoGenerated)}
{selectedDetailEvent.location && (
Venue / Scope {selectedDetailEvent.location}
)}
{/* Description / Content Box */}
{selectedDetailEvent.description || 'No detailed text recorded for this milestone.'}
{/* Attachments Section */} {selectedDetailEvent.attachments && selectedDetailEvent.attachments.length > 0 && (
Attached Documents ({selectedDetailEvent.attachments.length})
{selectedDetailEvent.attachments.map((att) => (
{att.name}
))}
)} {/* Footer Navigation & Actions */}
{selectedDetailEvent.isAutoGenerated && selectedDetailEvent.sourceTab && onNavigateTab && ( )}
)} {/* MODAL FOR MANUAL CREATION */} {isModalOpen && (

Create Custom Publication / Circular

setTitle(e.target.value)} placeholder="e.g. Published Governance Circular PIN-2026-003" required className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2.5 text-slate-200 focus:outline-none focus:border-amber-500" />
setDate(e.target.value)} required className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2.5 text-slate-200 focus:outline-none focus:border-amber-500" />
setReferenceNumber(e.target.value)} placeholder="e.g. PIN-2026-003" className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2.5 text-amber-300 font-mono focus:outline-none focus:border-amber-500" />