Pinnacle/src/components/TimelineView.tsx
2026-08-07 13:00:15 +05:00

1154 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<TimelineViewProps> = ({
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<TimelineEvent | null>(null);
// Search, Type, Source & Date Skip Filters
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [sourceFilter, setSourceFilter] = useState<string>('all');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); // Chronological order
const [fromDateFilter, setFromDateFilter] = useState<string>(''); // Date skip onwards (e.g. "2026-05-01")
const [selectedQuickMonth, setSelectedQuickMonth] = useState<string>('all');
// Manual Event Creation/Editing Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingEvent, setEditingEvent] = useState<Partial<TimelineEvent> | null>(null);
// Scroll Container Ref for Horizontal Flow
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Modal Form Inputs
const [title, setTitle] = useState('');
const [eventType, setEventType] = useState<TimelineEventType>('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<Attachment[]>([]);
// 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<string, string>(); // 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 <FileText className="w-3.5 h-3.5 text-cyan-400" />;
case 'meeting':
return <Users className="w-3.5 h-3.5 text-amber-400" />;
case 'milestone':
return <Award className="w-3.5 h-3.5 text-emerald-400" />;
case 'decision':
return <Gavel className="w-3.5 h-3.5 text-purple-400" />;
case 'release':
case 'audit':
return <ShieldAlert className="w-3.5 h-3.5 text-rose-400" />;
default:
return <Clock className="w-3.5 h-3.5 text-slate-400" />;
}
}
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 (
<div className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6 min-w-0">
{/* Page Header */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 bg-slate-900 border border-slate-800/80 p-6 rounded-3xl shadow-xl">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<div className="p-2.5 bg-amber-500/10 border border-amber-500/30 rounded-2xl text-amber-400">
<Clock className="w-6 h-6" />
</div>
<div>
<div className="flex items-center space-x-2">
<h1 className="text-xl font-bold text-slate-100">Project Flow Interactive Timeline</h1>
<span className="px-2.5 py-0.5 bg-amber-500/10 text-amber-400 border border-amber-500/30 rounded-full text-[10px] font-bold flex items-center space-x-1">
<Zap className="w-3 h-3 text-amber-400 animate-pulse" />
<span>Auto-Generated</span>
</span>
</div>
<p className="text-xs text-slate-400 mt-0.5">
Compact presentation-grade timeline flow. Hover or click any node for details.
</p>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-2.5 self-start md:self-auto shrink-0">
{/* View Mode Switcher */}
<div className="bg-slate-950 p-1 rounded-xl border border-slate-800 flex items-center space-x-1 text-xs">
<button
onClick={() => setActiveViewMode('flow')}
className={`px-3 py-1.5 rounded-lg font-bold flex items-center space-x-1.5 transition cursor-pointer ${
activeViewMode === 'flow'
? 'bg-amber-500 text-slate-950 shadow'
: 'text-slate-400 hover:text-slate-200'
}`}
>
<GitCommit className="w-3.5 h-3.5" />
<span>Process Flow</span>
</button>
<button
onClick={() => setActiveViewMode('feed')}
className={`px-3 py-1.5 rounded-lg font-bold flex items-center space-x-1.5 transition cursor-pointer ${
activeViewMode === 'feed'
? 'bg-amber-500 text-slate-950 shadow'
: 'text-slate-400 hover:text-slate-200'
}`}
>
<List className="w-3.5 h-3.5" />
<span>Detailed Feed</span>
</button>
<button
onClick={() => setActiveViewMode('circulars')}
className={`px-3 py-1.5 rounded-lg font-bold flex items-center space-x-1.5 transition cursor-pointer ${
activeViewMode === 'circulars'
? 'bg-cyan-500 text-slate-950 shadow'
: 'text-slate-400 hover:text-slate-200'
}`}
>
<FileText className="w-3.5 h-3.5" />
<span>Circulars</span>
<span className="px-1.5 py-0.2 bg-slate-900/60 text-[10px] rounded font-mono">{circularsCount}</span>
</button>
</div>
{canEdit && (
<button
onClick={handleOpenCreateModal}
className="px-4 py-2 bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold rounded-xl text-xs flex items-center justify-center space-x-1.5 shadow-lg transition cursor-pointer"
>
<Plus className="w-4 h-4" />
<span>Create Event / Circular</span>
</button>
)}
</div>
</div>
{/* Analytics Summary Cards */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3.5">
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl space-y-1">
<div className="flex items-center justify-between text-xs text-slate-400">
<span>Total Timeline Events</span>
<Clock className="w-4 h-4 text-amber-400" />
</div>
<div className="text-2xl font-extrabold text-slate-100">{dynamicTimelineEvents.length}</div>
<div className="text-[10px] text-slate-500 flex items-center space-x-1">
<span className="text-amber-400 font-semibold">{autoGeneratedCount} auto</span>
<span></span>
<span>{manualCount} manual</span>
</div>
</div>
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl space-y-1">
<div className="flex items-center justify-between text-xs text-slate-400">
<span>Circulars Issued</span>
<FileText className="w-4 h-4 text-cyan-400" />
</div>
<div className="text-2xl font-extrabold text-cyan-400">{circularsCount}</div>
<div className="text-[10px] text-slate-500">Governance & directive publications</div>
</div>
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl space-y-1">
<div className="flex items-center justify-between text-xs text-slate-400">
<span>Committee Sessions</span>
<Users className="w-4 h-4 text-amber-400" />
</div>
<div className="text-2xl font-extrabold text-amber-400">{meetingsCount}</div>
<div className="text-[10px] text-slate-500">Meetings & committee minutes</div>
</div>
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl space-y-1">
<div className="flex items-center justify-between text-xs text-slate-400">
<span>Executive Decisions</span>
<Gavel className="w-4 h-4 text-purple-400" />
</div>
<div className="text-2xl font-extrabold text-purple-400">{decisionsCount}</div>
<div className="text-[10px] text-slate-500">Board & PMT resolutions</div>
</div>
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl col-span-2 md:col-span-1 space-y-1">
<div className="flex items-center justify-between text-xs text-slate-400">
<span>Attachments & PDFs</span>
<Paperclip className="w-4 h-4 text-emerald-400" />
</div>
<div className="text-2xl font-extrabold text-emerald-400">{docsCount}</div>
<div className="text-[10px] text-slate-500">Attached files & documents</div>
</div>
</div>
{/* FILTER & SKIP-TO-DATE CONTROL PANEL */}
<div className="bg-slate-900 border border-slate-800 p-4 rounded-2xl space-y-3 shadow-lg">
{/* Row 1: Search, Type Badges & Source Dropdown */}
<div className="flex flex-col md:flex-row gap-3 items-center justify-between">
<div className="relative w-full md:w-80">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
<input
type="text"
value={searchQuery}
onChange={(e) => 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 && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-slate-300"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
<div className="flex items-center space-x-1 overflow-x-auto w-full md:w-auto pb-1 md:pb-0 text-xs">
{[
{ 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) => (
<button
key={tab.id}
onClick={() => setTypeFilter(tab.id)}
className={`px-3 py-1.5 rounded-lg font-medium whitespace-nowrap transition cursor-pointer ${
typeFilter === tab.id
? 'bg-amber-500 text-slate-950 font-bold shadow'
: 'bg-slate-950 text-slate-400 hover:bg-slate-800 hover:text-slate-200 border border-slate-800'
}`}
>
{tab.label}
</button>
))}
<select
value={sourceFilter}
onChange={(e) => setSourceFilter(e.target.value)}
className="px-2.5 py-1.5 bg-slate-950 text-slate-300 rounded-lg border border-slate-800 font-medium text-xs focus:outline-none focus:border-amber-500 ml-2 cursor-pointer"
>
<option value="all">All Sources</option>
<option value="auto">Auto-Generated</option>
<option value="manual">Manual</option>
<option value="meetings">Meetings Tab</option>
<option value="decisions">Decisions Tab</option>
<option value="phases">Phases Tab</option>
<option value="risks">Risks Tab</option>
</select>
<button
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
className="px-3 py-1.5 bg-slate-950 hover:bg-slate-800 text-slate-300 rounded-lg border border-slate-800 flex items-center space-x-1 font-medium transition cursor-pointer ml-auto shrink-0"
title="Sort Order based on Set Date"
>
<ArrowUpDown className="w-3.5 h-3.5 text-amber-400" />
<span className="text-[11px] font-semibold">{sortOrder === 'asc' ? 'Oldest First' : 'Newest First'}</span>
</button>
</div>
</div>
{/* Row 2: SKIP TO DATE & QUICK MONTH JUMP PANEL */}
<div className="pt-2 border-t border-slate-800/80 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 text-xs">
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center space-x-1.5 text-amber-400 font-semibold bg-amber-500/10 px-2.5 py-1 rounded-lg border border-amber-500/20">
<Calendar className="w-3.5 h-3.5 text-amber-400" />
<span>Skip / Onwards Date:</span>
</div>
{/* Skip Date Onwards Input */}
<input
type="date"
value={fromDateFilter}
onChange={(e) => 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 */}
<div className="flex items-center space-x-1 overflow-x-auto max-w-full">
<button
onClick={() => {
setSelectedQuickMonth('all');
setFromDateFilter('');
}}
className={`px-2.5 py-1 rounded-lg text-[11px] font-medium transition cursor-pointer whitespace-nowrap ${
selectedQuickMonth === 'all' && !fromDateFilter
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40 font-bold'
: 'bg-slate-950 text-slate-400 hover:text-slate-200 border border-slate-800'
}`}
>
All Dates
</button>
{availableMonths.map(([ym, label]) => (
<button
key={ym}
onClick={() => {
setSelectedQuickMonth(ym);
setFromDateFilter('');
}}
className={`px-2.5 py-1 rounded-lg text-[11px] font-medium transition cursor-pointer whitespace-nowrap ${
selectedQuickMonth === ym
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40 font-bold'
: 'bg-slate-950 text-slate-400 hover:text-slate-200 border border-slate-800'
}`}
>
{label}
</button>
))}
</div>
{(fromDateFilter || selectedQuickMonth !== 'all') && (
<button
onClick={() => {
setFromDateFilter('');
setSelectedQuickMonth('all');
}}
className="px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-[10px] flex items-center space-x-1 transition cursor-pointer"
title="Reset Date Skip Filter"
>
<RotateCcw className="w-3 h-3 text-amber-400" />
<span>Reset Date Skip</span>
</button>
)}
</div>
</div>
</div>
{/* VIEW 1: INFOGRAPHIC PROCESS FLOW (BOXED LAYOUT WITH MODAL DETAILS) */}
{activeViewMode === 'flow' && (
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-6 shadow-2xl space-y-6 relative overflow-hidden">
{/* Controls Bar for Horizontal Timeline */}
<div className="flex items-center justify-between text-xs text-slate-400 border-b border-slate-800/80 pb-3">
<div className="flex items-center space-x-2">
<Sparkles className="w-4 h-4 text-emerald-400" />
<span className="font-bold text-slate-200">Interactive Timeline Flow</span>
<span className="text-slate-500">
({filteredEvents.length} chronological milestones Click any node or card to view modal)
</span>
</div>
<div className="flex items-center space-x-2">
<button
onClick={handleScrollLeft}
className="p-1.5 bg-slate-950 hover:bg-slate-800 border border-slate-800 text-slate-300 rounded-xl transition cursor-pointer"
title="Scroll Left"
>
<ChevronLeft className="w-4 h-4" />
</button>
<button
onClick={handleScrollRight}
className="p-1.5 bg-slate-950 hover:bg-slate-800 border border-slate-800 text-slate-300 rounded-xl transition cursor-pointer"
title="Scroll Right"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
{filteredEvents.length === 0 ? (
<div className="py-16 text-center space-y-3">
<div className="w-12 h-12 rounded-2xl bg-slate-800 text-slate-500 mx-auto flex items-center justify-center">
<Clock className="w-6 h-6" />
</div>
<div className="text-sm font-semibold text-slate-300">No events match your selected date or filters</div>
{(fromDateFilter || selectedQuickMonth !== 'all') && (
<button
onClick={() => {
setFromDateFilter('');
setSelectedQuickMonth('all');
}}
className="px-4 py-1.5 bg-amber-500 text-slate-950 font-bold rounded-xl text-xs shadow inline-block cursor-pointer"
>
Clear Date Filters
</button>
)}
</div>
) : (
<div
ref={scrollContainerRef}
className="relative overflow-x-auto scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent pt-52 pb-52 px-8 min-h-[540px]"
>
<div className="relative flex items-center space-x-32 min-w-max py-4 z-10">
{/* Central Vector Arrow Line spanning full scroll width */}
<div className="absolute top-1/2 left-0 right-4 -translate-y-1/2 h-1 bg-gradient-to-r from-emerald-500/60 via-amber-500/80 to-emerald-400/90 rounded-full z-0 pointer-events-none" />
{/* Arrow Head */}
<div className="absolute top-1/2 right-0 -translate-y-1/2 text-emerald-400 z-0 pointer-events-none">
<ChevronRight className="w-7 h-7 -ml-3 stroke-[3]" />
</div>
{filteredEvents.map((evt, idx) => {
const isTop = idx % 2 === 0;
const formattedDate = formatDateNice(evt.date);
const caption = getTruncatedCaption(evt.description, 50);
return (
<div
key={evt.id}
id={`timeline-node-${evt.id}`}
className="relative flex flex-col items-center shrink-0 w-80 group cursor-pointer"
onClick={() => setSelectedDetailEvent(evt)}
>
{/* TOP CALLOUT CARD (For Even Index) */}
{isTop && (
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 w-80 text-center space-y-1 bg-slate-950/95 border border-slate-800 group-hover:border-amber-500/80 p-4 rounded-2xl shadow-xl transition-all duration-200 transform group-hover:-translate-y-1.5 group-hover:bg-slate-900">
<div className="text-xs font-bold text-emerald-400 tracking-wide flex items-center justify-center space-x-1">
<span>{formattedDate}</span>
</div>
<div className="text-xs font-bold text-slate-100 group-hover:text-amber-400 transition leading-snug">{evt.title}</div>
{caption && <div className="text-[11px] text-slate-400 italic leading-snug line-clamp-6">{caption}</div>}
<div className="pt-1 text-[10px] text-amber-400 font-semibold opacity-0 group-hover:opacity-100 transition flex items-center justify-center space-x-1">
<span>Click for Details</span>
<Info className="w-3 h-3" />
</div>
<div className="w-0.5 h-8 bg-emerald-500/40 group-hover:bg-amber-500/80 mx-auto absolute -bottom-8 left-1/2 -translate-x-1/2 transition" />
</div>
)}
{/* NODE DOT ON THE TIMELINE VECTOR */}
<div
className={`w-8 h-8 rounded-full border-3 flex items-center justify-center shadow-lg transition-all duration-300 z-20 group-hover:scale-125 group-hover:border-white group-hover:bg-amber-400 group-hover:text-slate-950 group-hover:ring-4 group-hover:ring-amber-500/30 ${
evt.type === 'circular'
? 'bg-slate-950 border-cyan-400 text-cyan-400'
: evt.type === 'meeting'
? 'bg-slate-950 border-amber-400 text-amber-400'
: evt.type === 'milestone'
? 'bg-slate-950 border-emerald-400 text-emerald-400'
: evt.type === 'decision'
? 'bg-slate-950 border-purple-400 text-purple-400'
: 'bg-slate-950 border-rose-400 text-rose-400'
}`}
title={`${evt.title} (${evt.date}) - Click for details`}
>
<div className="w-2.5 h-2.5 rounded-full bg-current" />
</div>
{/* BOTTOM CALLOUT CARD (For Odd Index) */}
{!isTop && (
<div className="absolute top-12 left-1/2 -translate-x-1/2 w-80 text-center space-y-1 bg-slate-950/95 border border-slate-800 group-hover:border-amber-500/80 p-4 rounded-2xl shadow-xl transition-all duration-200 transform group-hover:translate-y-1.5 group-hover:bg-slate-900">
<div className="w-0.5 h-8 bg-emerald-500/40 group-hover:bg-amber-500/80 mx-auto absolute -top-8 left-1/2 -translate-x-1/2 transition" />
<div className="text-xs font-bold text-emerald-400 tracking-wide">{formattedDate}</div>
<div className="text-xs font-bold text-slate-100 group-hover:text-amber-400 transition leading-snug">{evt.title}</div>
{caption && <div className="text-[11px] text-slate-400 italic leading-snug line-clamp-6">{caption}</div>}
<div className="pt-1 text-[10px] text-amber-400 font-semibold opacity-0 group-hover:opacity-100 transition flex items-center justify-center space-x-1">
<span>Click for Details</span>
<Info className="w-3 h-3" />
</div>
</div>
)}
</div>
);
})}
</div>
</div>
)}
</div>
)}
{/* VIEW 2: VERTICAL DETAILED FEED */}
{activeViewMode === 'feed' && (
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-6 shadow-xl relative">
<div className="relative pl-6 md:pl-8 border-l-2 border-slate-800 space-y-8 my-2">
{filteredEvents.map((evt) => (
<div
key={evt.id}
className="relative group cursor-pointer"
onClick={() => setSelectedDetailEvent(evt)}
>
<div
className={`absolute -left-[31px] md:-left-[39px] top-1.5 w-6 h-6 rounded-full border-2 flex items-center justify-center shadow-lg transition-transform group-hover:scale-125 ${
evt.type === 'circular'
? 'bg-slate-900 border-cyan-400 text-cyan-400'
: evt.type === 'meeting'
? 'bg-slate-900 border-amber-400 text-amber-400'
: evt.type === 'milestone'
? 'bg-slate-900 border-emerald-400 text-emerald-400'
: evt.type === 'decision'
? 'bg-slate-900 border-purple-400 text-purple-400'
: 'bg-slate-900 border-rose-400 text-rose-400'
}`}
>
<div className="w-2 h-2 rounded-full bg-current" />
</div>
<div className="bg-slate-950/90 border border-slate-800/90 group-hover:border-amber-500/60 rounded-2xl p-5 space-y-3 transition">
<div className="flex flex-wrap items-start justify-between gap-2 border-b border-slate-800/60 pb-3">
<div className="space-y-1.5 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className={`px-2.5 py-0.5 rounded-lg text-[10px] font-bold border ${getTypeBadge(evt.type)}`}>
{getTypeLabel(evt.type)}
</span>
{evt.referenceNumber && (
<span className="px-2 py-0.5 bg-slate-800 text-amber-300 font-mono text-[10px] rounded border border-slate-700 font-bold">
Ref: {evt.referenceNumber}
</span>
)}
<span className="px-2 py-0.5 bg-slate-900 text-slate-300 rounded text-[10px] border border-slate-800">
{getSourceLabel(evt.sourceTab, evt.isAutoGenerated)}
</span>
<span className="text-xs text-amber-400 font-semibold ml-auto">{evt.date}</span>
</div>
<h3 className="text-base font-bold text-slate-100 group-hover:text-amber-400 transition">{evt.title}</h3>
</div>
<button
onClick={(e) => {
e.stopPropagation();
setSelectedDetailEvent(evt);
}}
className="px-3 py-1 bg-amber-500/10 hover:bg-amber-500 text-amber-400 hover:text-slate-950 border border-amber-500/30 rounded-xl text-xs font-bold flex items-center space-x-1 transition cursor-pointer"
>
<Info className="w-3.5 h-3.5" />
<span>Details</span>
</button>
</div>
<div className="text-xs text-slate-300 leading-relaxed whitespace-pre-line bg-slate-950 p-3 rounded-xl border border-slate-800/80 line-clamp-3">
{evt.description}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* VIEW 3: OFFICIAL CIRCULARS REGISTRY */}
{activeViewMode === 'circulars' && (
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-6 shadow-xl space-y-4">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-950/60">
<th className="p-3">Reference PIN</th>
<th className="p-3">Set Date</th>
<th className="p-3">Circular Title & Subject</th>
<th className="p-3">Issuing Authority</th>
<th className="p-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{filteredEvents.map((circ) => (
<tr
key={circ.id}
className="hover:bg-slate-800/40 transition cursor-pointer"
onClick={() => setSelectedDetailEvent(circ)}
>
<td className="p-3 font-mono text-amber-300 font-bold whitespace-nowrap">
{circ.referenceNumber || 'PIN-OFFICIAL'}
</td>
<td className="p-3 text-slate-300 font-medium whitespace-nowrap">{circ.date}</td>
<td className="p-3 text-slate-100 font-bold min-w-[220px]">{circ.title}</td>
<td className="p-3 text-slate-300 whitespace-nowrap font-medium">{circ.author || 'Executive Office'}</td>
<td className="p-3 text-right whitespace-nowrap">
<button
onClick={(e) => {
e.stopPropagation();
setSelectedDetailEvent(circ);
}}
className="px-2.5 py-1 bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 border border-cyan-500/30 rounded-lg text-[11px] font-bold inline-flex items-center space-x-1 transition cursor-pointer"
>
<Info className="w-3 h-3" />
<span>View Details</span>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* MODAL WINDOW FOR EVENT DETAILS (Replaces Confinement-Prone Hover Balloons) */}
{selectedDetailEvent && (
<div
className="fixed inset-0 bg-slate-950/80 backdrop-blur-md z-50 flex items-center justify-center p-4 overflow-y-auto animate-in fade-in duration-200"
onClick={() => setSelectedDetailEvent(null)}
>
<div
className="bg-slate-900 border-2 border-amber-500/60 rounded-3xl max-w-2xl w-full p-6 shadow-2xl space-y-5 my-8 relative text-left"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-start justify-between border-b border-slate-800 pb-4">
<div className="space-y-1.5 pr-4">
<div className="flex flex-wrap items-center gap-2">
<span className={`px-2.5 py-0.5 rounded-lg text-[10px] font-bold border ${getTypeBadge(selectedDetailEvent.type)}`}>
{getTypeLabel(selectedDetailEvent.type)}
</span>
{selectedDetailEvent.referenceNumber && (
<span className="px-2 py-0.5 bg-slate-800 text-amber-300 font-mono text-[10px] rounded border border-slate-700 font-bold">
Ref: {selectedDetailEvent.referenceNumber}
</span>
)}
<span className="px-2 py-0.5 bg-slate-950 text-slate-400 font-mono text-[10px] rounded border border-slate-800">
{selectedDetailEvent.date}
</span>
</div>
<h2 className="text-lg font-bold text-slate-100">{selectedDetailEvent.title}</h2>
</div>
<button
onClick={() => setSelectedDetailEvent(null)}
className="p-1.5 rounded-xl bg-slate-950 text-slate-400 hover:text-white border border-slate-800 hover:bg-slate-800 transition cursor-pointer"
title="Close Dialog"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Metadata Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 text-xs bg-slate-950 p-3.5 rounded-2xl border border-slate-800/80">
<div>
<span className="text-slate-500 block text-[10px]">Publication Date</span>
<strong className="text-amber-400 font-semibold">{selectedDetailEvent.date}</strong>
</div>
<div>
<span className="text-slate-500 block text-[10px]">Authority / Author</span>
<strong className="text-slate-200">{selectedDetailEvent.author || 'Governance Office'}</strong>
</div>
<div>
<span className="text-slate-500 block text-[10px]">Source Classification</span>
<span className="text-slate-300">{getSourceLabel(selectedDetailEvent.sourceTab, selectedDetailEvent.isAutoGenerated)}</span>
</div>
{selectedDetailEvent.location && (
<div className="col-span-2 sm:col-span-3 pt-1 border-t border-slate-800/60">
<span className="text-slate-500 block text-[10px]">Venue / Scope</span>
<span className="text-slate-200">{selectedDetailEvent.location}</span>
</div>
)}
</div>
{/* Description / Content Box */}
<div className="space-y-1.5">
<label className="text-[11px] font-bold text-slate-400">Directive Details & Context</label>
<div className="text-xs text-slate-200 leading-relaxed bg-slate-950 p-4 rounded-2xl border border-slate-800 max-h-60 overflow-y-auto whitespace-pre-line">
{selectedDetailEvent.description || 'No detailed text recorded for this milestone.'}
</div>
</div>
{/* Attachments Section */}
{selectedDetailEvent.attachments && selectedDetailEvent.attachments.length > 0 && (
<div className="space-y-2 pt-1 border-t border-slate-800">
<div className="text-xs font-bold text-slate-300 flex items-center space-x-1.5">
<Paperclip className="w-4 h-4 text-amber-400" />
<span>Attached Documents ({selectedDetailEvent.attachments.length})</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{selectedDetailEvent.attachments.map((att) => (
<a
key={att.id}
href={att.dataUrl || '#'}
download={att.name}
target="_blank"
rel="noopener noreferrer"
className="px-3 py-2 bg-slate-950 hover:bg-slate-800 border border-slate-800 text-slate-200 rounded-xl text-xs flex items-center justify-between transition group"
>
<div className="flex items-center space-x-2 truncate pr-2">
<FileText className="w-4 h-4 text-amber-400 shrink-0" />
<span className="truncate">{att.name}</span>
</div>
<Download className="w-4 h-4 text-slate-500 group-hover:text-amber-400 shrink-0 transition" />
</a>
))}
</div>
</div>
)}
{/* Footer Navigation & Actions */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 pt-3 border-t border-slate-800">
<div>
{selectedDetailEvent.isAutoGenerated && selectedDetailEvent.sourceTab && onNavigateTab && (
<button
onClick={() => {
const tab = selectedDetailEvent.sourceTab;
setSelectedDetailEvent(null);
if (tab) onNavigateTab(tab);
}}
className="px-4 py-2 bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold rounded-xl text-xs flex items-center space-x-1.5 shadow transition cursor-pointer"
>
<span>Open Source Register ({getSourceLabel(selectedDetailEvent.sourceTab, true)})</span>
<ExternalLink className="w-3.5 h-3.5" />
</button>
)}
</div>
<button
onClick={() => setSelectedDetailEvent(null)}
className="px-5 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold rounded-xl transition cursor-pointer"
>
Close Modal
</button>
</div>
</div>
</div>
)}
{/* MODAL FOR MANUAL CREATION */}
{isModalOpen && (
<div className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 flex items-center justify-center p-4 overflow-y-auto">
<div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-xl w-full p-6 shadow-2xl space-y-5 my-8">
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<h2 className="text-lg font-bold text-slate-100 flex items-center space-x-2">
<Clock className="w-5 h-5 text-amber-400" />
<span>Create Custom Publication / Circular</span>
</h2>
<button onClick={() => setIsModalOpen(false)} className="p-1 rounded-lg text-slate-400 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSaveEvent} className="space-y-4 text-xs">
<div>
<label className="block text-slate-400 mb-1 font-medium">Publication Title *</label>
<input
type="text"
value={title}
onChange={(e) => 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"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-slate-400 mb-1 font-medium">Set Date *</label>
<input
type="date"
value={date}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-slate-400 mb-1 font-medium">Reference PIN</label>
<input
type="text"
value={referenceNumber}
onChange={(e) => 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"
/>
</div>
</div>
<div>
<label className="block text-slate-400 mb-1 font-medium">Directive Details & Context</label>
<textarea
rows={3}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Provide details..."
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"
/>
</div>
<div className="flex justify-end space-x-3 pt-3">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 bg-slate-800 text-slate-300 rounded-xl"
>
Cancel
</button>
<button type="submit" className="px-5 py-2 bg-amber-500 text-slate-950 font-bold rounded-xl shadow">
Publish Event
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};