Pinnacle/src/components/TimelineView.tsx

1155 lines
52 KiB
TypeScript
Raw Normal View History

2026-08-07 08:00:15 +00:00
import React, { useState, useMemo, useRef } from 'react';
2026-08-03 20:24:04 +00:00
import {
Clock,
Plus,
FileText,
Calendar,
Users,
Award,
Gavel,
ShieldAlert,
Search,
Filter,
Paperclip,
MapPin,
User,
Edit2,
Trash2,
Download,
X,
Tag,
ArrowUpDown,
Sparkles,
2026-08-07 08:00:15 +00:00
ExternalLink,
Zap,
ChevronLeft,
ChevronRight,
Maximize2,
Minimize2,
Info,
List,
GitCommit,
CheckCircle2,
RotateCcw
2026-08-03 20:24:04 +00:00
} from 'lucide-react';
import { ProjectStore, TimelineEvent, TimelineEventType, UserRole, Attachment } from '../types';
import { AttachmentUploader } from './AttachmentUploader';
2026-08-07 08:00:15 +00:00
import { generateDynamicTimeline } from '../utils/timelineGenerator';
2026-08-03 20:24:04 +00:00
interface TimelineViewProps {
store: ProjectStore;
onUpdateStore: (newStore: ProjectStore) => void;
userRole?: UserRole;
currentUserName?: string;
2026-08-07 08:00:15 +00:00
onNavigateTab?: (tab: string) => void;
2026-08-03 20:24:04 +00:00
}
export const TimelineView: React.FC<TimelineViewProps> = ({
store,
onUpdateStore,
userRole = 'viewer',
2026-08-07 08:00:15 +00:00
currentUserName = 'System User',
onNavigateTab
2026-08-03 20:24:04 +00:00
}) => {
const canEdit = userRole === 'admin' || userRole === 'editor';
2026-08-07 08:00:15 +00:00
// 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
2026-08-03 20:24:04 +00:00
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all');
2026-08-07 08:00:15 +00:00
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');
2026-08-03 20:24:04 +00:00
2026-08-07 08:00:15 +00:00
// Manual Event Creation/Editing Modal State
2026-08-03 20:24:04 +00:00
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingEvent, setEditingEvent] = useState<Partial<TimelineEvent> | null>(null);
2026-08-07 08:00:15 +00:00
// Scroll Container Ref for Horizontal Flow
const scrollContainerRef = useRef<HTMLDivElement>(null);
2026-08-03 20:24:04 +00:00
// 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[]>([]);
2026-08-07 08:00:15 +00:00
// Open modal to create a new manual event or circular
2026-08-03 20:24:04 +00:00
const handleOpenCreateModal = () => {
setEditingEvent(null);
setTitle('');
setEventType('circular');
2026-08-07 08:00:15 +00:00
setCategory('Official Governance Circular');
2026-08-03 20:24:04 +00:00
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);
};
2026-08-07 08:00:15 +00:00
// Open modal to edit existing manual event
2026-08-03 20:24:04 +00:00
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);
};
2026-08-07 08:00:15 +00:00
// Save manual event
2026-08-03 20:24:04 +00:00
const handleSaveEvent = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim() || !date) return;
2026-08-07 08:00:15 +00:00
const currentManualEvents = store.timelineEvents || [];
let updatedManualEvents = [...currentManualEvents];
2026-08-03 20:24:04 +00:00
if (editingEvent && editingEvent.id) {
2026-08-07 08:00:15 +00:00
updatedManualEvents = updatedManualEvents.map((evt) =>
String(evt.id) === String(editingEvent.id)
2026-08-03 20:24:04 +00:00
? {
...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 {
2026-08-07 08:00:15 +00:00
const nextId = (store.ids.timelineEvent || currentManualEvents.length + 1) + 1;
2026-08-03 20:24:04 +00:00
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,
2026-08-07 08:00:15 +00:00
createdAt: new Date().toISOString(),
isAutoGenerated: false,
sourceType: 'manual'
2026-08-03 20:24:04 +00:00
};
2026-08-07 08:00:15 +00:00
updatedManualEvents.push(newEvt);
2026-08-03 20:24:04 +00:00
}
onUpdateStore({
...store,
2026-08-07 08:00:15 +00:00
timelineEvents: updatedManualEvents,
2026-08-03 20:24:04 +00:00
ids: {
...store.ids,
2026-08-07 08:00:15 +00:00
timelineEvent: (store.ids.timelineEvent || currentManualEvents.length + 1) + 1
2026-08-03 20:24:04 +00:00
},
logs: [
{
id: store.ids.log || Date.now(),
action: editingEvent ? 'TIMELINE_EVENT_UPDATED' : 'TIMELINE_EVENT_CREATED',
type: 'timeline',
2026-08-07 08:00:15 +00:00
details: `${editingEvent ? 'Updated' : 'Created'} timeline publication: ${title.trim()} (${referenceNumber || eventType})`,
2026-08-03 20:24:04 +00:00
user: currentUserName,
timestamp: new Date().toISOString()
},
...store.logs
]
});
setIsModalOpen(false);
};
2026-08-07 08:00:15 +00:00
// 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));
2026-08-03 20:24:04 +00:00
onUpdateStore({
...store,
2026-08-07 08:00:15 +00:00
timelineEvents: updatedManualEvents,
2026-08-03 20:24:04 +00:00
logs: [
{
id: store.ids.log || Date.now(),
action: 'TIMELINE_EVENT_DELETED',
type: 'timeline',
2026-08-07 08:00:15 +00:00
details: `Deleted manual timeline entry ID #${id}`,
2026-08-03 20:24:04 +00:00
user: currentUserName,
timestamp: new Date().toISOString()
},
...store.logs
]
});
};
2026-08-07 08:00:15 +00:00
// 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);
}
2026-08-03 20:24:04 +00:00
}
});
2026-08-07 08:00:15 +00:00
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
]);
2026-08-03 20:24:04 +00:00
2026-08-07 08:00:15 +00:00
// 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
2026-08-03 20:24:04 +00:00
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':
2026-08-07 08:00:15 +00:00
return <FileText className="w-3.5 h-3.5 text-cyan-400" />;
2026-08-03 20:24:04 +00:00
case 'meeting':
2026-08-07 08:00:15 +00:00
return <Users className="w-3.5 h-3.5 text-amber-400" />;
2026-08-03 20:24:04 +00:00
case 'milestone':
2026-08-07 08:00:15 +00:00
return <Award className="w-3.5 h-3.5 text-emerald-400" />;
2026-08-03 20:24:04 +00:00
case 'decision':
2026-08-07 08:00:15 +00:00
return <Gavel className="w-3.5 h-3.5 text-purple-400" />;
2026-08-03 20:24:04 +00:00
case 'release':
case 'audit':
2026-08-07 08:00:15 +00:00
return <ShieldAlert className="w-3.5 h-3.5 text-rose-400" />;
2026-08-03 20:24:04 +00:00
default:
2026-08-07 08:00:15 +00:00
return <Clock className="w-3.5 h-3.5 text-slate-400" />;
2026-08-03 20:24:04 +00:00
}
}
function getTypeLabel(type: TimelineEventType) {
switch (type) {
case 'circular':
2026-08-07 08:00:15 +00:00
return 'Governance Circular';
2026-08-03 20:24:04 +00:00
case 'meeting':
2026-08-07 08:00:15 +00:00
return 'Committee Session';
2026-08-03 20:24:04 +00:00
case 'milestone':
2026-08-07 08:00:15 +00:00
return 'Project Milestone';
2026-08-03 20:24:04 +00:00
case 'decision':
2026-08-07 08:00:15 +00:00
return 'Executive Decision';
2026-08-03 20:24:04 +00:00
case 'release':
return 'Official Release';
case 'audit':
2026-08-07 08:00:15 +00:00
return 'Risk & Audit Alert';
2026-08-03 20:24:04 +00:00
default:
2026-08-07 08:00:15 +00:00
return 'Portal Update';
2026-08-03 20:24:04 +00:00
}
}
2026-08-07 08:00:15 +00:00
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(' ') + '...';
}
2026-08-03 20:24:04 +00:00
return (
2026-08-07 08:00:15 +00:00
<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">
2026-08-03 20:24:04 +00:00
{/* 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">
2026-08-07 08:00:15 +00:00
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
2026-08-03 20:24:04 +00:00
<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>
2026-08-07 08:00:15 +00:00
<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.
2026-08-03 20:24:04 +00:00
</p>
</div>
</div>
</div>
2026-08-07 08:00:15 +00:00
<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>
2026-08-03 20:24:04 +00:00
</div>
2026-08-07 08:00:15 +00:00
{/* Analytics Summary Cards */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3.5">
2026-08-03 20:24:04 +00:00
<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>
2026-08-07 08:00:15 +00:00
<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>
2026-08-03 20:24:04 +00:00
</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>
2026-08-07 08:00:15 +00:00
<div className="text-[10px] text-slate-500">Governance & directive publications</div>
2026-08-03 20:24:04 +00:00
</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">
2026-08-07 08:00:15 +00:00
<span>Committee Sessions</span>
2026-08-03 20:24:04 +00:00
<Users className="w-4 h-4 text-amber-400" />
</div>
<div className="text-2xl font-extrabold text-amber-400">{meetingsCount}</div>
2026-08-07 08:00:15 +00:00
<div className="text-[10px] text-slate-500">Meetings & committee minutes</div>
2026-08-03 20:24:04 +00:00
</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">
2026-08-07 08:00:15 +00:00
<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>
2026-08-03 20:24:04 +00:00
<Paperclip className="w-4 h-4 text-emerald-400" />
</div>
<div className="text-2xl font-extrabold text-emerald-400">{docsCount}</div>
2026-08-07 08:00:15 +00:00
<div className="text-[10px] text-slate-500">Attached files & documents</div>
2026-08-03 20:24:04 +00:00
</div>
</div>
2026-08-07 08:00:15 +00:00
{/* 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 */}
2026-08-03 20:24:04 +00:00
<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)}
2026-08-07 08:00:15 +00:00
placeholder="Search reference, title, author, attachments..."
2026-08-03 20:24:04 +00:00
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">
{[
2026-08-07 08:00:15 +00:00
{ id: 'all', label: 'All Updates' },
2026-08-03 20:24:04 +00:00
{ id: 'circular', label: 'Circulars' },
{ id: 'meeting', label: 'Meetings' },
2026-08-07 08:00:15 +00:00
{ id: 'decision', label: 'Decisions' },
2026-08-03 20:24:04 +00:00
{ id: 'milestone', label: 'Milestones' },
2026-08-07 08:00:15 +00:00
{ id: 'audit', label: 'Risks / Audits' }
2026-08-03 20:24:04 +00:00
].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>
))}
2026-08-07 08:00:15 +00:00
<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>
2026-08-03 20:24:04 +00:00
<button
2026-08-07 08:00:15 +00:00
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"
2026-08-03 20:24:04 +00:00
>
<ArrowUpDown className="w-3.5 h-3.5 text-amber-400" />
2026-08-07 08:00:15 +00:00
<span className="text-[11px] font-semibold">{sortOrder === 'asc' ? 'Oldest First' : 'Newest First'}</span>
2026-08-03 20:24:04 +00:00
</button>
</div>
</div>
2026-08-07 08:00:15 +00:00
{/* 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>
2026-08-03 20:24:04 +00:00
</div>
2026-08-07 08:00:15 +00:00
{/* 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">
2026-08-03 20:24:04 +00:00
<button
2026-08-07 08:00:15 +00:00
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'
}`}
2026-08-03 20:24:04 +00:00
>
2026-08-07 08:00:15 +00:00
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>
2026-08-03 20:24:04 +00:00
</button>
)}
</div>
2026-08-07 08:00:15 +00:00
</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">
2026-08-03 20:24:04 +00:00
<div className="relative pl-6 md:pl-8 border-l-2 border-slate-800 space-y-8 my-2">
{filteredEvents.map((evt) => (
2026-08-07 08:00:15 +00:00
<div
key={evt.id}
className="relative group cursor-pointer"
onClick={() => setSelectedDetailEvent(evt)}
>
2026-08-03 20:24:04 +00:00
<div
2026-08-07 08:00:15 +00:00
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 ${
2026-08-03 20:24:04 +00:00
evt.type === 'circular'
2026-08-07 08:00:15 +00:00
? 'bg-slate-900 border-cyan-400 text-cyan-400'
2026-08-03 20:24:04 +00:00
: evt.type === 'meeting'
2026-08-07 08:00:15 +00:00
? 'bg-slate-900 border-amber-400 text-amber-400'
2026-08-03 20:24:04 +00:00
: evt.type === 'milestone'
2026-08-07 08:00:15 +00:00
? 'bg-slate-900 border-emerald-400 text-emerald-400'
2026-08-03 20:24:04 +00:00
: evt.type === 'decision'
2026-08-07 08:00:15 +00:00
? 'bg-slate-900 border-purple-400 text-purple-400'
: 'bg-slate-900 border-rose-400 text-rose-400'
2026-08-03 20:24:04 +00:00
}`}
>
<div className="w-2 h-2 rounded-full bg-current" />
</div>
2026-08-07 08:00:15 +00:00
<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">
2026-08-03 20:24:04 +00:00
<div className="flex flex-wrap items-start justify-between gap-2 border-b border-slate-800/60 pb-3">
2026-08-07 08:00:15 +00:00
<div className="space-y-1.5 flex-1">
2026-08-03 20:24:04 +00:00
<div className="flex flex-wrap items-center gap-2">
2026-08-07 08:00:15 +00:00
<span className={`px-2.5 py-0.5 rounded-lg text-[10px] font-bold border ${getTypeBadge(evt.type)}`}>
{getTypeLabel(evt.type)}
2026-08-03 20:24:04 +00:00
</span>
{evt.referenceNumber && (
2026-08-07 08:00:15 +00:00
<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}
2026-08-03 20:24:04 +00:00
</span>
)}
2026-08-07 08:00:15 +00:00
<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>
2026-08-03 20:24:04 +00:00
</div>
2026-08-07 08:00:15 +00:00
<h3 className="text-base font-bold text-slate-100 group-hover:text-amber-400 transition">{evt.title}</h3>
2026-08-03 20:24:04 +00:00
</div>
2026-08-07 08:00:15 +00:00
<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>
2026-08-03 20:24:04 +00:00
</div>
2026-08-07 08:00:15 +00:00
<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>
)}
2026-08-03 20:24:04 +00:00
2026-08-07 08:00:15 +00:00
{/* 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>
)}
2026-08-03 20:24:04 +00:00
2026-08-07 08:00:15 +00:00
{/* 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>
2026-08-03 20:24:04 +00:00
2026-08-07 08:00:15 +00:00
<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>
2026-08-03 20:24:04 +00:00
2026-08-07 08:00:15 +00:00
{/* 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>
2026-08-03 20:24:04 +00:00
</div>
2026-08-07 08:00:15 +00:00
<Download className="w-4 h-4 text-slate-500 group-hover:text-amber-400 shrink-0 transition" />
</a>
))}
2026-08-03 20:24:04 +00:00
</div>
</div>
2026-08-07 08:00:15 +00:00
)}
{/* 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>
2026-08-03 20:24:04 +00:00
</div>
2026-08-07 08:00:15 +00:00
</div>
)}
2026-08-03 20:24:04 +00:00
2026-08-07 08:00:15 +00:00
{/* MODAL FOR MANUAL CREATION */}
2026-08-03 20:24:04 +00:00
{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">
2026-08-07 08:00:15 +00:00
<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">
2026-08-03 20:24:04 +00:00
<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" />
2026-08-07 08:00:15 +00:00
<span>Create Custom Publication / Circular</span>
2026-08-03 20:24:04 +00:00
</h2>
2026-08-07 08:00:15 +00:00
<button onClick={() => setIsModalOpen(false)} className="p-1 rounded-lg text-slate-400 hover:text-white">
2026-08-03 20:24:04 +00:00
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSaveEvent} className="space-y-4 text-xs">
<div>
2026-08-07 08:00:15 +00:00
<label className="block text-slate-400 mb-1 font-medium">Publication Title *</label>
2026-08-03 20:24:04 +00:00
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
2026-08-07 08:00:15 +00:00
placeholder="e.g. Published Governance Circular PIN-2026-003"
2026-08-03 20:24:04 +00:00
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>
2026-08-07 08:00:15 +00:00
<label className="block text-slate-400 mb-1 font-medium">Set Date *</label>
2026-08-03 20:24:04 +00:00
<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>
2026-08-07 08:00:15 +00:00
<label className="block text-slate-400 mb-1 font-medium">Reference PIN</label>
2026-08-03 20:24:04 +00:00
<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>
2026-08-07 08:00:15 +00:00
<label className="block text-slate-400 mb-1 font-medium">Directive Details & Context</label>
2026-08-03 20:24:04 +00:00
<textarea
rows={3}
value={description}
onChange={(e) => setDescription(e.target.value)}
2026-08-07 08:00:15 +00:00
placeholder="Provide details..."
2026-08-03 20:24:04 +00:00
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)}
2026-08-07 08:00:15 +00:00
className="px-4 py-2 bg-slate-800 text-slate-300 rounded-xl"
2026-08-03 20:24:04 +00:00
>
Cancel
</button>
2026-08-07 08:00:15 +00:00
<button type="submit" className="px-5 py-2 bg-amber-500 text-slate-950 font-bold rounded-xl shadow">
Publish Event
2026-08-03 20:24:04 +00:00
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};