import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { Info, Upload, Download, Printer, Plus, Trash2, ChevronDown, Check, CheckCircle2, RotateCcw, AlertTriangle, Lock } from 'lucide-react'; // --- CUSTOM HOOK PARA PERSISTÊNCIA OFFLINE (LOCALSTORAGE) --- function useStickyState(defaultValue, key) { const [value, setValue] = useState(() => { try { const stickyValue = window.localStorage.getItem(key); return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue; } catch (error) { console.warn("Error reading localStorage", error); return defaultValue; } }); useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error("Error setting localStorage (Quota exceeded?)", error); if (error.name === 'QuotaExceededError') { alert("Storage limit exceeded. Try uploading a smaller logo or clearing data."); } } }, [key, value]); return [value, setValue]; } // --- INITIAL DATA --- const PREDEFINED_STAGES = [ 'STAGE 1 - START-UP', 'STAGE 2 - INITIATION', 'STAGE 3 - PREPARATION', 'STAGE 4 - MIGRATION', 'STAGE 5 - OPERATIONS' ]; const STAGE_DISPLAY_NAMES = { 'STAGE 1 - START-UP': 'STAGE 1 - START-UP', 'STAGE 2 - INITIATION': 'STAGE 2 - INITIATION', 'STAGE 3 - PREPARATION': 'STAGE 3 - PREPARATION', 'STAGE 4 - MIGRATION': 'STAGE 4 - MIGRATION', 'STAGE 5 - OPERATIONS': 'STAGE 5 - OPERATIONS' }; const BASE_TIMELINE_DATA = [ { id: 'TM-027', type: 'T-Minus', phase: 'STAGE 2 - INITIATION', tNum: 10, t: 'T-10', unit: 'Weeks', task: 'Provider to submit clean order forms for GSIP sites. Customer to follow-up with local provider for number portings.', ownerRole: 'Provider / Customer Delivery PM' }, { id: 'TM-029', type: 'T-Minus', phase: 'STAGE 2 - INITIATION', tNum: 9, t: 'T-09', unit: 'Weeks', task: 'Order headsets, replacement phones, PSTN lines for modems, lifts, and security systems.', ownerRole: 'Customer Impl PM' }, { id: 'TM-030', type: 'T-Minus', phase: 'STAGE 2 - INITIATION', tNum: 11, t: 'T-11', unit: 'Weeks', task: 'Order essential hardware (Gateways, Media Packs, Handsets).', ownerRole: 'Provider Delivery PM' }, { id: 'TM-101', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 8, t: 'T-08', unit: 'Weeks', task: 'Ensure all O365 E3/E5 and Phone System licences are allocated to end-users.', ownerRole: 'Customer O365 Admin' }, { id: 'TM-102', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 7, t: 'T-07', unit: 'Weeks', task: 'Provision Public IP addresses for SBC interfaces and configure network Firewalls.', ownerRole: 'Customer Network/Security' }, { id: 'TM-103', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 6, t: 'T-06', unit: 'Weeks', task: 'Build base SBC VM/Appliance and install required Public SSL Certificates.', ownerRole: 'Provider Impl Eng' }, { id: 'TM-104', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 5, t: 'T-05', unit: 'Weeks', task: 'Execute PowerShell pairing for SBC Direct Routing to Tenant.', ownerRole: 'Provider / Customer Teams Admin' }, { id: 'TM-105', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 3, t: 'T-03', unit: 'Weeks', task: 'Perform end-to-end PSTN test calls using restricted pilot user group.', ownerRole: 'Provider / Customer All' }, { id: 'TM-106', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 2, t: 'T-02', unit: 'Weeks', task: 'Readiness Sign-off: Executive confirmation to proceed with the migration window.', ownerRole: 'Customer Impl PM' }, { id: 'RB-001', type: 'Runbook', phase: 'STAGE 1 - START-UP', tNum: 13, t: 'T-13', unit: 'Weeks', task: 'Schedule and host the official Site Implementation Kick-off session.', ownerRole: 'Provider Impl PM' }, { id: 'RB-002', type: 'Runbook', phase: 'STAGE 1 - START-UP', tNum: 12, t: 'T-12', unit: 'Weeks', task: 'MEETING: Conduct Kick-off and align project milestones.', ownerRole: 'All' }, { id: 'RB-003', type: 'Runbook', phase: 'STAGE 1 - START-UP', tNum: 12, t: 'T-12', unit: 'Weeks', task: 'Collect technical parameters and finalize the Site Information Document (SID).', ownerRole: 'Provider / Customer Impl Eng' }, { id: 'RB-028', type: 'Runbook', phase: 'STAGE 2 - INITIATION', tNum: 11, t: 'T-11', unit: 'Weeks', task: 'Hardware quotations validated and approved by finance.', ownerRole: 'Provider Delivery PM' }, { id: 'RB-031', type: 'Runbook', phase: 'STAGE 3 - PREPARATION', tNum: 10, t: 'T-10', unit: 'Weeks', task: 'Allocate IP subnets for local voice devices (AudioCodes, Poly, etc).', ownerRole: 'Customer Impl PM' }, { id: 'RB-032', type: 'Runbook', phase: 'STAGE 3 - PREPARATION', tNum: 9, t: 'T-09', unit: 'Weeks', task: 'Draft and approve End-User migration communication templates.', ownerRole: 'Customer Impl PM' }, { id: 'RB-033', type: 'Runbook', phase: 'STAGE 3 - PREPARATION', tNum: 2, t: 'T-02', unit: 'Weeks', task: 'Submit Change Management (CRQ) request for the migration event.', ownerRole: 'Customer Impl PM' }, { id: 'RB-034', type: 'Runbook', phase: 'STAGE 3 - PREPARATION', tNum: 1, t: 'T-01', unit: 'Weeks', task: 'Pre-migration Go/No-Go checkpoint call with all stakeholders.', ownerRole: 'Provider / Customer All' }, { id: 'RB-014', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: 0, t: '01:30', unit: 'Hours', task: 'Redirect SIP traffic to Teams and execute automated user voice assignments.', ownerRole: 'Provider Impl Eng' }, { id: 'RB-015', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: 0, t: '03:30', unit: 'Hours', task: 'Migration UAT: Verify voice services on-site and provide sign-off.', ownerRole: 'Customer Impl PM' }, { id: 'RB-016', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: 0, t: '04:00', unit: 'Hours', task: 'Notification: Site successfully migrated to Teams Phone System.', ownerRole: 'Provider Impl PM' }, { id: 'RB-017', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: 0, t: '04:00', unit: 'Hours', task: 'Executive Report: Successful digital transformation of voice services.', ownerRole: 'Customer Impl PM' }, { id: 'RB-018', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Standby', unit: 'FDOB', task: 'Remote standby support during First Day of Business (FDOB).', ownerRole: 'Provider / Customer All' }, { id: 'RB-019', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Issues', unit: 'FDOB', task: 'Collect and raise migration related issues to Provider project team. Changes will only be performed after validation of the request', ownerRole: 'Customer Impl PM' }, { id: 'RB-020', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Fix', unit: 'FDOB', task: 'Troubleshoot ISSUES raised', ownerRole: 'Provider Impl Eng' }, { id: 'RB-021', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Review', unit: 'FDOB', task: 'Post Implementation review - Sign-off Migration', ownerRole: 'Provider Impl PM' }, { id: 'RB-022', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Actions', unit: 'FDOB', task: 'EMAIL: Document outstanding actions for the site and agree responsibilities', ownerRole: 'Provider Impl PM' }, { id: 'RB-001-Ops', type: 'Runbook', phase: 'STAGE 5 - OPERATIONS', tNum: -0.2, t: 'Handover', unit: 'Post', task: 'Transition site to the 24/7 Global Operational Support Team.', ownerRole: 'Provider' }, { id: 'RB-003-Ops', type: 'Runbook', phase: 'STAGE 5 - OPERATIONS', tNum: -0.2, t: 'BAU', unit: 'Post', task: 'Reinforce Business As Usual (BAU) support processes.', ownerRole: 'Customer' } ]; const DEFAULT_TASKS = BASE_TIMELINE_DATA.map(t => ({ ...t, manualStatus: 'Not Started', checked: false, deliveredDate: null })); export default function App() { // --- AUTHENTICATION STATE --- const [isAuthenticated, setIsAuthenticated] = useState(() => { return sessionStorage.getItem('migration_auth') === 'true'; }); const [passwordInput, setPasswordInput] = useState(''); const [authError, setAuthError] = useState(false); // A SENHA FICA AQUI (Mude para o que desejar) const SECRET_PASSWORD = "admin"; const handleLogin = (e) => { e.preventDefault(); if (passwordInput === SECRET_PASSWORD) { sessionStorage.setItem('migration_auth', 'true'); setIsAuthenticated(true); setAuthError(false); } else { setAuthError(true); } }; const handleLogout = () => { sessionStorage.removeItem('migration_auth'); setIsAuthenticated(false); }; // TELA DE BLOQUEIO if (!isAuthenticated) { return (

Acesso Restrito

Insira a credencial para acessar o painel de migração.

setPasswordInput(e.target.value)} className={`w-full px-4 py-3 rounded-xl border ${authError ? 'border-red-500 bg-red-50' : 'border-gray-300 bg-gray-50'} text-center font-bold text-[#1b222b] outline-none focus:border-[#2563eb] transition-all mb-4`} /> {authError &&

Senha incorreta. Tente novamente.

}
); } // --- PERSISTENT STATE --- const [tasks, setTasks] = useStickyState(DEFAULT_TASKS, 'migration_tasks_v1'); const [projectStart, setProjectStart] = useStickyState(new Date().toISOString().split('T')[0], 'migration_start_v1'); const [customerName, setCustomerName] = useStickyState('Customer', 'migration_cust_name_v1'); const [providerName, setProviderName] = useStickyState('Provider', 'migration_prov_name_v1'); const [customerLogo, setCustomerLogo] = useStickyState(null, 'migration_cust_logo_v1'); const [providerLogo, setProviderLogo] = useStickyState(null, 'migration_prov_logo_v1'); const [stakeholders, setStakeholders] = useStickyState([ { id: 1, name: 'John Doe', team: 'Provider', role: 'Project Manager', contact: 'john.doe@provider.com' } ], 'migration_stakeholders_v1'); // --- VOLATILE STATE --- const [filter, setFilter] = useState('All'); const [collapsedStages, setCollapsedStages] = useState({}); const [isInfoOpen, setIsInfoOpen] = useState(false); const [addTaskStage, setAddTaskStage] = useState(null); const [newTask, setNewTask] = useState({ id: '', type: 'Runbook', tNum: '', tDisplay: '', unit: 'Weeks', desc: '', owner: '' }); const fileInputRef = useRef(null); // --- PRINT STYLES (Injected once) --- useEffect(() => { const style = document.createElement('style'); style.innerHTML = ` @media print { .no-print { display: none !important; } body { background: white !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } main { max-width: 100% !important; padding: 0 !important; } .shadow-md, .shadow-sm, .shadow-2xl { box-shadow: none !important; border: 1px solid #e5e7eb !important; } header { position: static !important; border-bottom: 2px solid #1b222b !important; padding-bottom: 1rem !important; margin-bottom: 2rem !important; } button, select, input[type="file"], label[for="csv-upload"] { display: none !important; } input[type="text"], input[type="date"] { border: none !important; padding: 0 !important; background: transparent !important; } .bg-white { background: white !important; } .bg-\\[\\#f0efeb\\] { background: #fafafa !important; } .print-border { border: 1px solid #d1d5db !important; } .divide-y > * + * { border-top-width: 1px !important; border-color: #e5e7eb !important; } } `; document.head.appendChild(style); return () => document.head.removeChild(style); }, []); // --- DERIVED STATE --- const tasksWithDates = useMemo(() => { if (!projectStart) return tasks; const startDate = new Date(projectStart + 'T12:00:00'); return tasks.map(task => { let expected = new Date(startDate); const isWeeks = task.unit === 'Weeks' || task.unit === 'Semanas'; const isHours = task.unit === 'Hours' || task.unit === 'Horas'; if (isWeeks) expected.setDate(expected.getDate() + ((13 - task.tNum) * 7)); else if (isHours) expected.setDate(expected.getDate() + (13 * 7)); else if (task.unit === 'FDOB') expected.setDate(expected.getDate() + (13 * 7) + 1); else if (task.unit === 'Post') expected.setDate(expected.getDate() + (13 * 7) + 3); else expected = null; // Fallback return { ...task, expectedDate: expected }; }); }, [tasks, projectStart]); const filteredTasks = useMemo(() => { return tasksWithDates.filter(t => filter === 'All' || t.type === filter); }, [tasksWithDates, filter]); const progressPercent = useMemo(() => { if (!tasksWithDates.length) return 0; const completed = tasksWithDates.filter(t => t.checked).length; return Math.round((completed / tasksWithDates.length) * 100); }, [tasksWithDates]); // --- HANDLERS --- const handleLogoUpload = useCallback((e, setter) => { const file = e.target.files[0]; if (file) { if (file.size > 2 * 1024 * 1024) { // 2MB limit for localStorage safety alert("File is too large. Please upload an image under 2MB."); return; } const reader = new FileReader(); reader.onload = (ev) => setter(ev.target.result); reader.readAsDataURL(file); } }, []); const toggleCheck = useCallback((id) => { if (!projectStart) { alert("Please set the Project Kick-off Date first."); return; } setTasks(prev => prev.map(t => { if (t.id === id) { const isChecked = !t.checked; return { ...t, checked: isChecked, deliveredDate: isChecked ? new Date().toISOString() : null, // Save as ISO string for JSON manualStatus: isChecked ? 'Completed' : 'Not Started' }; } return t; })); }, [projectStart, setTasks]); const updateManualStatus = useCallback((id, status) => { setTasks(prev => prev.map(t => t.id === id ? { ...t, manualStatus: status } : t)); }, [setTasks]); const removeTask = useCallback((id) => { if (window.confirm("Are you sure you want to permanently remove this action?")) { setTasks(prev => prev.filter(t => t.id !== id)); } }, [setTasks]); const resetProject = useCallback(() => { if (window.confirm("WARNING: This will erase all your custom tasks, stakeholders, and progress, resetting the project to default. Continue?")) { setTasks(DEFAULT_TASKS); setProjectStart(new Date().toISOString().split('T')[0]); setCustomerName('Customer'); setProviderName('Provider'); setCustomerLogo(null); setProviderLogo(null); setStakeholders([{ id: 1, name: 'John Doe', team: 'Provider', role: 'Project Manager', contact: 'john.doe@provider.com' }]); setIsInfoOpen(false); } }, [setTasks, setProjectStart, setCustomerName, setProviderName, setCustomerLogo, setProviderLogo, setStakeholders]); const toggleStage = (stage) => setCollapsedStages(prev => ({ ...prev, [stage]: !prev[stage] })); const expandCollapseAll = (expand) => { const newState = {}; PREDEFINED_STAGES.forEach(s => newState[s] = !expand); setCollapsedStages(newState); }; const handleAddTaskSubmit = (e) => { e.preventDefault(); const newTaskObj = { id: newTask.id || `TSK-${Date.now().toString().slice(-4)}`, type: newTask.type, phase: addTaskStage, tNum: parseFloat(newTask.tNum) || 0, t: newTask.tDisplay || `T-${newTask.tNum}`, unit: newTask.unit, task: newTask.desc, ownerRole: newTask.owner || 'Unassigned', manualStatus: 'Not Started', checked: false, deliveredDate: null }; setTasks(prev => [...prev, newTaskObj]); setAddTaskStage(null); setNewTask({ id: '', type: 'Runbook', tNum: '', tDisplay: '', unit: 'Weeks', desc: '', owner: '' }); }; // --- CSV LOGIC --- const exportToCSV = () => { let csvContent = "data:text/csv;charset=utf-8,\uFEFF"; csvContent += "ID,Type,Stage,T-Minus,Unit,Expected Date,Task Description,Action Owner,Status,Delivered Date\n"; filteredTasks.forEach(item => { let expected = item.expectedDate ? new Date(item.expectedDate).toLocaleDateString('en-GB') : ''; let delivered = item.deliveredDate ? new Date(item.deliveredDate).toLocaleDateString('en-GB') : ''; let finalRole = item.ownerRole.replace(/Customer/gi, customerName).replace(/Provider/gi, providerName); let taskEscaped = `"${item.task.replace(/"/g, '""')}"`; csvContent += `"${item.id}","${item.type}","${item.phase}","${item.t}","${item.unit}","${expected}",${taskEscaped},"${finalRole}","${item.manualStatus}","${delivered}"\n`; }); const encodedUri = encodeURI(csvContent); const link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", `migration_plan_${new Date().toISOString().split('T')[0]}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const parseCSVText = (text) => { const result = []; let row = [], cell = '', inQuotes = false; for (let i = 0; i < text.length; i++) { const char = text[i], nextChar = text[i + 1]; if (char === '"' && inQuotes && nextChar === '"') { cell += '"'; i++; } else if (char === '"') inQuotes = !inQuotes; else if (char === ',' && !inQuotes) { row.push(cell.trim()); cell = ''; } else if ((char === '\n' || char === '\r') && !inQuotes) { if (char === '\r' && nextChar === '\n') i++; row.push(cell.trim()); result.push(row); row = []; cell = ''; } else cell += char; } if (cell || row.length > 0) { row.push(cell.trim()); result.push(row); } return result; }; const parseDateToISO = (dateStr) => { if(!dateStr) return null; const parts = dateStr.split('/'); if(parts.length === 3) { // Assuming DD/MM/YYYY from our export return new Date(parts[2], parts[1] - 1, parts[0]).toISOString(); } const d = new Date(dateStr); return isNaN(d.getTime()) ? null : d.toISOString(); }; const importFromCSV = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const text = ev.target.result; const rows = parseCSVText(text); if (rows.length < 2) throw new Error("Empty or invalid CSV structure."); const newTimelineData = []; for (let i = 1; i < rows.length; i++) { const row = rows[i]; if (row.length < 9 || !row[0]) continue; // Skip malformed rows const [id, type, phase, tDisplay, unit, , taskDesc, ownerRole, status, deliveredRaw] = row; let tNum = 0; const uLower = unit.toLowerCase(); if (uLower === 'weeks' || uLower === 'semanas') { const match = tDisplay.match(/-?\d+(\.\d+)?/); tNum = match ? parseFloat(match[0]) : 0; // Adjust sign if T- was used if (tDisplay.includes('T-') && tNum > 0) tNum = Math.abs(tNum); } else if (uLower === 'fdob') tNum = -0.1; else if (uLower === 'post') tNum = -0.2; const isCompleted = status === 'Completed' || status === 'Concluído'; newTimelineData.push({ id: id.replace(/['"]/g, ''), type: type.replace(/['"]/g, ''), phase: phase.replace(/['"]/g, ''), tNum, t: tDisplay.replace(/['"]/g, ''), unit: unit.replace(/['"]/g, ''), task: taskDesc, // Handled by parser ownerRole: ownerRole.replace(/['"]/g, ''), manualStatus: isCompleted ? 'Completed' : (status ? status.replace(/['"]/g, '') : 'Not Started'), checked: isCompleted, deliveredDate: deliveredRaw ? parseDateToISO(deliveredRaw.replace(/['"]/g, '')) : null }); } if (newTimelineData.length > 0) { setTasks(newTimelineData); alert(`Successfully imported ${newTimelineData.length} tasks.`); } } catch (err) { alert("Error parsing CSV: " + err.message); } finally { e.target.value = ''; // Reset input } }; reader.readAsText(file); }; // --- RENDER HELPERS --- const getBadgeClass = (str) => { const s = str.toLowerCase(); if (s.includes(providerName.toLowerCase()) || s.includes('provider')) return 'bg-[#2563eb]/10 text-[#2563eb] border-[#2563eb]/30 border'; if (s.includes(customerName.toLowerCase()) || s.includes('customer')) return 'bg-[#1b222b]/5 text-[#1b222b] border-[#1b222b]/20 border'; return 'bg-[#9fb4c4]/10 text-[#1b222b]/70 border-[#9fb4c4]/30 border'; }; const getStatusSelectClass = (status) => { switch(status) { case 'In Progress': return 'bg-[#2563eb]/10 text-[#2563eb] border-[#2563eb]/30 hover:bg-[#2563eb]/20'; case 'Need Attention': return 'bg-[#e11d48]/10 text-[#e11d48] border-[#e11d48]/30 hover:bg-[#e11d48]/20'; default: return 'bg-white text-[#1b222b]/60 border-[#9fb4c4]/40 hover:border-[#1b222b]/40'; } }; return (
{/* HEADER */}
{/* CUSTOMER SECTION */}
{customerLogo && (
Customer Logo
)}
Customer setCustomerName(e.target.value)} className="bg-transparent border-b border-dashed border-gray-300 text-[#1b222b] text-base md:text-lg font-bold w-full max-w-[180px] outline-none focus:border-[#2563eb] transition-colors" placeholder="Client Name" /> {!customerLogo && ( )}
{/* TITLE */}

Migration Plan

Unified T-Minus & Runbook

{/* PROVIDER SECTION */}
Provider setProviderName(e.target.value)} className="bg-transparent border-b border-dashed border-gray-300 text-[#1b222b] text-base md:text-lg font-bold w-full max-w-[180px] outline-none focus:border-[#2563eb] transition-colors text-right" placeholder="Your Company" /> {!providerLogo && ( )}
{providerLogo && (
Provider Logo
)}
{/* CONTROL BAR */}
setProjectStart(e.target.value)} className="bg-gray-50 border border-gray-200 text-[#1b222b] rounded-lg px-3 py-2 outline-none font-bold focus:border-[#2563eb] focus:ring-2 focus:ring-[#2563eb]/20 text-sm transition-all" />

Progress

{progressPercent}%

{/* Circular progress visual representation */}
50 ? '150% -50%, 150% 150%, -50% 150%' : progressPercent + '% -50%'})`, transform: 'rotate(-45deg)'}}>
{/* TOP ROW: STAKEHOLDERS & LEGENDS */}

Action Owners Legend

Customer Action or Approval required.

Provider Engineering / Delivery.

Project Stakeholders

{stakeholders.map(s => { const teamLabel = s.team.replace(/Provider/gi, providerName).replace(/Customer/gi, customerName); return ( ) })} {stakeholders.length === 0 && ( )}
Name Team Role Contact Action
{s.name} {teamLabel} {s.role} {s.contact}
No stakeholders added yet.
{/* MACRO TIMELINE (PROGRESS) */}

Migration Phase Progress

{/* Background Track */}
{/* Fill Track */}
{[1, 2, 3, 4, 5].map(stageNum => { const thresholds = [0, 15, 58, 73, 91]; const t = thresholds[stageNum-1]; const isDone = progressPercent >= (stageNum === 5 ? 100 : thresholds[stageNum]); const isActive = progressPercent > t && !isDone; const stageTasks = filteredTasks.filter(task => task.phase === PREDEFINED_STAGES[stageNum-1]); let datesText = "No Target Dates"; if (stageTasks.length > 0) { const times = stageTasks.filter(t => t.expectedDate).map(t => new Date(t.expectedDate).getTime()); if(times.length > 0) { const min = new Date(Math.min(...times)).toLocaleDateString('en-GB', {day:'2-digit', month:'short'}); const max = new Date(Math.max(...times)).toLocaleDateString('en-GB', {day:'2-digit', month:'short'}); datesText = min === max ? min : `${min} - ${max}`; } } return (
{isDone ? : stageNum}

STAGE {stageNum}

{STAGE_DISPLAY_NAMES[PREDEFINED_STAGES[stageNum-1]].split(' - ')[1]}

{/* Tooltip */}
{datesText}
); })}
{/* GANTT CHART (CSS-BASED VISUAL) */}

Visual Schedule (Gantt Macro)

Phase Flow
T-13 WksT-10 WksT-6 WksT-2 WksMigrationPost-Ops
{/* Vertical Dashed Lines */}
{[1,2,3,4,5].map(i =>
)}
{/* Bars */} {[ { label: 'S1: Start-Up', w: '20%', ml: '0%', bg: 'bg-gray-200', text: 'text-gray-700', border: 'border-gray-300' }, { label: 'S2: Initiation', w: '40%', ml: '15%', bg: 'bg-[#2563eb]', text: 'text-white', border: 'border-[#2563eb]' }, { label: 'S3: Preparation', w: '15%', ml: '50%', bg: 'bg-indigo-100', text: 'text-indigo-800', border: 'border-indigo-200' }, { label: 'S4: Migration', w: '5%', ml: '65%', bg: 'bg-[#e11d48]', text: 'text-white', border: 'border-[#be123c]' }, { label: 'S5: Operations', w: '25%', ml: '70%', bg: 'bg-[#55c977]/20', text: 'text-[#10b981]', border: 'border-[#55c977]/40' } ].map((bar, idx) => (
{bar.label}
S{idx+1}
))}
{/* TASK LIST CONTROLS */}
Filter:
{/* TASK LIST CONTAINER */}
{filteredTasks.length === 0 ? (

No actions match the current filters.

Adjust your filters or add new tasks.

) : ( PREDEFINED_STAGES.map((stage) => { const stageTasks = filteredTasks.filter(t => t.phase === stage).sort((a,b) => b.tNum - a.tNum); if (stageTasks.length === 0) return null; const isCollapsed = collapsedStages[stage] || false; const stageProgress = Math.round((stageTasks.filter(t => t.checked).length / stageTasks.length) * 100); return (
{/* Stage Header */}
toggleStage(stage)} className="bg-gray-50/80 px-4 md:px-6 py-4 flex flex-wrap justify-between items-center cursor-pointer hover:bg-gray-100 transition-colors border-b border-gray-200 select-none" >

{STAGE_DISPLAY_NAMES[stage]}

{stageTasks.length} Items
{/* Tasks List */} {!isCollapsed && (
{stageTasks.map(item => { const finalTaskText = item.task.replace(/Provider/g, providerName).replace(/Customer/gi, customerName); const finalRole = item.ownerRole.replace(/Customer/gi, customerName).replace(/Provider/gi, providerName); const dFormat = item.expectedDate ? new Date(item.expectedDate).toLocaleDateString('en-GB') : 'N/A'; return (
{/* Left Indicator */}
{item.id} {item.t} {item.unit}
{/* Middle Meta */}

{finalTaskText}

{/* Type Badge */} {item.type === 'T-Minus' ? T-Minus : Runbook } {/* Date Badge */} Target: {dFormat} {/* Delivered Badge */} {item.deliveredDate && ( {new Date(item.deliveredDate).toLocaleDateString('en-GB')} )} {/* Status Select */} {!item.checked ? ( ) : ( Done )} {/* Owner and Actions */}
👤 {finalRole}
{/* Right Checkbox (Desktop: side border, Mobile: top border) */}
{item.checked ? 'Completed' : 'Mark Complete'}
); })}
)}
); }) )}
{/* --- MODALS --- */} {/* INFO MODAL */} {isInfoOpen && (
setIsInfoOpen(false)}>
e.stopPropagation()}>

Project Framework

This React-powered console combines strategic planning (T-Minus) with tactical execution (Runbook) into a single chronology.

Data is saved securely offline in your browser's local storage.

T-Minus Strategy

Preparatory activities counting down to the migration event.

🚀
Execution Runbook

Technical tasks performed during the cutover window and support period.

)} {/* ADD TASK MODAL */} {addTaskStage && (
setAddTaskStage(null)}>
e.stopPropagation()}>

Add Task {STAGE_DISPLAY_NAMES[addTaskStage]}

setNewTask({...newTask, id: e.target.value})} className="w-full bg-gray-50 border border-gray-200 rounded-xl px-3 py-2.5 text-sm text-[#1b222b] focus:bg-white focus:border-[#2563eb] focus:ring-2 focus:ring-[#2563eb]/20 outline-none font-bold transition-all" />
setNewTask({...newTask, tNum: e.target.value, tDisplay: `T-${e.target.value}`})} className="w-full bg-gray-50 border border-gray-200 rounded-xl px-3 py-2.5 text-sm text-[#1b222b] focus:bg-white focus:border-[#2563eb] focus:ring-2 focus:ring-[#2563eb]/20 outline-none font-bold transition-all" />
setNewTask({...newTask, tDisplay: e.target.value})} className="w-full bg-gray-50 border border-gray-200 rounded-xl px-3 py-2.5 text-sm text-[#1b222b] focus:bg-white focus:border-[#2563eb] focus:ring-2 focus:ring-[#2563eb]/20 outline-none font-bold transition-all" />
setNewTask({...newTask, owner: e.target.value})} className="w-full bg-gray-50 border border-gray-200 rounded-xl px-3 py-2.5 text-sm text-[#1b222b] focus:bg-white focus:border-[#2563eb] focus:ring-2 focus:ring-[#2563eb]/20 outline-none font-bold transition-all" />
)}
); }