top of page
Nothing to book right now. Check back soon.
bottom of page
import React, { useMemo, useState, useEffect, useRef } from "react"; const RAW_ITEMS = [ {"sheet":"M-B6","section":"Section I -- BASIS OF PROJECT DECISION","code":"B6","title":"Future Expansion Considerations","default_level":1,"weight":1}, {"sheet":"M-B7","section":"Section I -- BASIS OF PROJECT DECISION","code":"B7","title":"Expected Project Life Cycle","default_level":1,"weight":1}, {"sheet":"M-C1","section":"Section I -- BASIS OF PROJECT DECISION","code":"C1","title":"Technology","default_level":1,"weight":1}, {"sheet":"M-J1","section":"Section II -- BASIS OF DESIGN","code":"J1","title":"Facility Requirements","default_level":1,"weight":1}, {"sheet":"M-J2","section":"Section II -- BASIS OF DESIGN","code":"J2","title":"Utility Requirements","default_level":1,"weight":1} ]; function useLocalState(key, initial) { const [v, setV] = useState(() => { const raw = typeof window !== 'undefined' ? localStorage.getItem(key) : null; return raw ? JSON.parse(raw) : initial; }); useEffect(() => { try { localStorage.setItem(key, JSON.stringify(v)); } catch {} }, [key, v]); return [v, setV]; } export default function ProjectReadinessIndex() { const [items, setItems] = useLocalState("pdri-items", RAW_ITEMS); const [clientName, setClientName] = useLocalState("pdri-client", ""); const [projectName, setProjectName] = useLocalState("pdri-project", ""); const [activeSection, setActiveSection] = useState(""); const grouped = useMemo(() => { const map = new Map(); for (const i of items) { if (!map.has(i.section)) map.set(i.section, []); map.get(i.section).push(i); } return Array.from(map.entries()); }, [items]); const updateLevel = (code, level) => setItems(prev => prev.map(i => i.code === code ? { ...i, level } : i)); const reset = () => setItems(prev => prev.map(i => ({...i, level: undefined}))); const totals = useMemo(() => { let totalScore = 0, totalMax = 0; for (const i of items) { const w = Number(i.weight ?? 1); const v = Number(i.level ?? i.default_level ?? 0); totalScore += w * v; totalMax += 5 * w; } return { totalScore, totalMax, pct: totalMax ? (totalScore / totalMax) * 100 : 0 }; }, [items]); const sliderColors = ["#1E3A8A", "#0F766E", "#9333EA", "#B91C1C", "#2563EB"]; const COL_WIDTHS = { code: 88, title: 420, weight: 120, level: 180, score: 120 }; const getTrafficLightColor = (pct) => { if (pct < 25) return 'red'; if (pct < 75) return 'orange'; return 'green'; }; const exportExcel = async () => { const safe = (s) => (s||"").toString().replace(/[^a-z0-9]+/gi, "-"); const fileName = `project-readiness-index-${safe(clientName||"client")}-${safe(projectName||"project")}.xlsx`; const XLSX = await import('xlsx'); const rows = items.map(i => { const weight = Number(i.weight ?? 1) || 1; const level = Number(i.level ?? i.default_level ?? 0) || 0; return { Client: clientName || '', Project: projectName || '', Section: i.section, Code: i.code, Title: i.title, Weight: weight, Default_Level: Number(i.default_level ?? 0) || 0, Level: level, Score: weight * level }; }); const wsScores = XLSX.utils.json_to_sheet(rows); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, wsScores, 'Scores'); XLSX.writeFile(wb, fileName); }; // ---- Section helpers (ids, refs, observer) ---- const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); const sectionRefs = useRef({}); useEffect(() => { const observer = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) { setActiveSection(entry.target.id); break; } } }, { root: null, rootMargin: '0px 0px -70% 0px', threshold: 0.1 }); const nodes = Object.values(sectionRefs.current || {}); nodes.forEach(n => n && observer.observe(n)); return () => observer.disconnect(); }, [grouped]); const scrollToSection = (id) => { const el = sectionRefs.current[id]; if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; // Summaries per section for nav buttons const sectionSummaries = useMemo(() => grouped.map(([section, rows]) => { const sums = rows.reduce((a, r) => { const w = Number(r.weight ?? 1)||1; const v = Number(r.level ?? r.default_level ?? 0)||0; a.score+=w*v; a.max+=5*w; return a; }, {score:0,max:0}); const pct = sums.max ? (100*sums.score/sums.max) : 0; return { id: slug(section), name: section, score: Math.round(sums.score), max: Math.round(sums.max), pct, color: getTrafficLightColor(pct) }; }), [grouped]); // Max weight across all items (for normalizing activity % so weight affects indicator) const maxWeight = useMemo(() => items.reduce((m, i) => Math.max(m, Number(i.weight ?? 1) || 1), 1), [items]); return (
T

Teesbank Partners

Professional • Reliable • Trusted

Project Readiness Index

{/* Score, Client, Project, and Buttons on same line */}
Overall
{Math.round(totals.totalScore)} / {Math.round(totals.totalMax)}
{totals.pct.toFixed(1)}%
setClientName(e.target.value)} />
setProjectName(e.target.value)} />
{/* --- NEW: Section Nav Bar with traffic lights & percent --- */}
{grouped.map(([section, rows], sectionIndex) => { const sec = rows.reduce((a, r) => { const w = Number(r.weight ?? 1) || 1; const v = Number(r.level ?? r.default_level ?? 0) || 0; a.score += w*v; a.max += 5*w; return a; }, { score:0, max:0 }); const spct = sec.max ? (100*sec.score/sec.max).toFixed(1) : '0.0'; const sliderColor = sliderColors[sectionIndex % sliderColors.length]; const trafficColor = getTrafficLightColor(spct); const id = slug(section); return (
{ sectionRefs.current[id]=n; }} className="mb-6 bg-white rounded-2xl shadow overflow-hidden">
{section}
{Math.round(sec.score)} / {Math.round(sec.max)} ({spct}%)
{rows.map(i => { const w = Number(i.weight ?? 1); const v = Number(i.level ?? i.default_level ?? 0); const itemScore = w * v; const basePct = (v/5)*100; const weightedPct = maxWeight ? basePct * (w / maxWeight) : basePct; const color = getTrafficLightColor(weightedPct); return ( ); })}
Code Title Weight Level (0–5) Score
{i.code} {i.title}
setItems(prev => prev.map(it => it.code === i.code ? { ...it, weight: Math.max(0, Number(e.target.value) || 0) } : it))} className="w-20 border rounded px-2 py-1 text-right" /> ×
updateLevel(i.code, Number(e.target.value))} className="flex-grow" style={{accentColor: sliderColors[sectionIndex % sliderColors.length]}} /> {v}
{itemScore} ({weightedPct.toFixed(0)}%)
); })}
); }