top of page
Collaborating

THE VALUE OF DOQUMENT

Contact

BIO

Since 2000, I’ve been providing professional consulting services to clients from San Francisco and beyond. From strategic planning to innovative solutions, my focus is always on building an efficient and results-driven relationship. I’ll work with you to create a customized plan of action for yourself or your organization. Get in touch to learn more about my vision and consulting approach.

Modern Work Space
Nothing to book right now. Check back soon.

This is your Testimonial quote. Use this space to share customers’ reviews about you, your services and exciting success stories. Get your visitors excited to work with you!

Robbie White

This is your Testimonial quote. Use this space to share customers’ reviews about you, your services and exciting success stories. Get your visitors excited to work with you!

Riley Jones

This is your Testimonial quote. Use this space to share customers’ reviews about you, your services and exciting success stories. Get your visitors excited to work with you!

Payton Hillman

CONTACT ME

500 Terry Francois Street, 6th Floor. San Francisco, CA 94158

123-456-7890

Thanks for submitting!

Subscribe Form

Thanks for subscribing!

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)}%)
); })}
); }