tempspooltracker / index.html
Kaliman-1981's picture
Add 3 files
3a7a2e3 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>ISO Scope Tracker – V2 Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
<style>
@media print {
body {
background: #ffffff !important;
color: #000000 !important;
}
header,
#file-input,
#btn-clear,
#btn-report,
#btn-print::after {
box-shadow: none !important;
}
#file-input,
#btn-clear,
#btn-report,
#btn-print,
#status-msg,
#error-msg {
display: none !important;
}
.bg-gray-900,
.bg-gray-950,
.bg-gray-800,
.bg-gray-700 {
background-color: #ffffff !important;
}
.border-gray-800,
.border-gray-900 {
border-color: #cccccc !important;
}
}
</style>
</head>
<body class="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 text-gray-100">
<header class="py-8 border-b border-gray-800 mb-6">
<div class="max-w-7xl mx-auto px-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 class="text-3xl md:text-4xl font-bold text-white">ISO Scope Tracker – Overview</h1>
<p class="text-gray-300 mt-1 text-sm md:text-base">
PipeScope Pro · ISO &amp; Spool performance across units, fabricators, and shipment status
</p>
</div>
<div class="flex gap-3 items-center flex-wrap">
<label class="inline-flex items-center px-4 py-2 bg-indigo-600 hover:bg-indigo-700 rounded-xl cursor-pointer text-white text-sm shadow">
Choose CSV
<input id="file-input" type="file" accept=".csv" class="hidden" />
</label>
<button id="btn-clear" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-xl text-sm text-gray-100 shadow">
Clear / Reset
</button>
<button id="btn-report" class="px-4 py-2 bg-sky-600 hover:bg-sky-500 rounded-xl text-sm text-white shadow flex items-center gap-2">
<span>Download Report</span>
</button>
<button id="btn-print" class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 rounded-xl text-sm text-white shadow flex items-center gap-2">
<span>PDF Report</span>
</button>
</div>
</div>
<div class="max-w-7xl mx-auto px-6 mt-2 text-sm text-gray-300">
<span id="file-name" class="font-medium">No file selected</span>
<span id="status-msg" class="ml-4 text-green-400 hidden"></span>
<span id="error-msg" class="ml-4 text-red-400 hidden"></span>
</div>
</header>
<main class="max-w-7xl mx-auto px-6 pb-16">
<!-- Filters -->
<section id="filter-section" class="hidden mb-6 bg-gray-900/70 rounded-2xl border border-gray-800 p-4">
<div class="flex flex-wrap gap-3 items-center">
<div class="flex flex-col text-xs text-gray-400">
<label for="filter-unit" class="mb-1">Unit</label>
<select id="filter-unit" class="bg-gray-800 border border-gray-700 rounded-lg px-2 py-1 text-sm min-w-[120px]"></select>
</div>
<div class="flex flex-col text-xs text-gray-400">
<label for="filter-priority" class="mb-1">Priority</label>
<select id="filter-priority" class="bg-gray-800 border border-gray-700 rounded-lg px-2 py-1 text-sm min-w-[120px]"></select>
</div>
<div class="flex flex-col text-xs text-gray-400">
<label for="filter-fabricator" class="mb-1">Fabricator</label>
<select id="filter-fabricator" class="bg-gray-800 border border-gray-700 rounded-lg px-2 py-1 text-sm min-w-[140px]"></select>
</div>
<div class="flex flex-col text-xs text-gray-400">
<label for="filter-status" class="mb-1">Status</label>
<select id="filter-status" class="bg-gray-800 border border-gray-700 rounded-lg px-2 py-1 text-sm min-w-[140px]"></select>
</div>
<div class="flex flex-col text-xs text-gray-400 flex-1 min-w-[160px]">
<label for="filter-spool" class="mb-1">Spool Number Contains</label>
<input id="filter-spool" type="text" placeholder="Search spool..."
class="bg-gray-800 border border-gray-700 rounded-lg px-3 py-1 text-sm w-full" />
</div>
</div>
</section>
<!-- KPI row -->
<section id="kpi-section" class="hidden mb-8">
<div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-7 gap-4">
<div class="bg-gray-900 rounded-xl p-4 text-center border border-gray-800 shadow">
<p class="text-[11px] text-gray-400 uppercase tracking-wide">Total ISOs</p>
<p id="kpi-total-isos" class="text-2xl md:text-3xl font-bold mt-1">0</p>
</div>
<div class="bg-gray-900 rounded-xl p-4 text-center border border-gray-800 shadow">
<p class="text-[11px] text-gray-400 uppercase tracking-wide">Total Spools</p>
<p id="kpi-total-spools" class="text-2xl md:text-3xl font-bold mt-1">0</p>
</div>
<div class="bg-gray-900 rounded-xl p-4 text-center border border-gray-800 shadow">
<p class="text-[11px] text-gray-400 uppercase tracking-wide">Unique Lines</p>
<p id="kpi-unique-lines" class="text-2xl md:text-3xl font-bold mt-1">0</p>
</div>
<div class="bg-gray-900 rounded-xl p-4 text-center border border-gray-800 shadow">
<p class="text-[11px] text-gray-400 uppercase tracking-wide">Delivered (Received)</p>
<p id="kpi-delivered" class="text-2xl md:text-3xl font-bold mt-1">0</p>
</div>
<div class="bg-gray-900 rounded-xl p-4 text-center border border-gray-800 shadow">
<p class="text-[11px] text-gray-400 uppercase tracking-wide">Remaining to Ship</p>
<p id="kpi-rem-ship" class="text-2xl md:text-3xl font-bold mt-1">0</p>
</div>
<div class="bg-gray-900 rounded-xl p-4 text-center border border-gray-800 shadow">
<p class="text-[11px] text-gray-400 uppercase tracking-wide">Remaining to Install</p>
<p id="kpi-rem-install" class="text-2xl md:text-3xl font-bold mt-1">0</p>
</div>
<div class="bg-gray-900 rounded-xl p-4 text-center border border-gray-800 shadow">
<p class="text-[11px] text-gray-400 uppercase tracking-wide">Insulated</p>
<p id="kpi-insulated" class="text-2xl md:text-3xl font-bold mt-1">0</p>
</div>
</div>
</section>
<!-- Fabricator cards -->
<section id="fabricator-section" class="hidden mb-8">
<h2 class="text-xl font-semibold mb-3">Spools by Fabricator – Shipment Status</h2>
<div id="fabricator-cards" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
</section>
<!-- Unit cards -->
<section id="unit-section" class="hidden mb-10">
<h2 class="text-xl font-semibold mb-3">Spools by Unit – Shipment Status</h2>
<div id="unit-cards" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
</section>
<!-- Status by Unit table -->
<section id="tables-section" class="hidden mb-10">
<div class="bg-gray-900 rounded-xl border border-gray-800 p-4 shadow">
<h3 class="text-lg font-semibold mb-3">Status by Unit</h3>
<div id="table-unit-wrapper" class="overflow-x-auto text-sm"></div>
</div>
</section>
<!-- Heatmap -->
<section id="heatmap-section" class="hidden mb-10 bg-gray-900 rounded-xl border border-gray-800 p-4 shadow">
<h3 class="text-lg font-semibold mb-3">Remaining to Ship – Fabricator × Unit</h3>
<div id="heatmap-wrapper" class="overflow-x-auto text-sm"></div>
</section>
<!-- Charts -->
<section id="charts-section" class="hidden grid grid-cols-1 lg:grid-cols-2 gap-6 mb-10">
<div class="bg-gray-900 rounded-xl p-4 border border-gray-800 shadow">
<h3 class="text-lg font-semibold mb-2">ISOs by Unit</h3>
<canvas id="chart-unit"></canvas>
</div>
<div class="bg-gray-900 rounded-xl p-4 border border-gray-800 shadow">
<h3 class="text-lg font-semibold mb-2">ISOs by Zone</h3>
<canvas id="chart-zone"></canvas>
</div>
<div class="bg-gray-900 rounded-xl p-4 border border-gray-800 shadow">
<h3 class="text-lg font-semibold mb-2">ISOs by NDE Class</h3>
<canvas id="chart-nde"></canvas>
</div>
<div class="bg-gray-900 rounded-xl p-4 border border-gray-800 shadow">
<h3 class="text-lg font-semibold mb-2">Spools by Unit</h3>
<canvas id="chart-spool-unit"></canvas>
</div>
<div class="bg-gray-900 rounded-xl p-4 border border-gray-800 shadow">
<h3 class="text-lg font-semibold mb-2">Installed vs Remaining Spools by Unit</h3>
<canvas id="chart-spool-unit-install"></canvas>
</div>
</section>
<!-- Completion summary -->
<section id="completion-section" class="hidden bg-gray-900 rounded-xl border border-gray-800 p-4 shadow mb-10">
<h3 class="text-lg font-semibold mb-3">Completion Summary (Filtered ISOs)</h3>
<div id="completion-table-wrapper" class="text-sm"></div>
</section>
<!-- Status by Fabricator table -->
<section id="fabricator-status-section" class="hidden bg-gray-900 rounded-xl border border-gray-800 p-4 shadow mb-16">
<h3 class="text-lg font-semibold mb-3">Status by Fabricator</h3>
<div id="table-fab-wrapper" class="overflow-x-auto text-sm"></div>
</section>
</main>
<script>
let isoRows = [];
let allRows = [];
let filteredIso = [];
let filteredAll = [];
const charts = {};
const fileInput = document.getElementById("file-input");
const clearButton = document.getElementById("btn-clear");
const reportButton = document.getElementById("btn-report");
const printButton = document.getElementById("btn-print");
fileInput.addEventListener("change", handleFile);
clearButton.addEventListener("click", resetDashboard);
reportButton.addEventListener("click", downloadReport);
printButton.addEventListener("click", () => window.print());
const filters = {
unit: "All",
priority: "All",
fabricator: "All",
status: "All",
spool: ""
};
function handleFile(e) {
const file = e.target.files[0];
if (!file) return;
document.getElementById("file-name").textContent = file.name;
Papa.parse(file, {
header: true,
skipEmptyLines: true,
complete: (results) => processCsv(results.data || []),
error: (err) => showError("Error parsing CSV: " + err)
});
}
function resetDashboard() {
isoRows = [];
allRows = [];
filteredIso = [];
filteredAll = [];
fileInput.value = "";
document.getElementById("file-name").textContent = "No file selected";
document.getElementById("status-msg").classList.add("hidden");
document.getElementById("error-msg").classList.add("hidden");
const sections = [
"filter-section","kpi-section","fabricator-section","unit-section",
"tables-section","heatmap-section","charts-section","completion-section",
"fabricator-status-section"
];
sections.forEach(id => document.getElementById(id).classList.add("hidden"));
document.getElementById("fabricator-cards").innerHTML = "";
document.getElementById("unit-cards").innerHTML = "";
document.getElementById("table-unit-wrapper").innerHTML = "";
document.getElementById("table-fab-wrapper").innerHTML = "";
document.getElementById("heatmap-wrapper").innerHTML = "";
document.getElementById("completion-table-wrapper").innerHTML = "";
["kpi-total-isos","kpi-total-spools","kpi-unique-lines",
"kpi-delivered","kpi-rem-ship","kpi-rem-install","kpi-insulated"]
.forEach(id => document.getElementById(id).textContent = "0");
for (const key in charts) {
if (charts[key]) { charts[key].destroy(); charts[key] = null; }
}
}
function showError(msg) {
const err = document.getElementById("error-msg");
const ok = document.getElementById("status-msg");
err.textContent = msg;
err.classList.remove("hidden");
ok.classList.add("hidden");
}
function showStatus(msg) {
const err = document.getElementById("error-msg");
const ok = document.getElementById("status-msg");
ok.textContent = msg;
ok.classList.remove("hidden");
err.classList.add("hidden");
}
function normalizeFlag(v) {
if (v === undefined || v === null) return "No";
const t = v.toString().trim().toLowerCase();
if (!t || t === "0" || t === "no" || t === "n" || t === "na" || t === "n/a") return "No";
if (t === "yes" || t === "y" || t === "1") return "Yes";
return "No";
}
function normalizeInstalled(v) {
if (v === undefined || v === null) return "No";
const t = v.toString().trim().toLowerCase();
if (!t || t === "na" || t === "n/a") return "No";
return "Yes";
}
function processCsv(rows) {
allRows = rows.slice();
const cleaned = [];
rows.forEach(row => {
const iso = (row["Iso Number"] || "").toString().trim();
row.Painted = normalizeFlag(row["Painted"]);
row.Insulated = normalizeFlag(row["Insulated"]);
row.Installed = normalizeInstalled(row["Installed"]);
if (!iso) return;
cleaned.push(row);
});
if (!cleaned.length) {
showError("No valid rows with 'Iso Number' found.");
return;
}
isoRows = cleaned;
initFilters(isoRows);
applyFilters();
showStatus("Loaded " + isoRows.length + " ISO rows from " + allRows.length + " total rows.");
[
"filter-section","kpi-section","fabricator-section","unit-section",
"tables-section","heatmap-section","charts-section","completion-section",
"fabricator-status-section"
].forEach(id => document.getElementById(id).classList.remove("hidden"));
}
// Filters
function initFilters(rows) {
const unitSet = new Set();
const prioritySet = new Set();
const fabSet = new Set();
rows.forEach(r => {
const u = (r["Unit"] || "Unknown").toString().trim() || "Unknown";
const p = (r["Priority"] || "Unknown").toString().trim() || "Unknown";
const f = (r["Fabrication"] || "Unknown").toString().trim() || "Unknown";
unitSet.add(u);
prioritySet.add(p);
fabSet.add(f);
});
const unitSel = document.getElementById("filter-unit");
const priSel = document.getElementById("filter-priority");
const fabSel = document.getElementById("filter-fabricator");
const statusSel = document.getElementById("filter-status");
const spoolInput = document.getElementById("filter-spool");
function fillSelect(sel, values) {
sel.innerHTML = "";
const optAll = document.createElement("option");
optAll.value = "All";
optAll.textContent = "All";
sel.appendChild(optAll);
[...values].sort().forEach(v => {
const opt = document.createElement("option");
opt.value = v;
opt.textContent = v;
sel.appendChild(opt);
});
}
fillSelect(unitSel, unitSet);
fillSelect(priSel, prioritySet);
fillSelect(fabSel, fabSet);
statusSel.innerHTML = "";
["All","No Ship","Shipped","Received","Installed"].forEach(s => {
const opt = document.createElement("option");
opt.value = s;
opt.textContent = s;
statusSel.appendChild(opt);
});
unitSel.onchange = () => { filters.unit = unitSel.value; applyFilters(); };
priSel.onchange = () => { filters.priority = priSel.value; applyFilters(); };
fabSel.onchange = () => { filters.fabricator = fabSel.value; applyFilters(); };
statusSel.onchange = () => { filters.status = statusSel.value; applyFilters(); };
spoolInput.oninput = () => { filters.spool = spoolInput.value.trim(); applyFilters(); };
}
function getStatus(row) {
if (row.Installed === "Yes") return "Installed";
const received = (row["Received"] || "").toString().trim();
const shipped = (row["Shipped"] || "").toString().trim();
if (received) return "Received";
if (shipped) return "Shipped";
return "No Ship";
}
function passesFilters(row) {
const unit = (row["Unit"] || "Unknown").toString().trim() || "Unknown";
const pri = (row["Priority"] || "Unknown").toString().trim() || "Unknown";
const fab = (row["Fabrication"] || "Unknown").toString().trim() || "Unknown";
const status = getStatus(row);
const spool = (row["Spool Number"] || "").toString().trim();
if (filters.unit !== "All" && unit !== filters.unit) return false;
if (filters.priority !== "All" && pri !== filters.priority) return false;
if (filters.fabricator !== "All" && fab !== filters.fabricator) return false;
if (filters.status !== "All" && status !== filters.status) return false;
if (filters.spool && !spool.includes(filters.spool)) return false;
return true;
}
function applyFilters() {
filteredIso = isoRows.filter(passesFilters);
filteredAll = allRows.filter(passesFilters);
if (!filteredIso.length) {
showError("No ISO rows match the current filters.");
} else {
showStatus("Filtered to " + filteredIso.length + " ISO rows.");
}
updateKpis(filteredIso, filteredAll);
updateFabricatorCards(filteredIso);
updateUnitCards(filteredIso);
updateStatusTables(filteredIso);
updateHeatmap(filteredIso);
updateCharts(filteredIso);
updateCompletionTable(filteredIso);
}
// KPIs
function updateKpis(rowsIso, rowsAll) {
const isoSet = new Set();
const spoolSet = new Set();
const lineSet = new Set();
let delivered = 0;
let installed = 0;
let insulated = 0;
rowsIso.forEach(r => {
const iso = (r["Iso Number"] || "").toString().trim();
if (iso) isoSet.add(iso);
const spool = (r["Spool Number"] || "").toString().trim();
if (spool) spoolSet.add(spool);
const line = (r["Line Number"] || "").toString().trim();
if (line) lineSet.add(line);
const received = (r["Received"] || "").toString().trim();
if (received) delivered++;
if (getStatus(r) === "Installed") installed++;
if (r.Insulated === "Yes") insulated++;
});
rowsAll.forEach(r => {
const line = (r["Line Number"] || "").toString().trim();
if (line) lineSet.add(line);
});
const totalSpools = spoolSet.size;
const remShip = totalSpools - delivered;
const remInstall = totalSpools - installed;
document.getElementById("kpi-total-isos").textContent = isoSet.size;
document.getElementById("kpi-total-spools").textContent = totalSpools;
document.getElementById("kpi-unique-lines").textContent = lineSet.size;
document.getElementById("kpi-delivered").textContent = delivered;
document.getElementById("kpi-rem-ship").textContent = remShip < 0 ? 0 : remShip;
document.getElementById("kpi-rem-install").textContent = remInstall < 0 ? 0 : remInstall;
document.getElementById("kpi-insulated").textContent = insulated;
}
// Fabricator cards
function updateFabricatorCards(rows) {
const container = document.getElementById("fabricator-cards");
container.innerHTML = "";
const map = {};
const overall = { spools:0, forecast:0, shipped:0, received:0 };
rows.forEach(r => {
let fab = (r["Fabrication"] || "Unknown").toString().trim() || "Unknown";
fab = fab.replace(/\u00A0/g," ").replace(/\s+/g," ").toLowerCase();
fab = fab.replace(/\b\w/g, c => c.toUpperCase());
if (!map[fab]) map[fab] = { spools:0, forecast:0, shipped:0, received:0 };
map[fab].spools++;
overall.spools++;
if ((r["Forcast Ship"] || "").toString().trim()) { map[fab].forecast++; overall.forecast++; }
if ((r["Shipped"] || "").toString().trim()) { map[fab].shipped++; overall.shipped++; }
if ((r["Received"] || "").toString().trim()) { map[fab].received++; overall.received++; }
});
const overallPct = overall.spools ? ((overall.received/overall.spools)*100).toFixed(1) : "0.0";
let html = `
<div class="bg-gray-950 rounded-xl p-5 shadow border border-gray-800 col-span-1 md:col-span-2 xl:col-span-3">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold">All Fabricators</h3>
<span class="text-xs text-gray-400">Overview</span>
</div>
<p class="text-gray-300">Total Spools: <span class="font-bold">${overall.spools}</span></p>
<p class="text-gray-300">Forecast Ship: <span>${overall.forecast}</span></p>
<p class="text-gray-300">Shipped: <span>${overall.shipped}</span></p>
<p class="text-gray-300">Received: <span>${overall.received}</span></p>
<p class="mt-3">Progress:
<span class="px-3 py-1 rounded-full bg-emerald-600 text-white text-xs">${overallPct}%</span>
</p>
</div>
`;
Object.entries(map).forEach(([fab,data]) => {
const pct = data.spools ? ((data.received/data.spools)*100).toFixed(1) : "0.0";
html += `
<div class="bg-gray-900 rounded-xl p-5 shadow border border-gray-800">
<h3 class="text-lg font-semibold mb-2">${fab}</h3>
<p class="text-gray-300">Total Spools: <span class="font-bold">${data.spools}</span></p>
<p class="text-gray-300">Forecast Ship: <span>${data.forecast}</span></p>
<p class="text-gray-300">Shipped: <span>${data.shipped}</span></p>
<p class="text-gray-300">Received: <span>${data.received}</span></p>
<p class="mt-3">Progress:
<span class="px-3 py-1 rounded-full bg-emerald-600 text-white text-xs">${pct}%</span>
</p>
</div>
`;
});
container.innerHTML = html;
}
function updateUnitCards(rows) {
const container = document.getElementById("unit-cards");
container.innerHTML = "";
const map = {};
const overall = { spools:0, forecast:0, shipped:0, received:0 };
rows.forEach(r => {
let unit = (r["Unit"] || "Unknown").toString().trim() || "Unknown";
if (!map[unit]) map[unit] = { spools:0, forecast:0, shipped:0, received:0 };
map[unit].spools++;
overall.spools++;
if ((r["Forcast Ship"] || "").toString().trim()) { map[unit].forecast++; overall.forecast++; }
if ((r["Shipped"] || "").toString().trim()) { map[unit].shipped++; overall.shipped++; }
if ((r["Received"] || "").toString().trim()) { map[unit].received++; overall.received++; }
});
const overallPct = overall.spools ? ((overall.received/overall.spools)*100).toFixed(1) : "0.0";
let html = `
<div class="bg-gray-950 rounded-xl p-5 shadow border border-gray-800 col-span-1 md:col-span-2 xl:col-span-3">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold">All Units</h3>
<span class="text-xs text-gray-400">Overview</span>
</div>
<p class="text-gray-300">Total Spools: <span class="font-bold">${overall.spools}</span></p>
<p class="text-gray-300">Forecast Ship: <span>${overall.forecast}</span></p>
<p class="text-gray-300">Shipped: <span>${overall.shipped}</span></p>
<p class="text-gray-300">Received: <span>${overall.received}</span></p>
<p class="mt-3">Progress:
<span class="px-3 py-1 rounded-full bg-emerald-600 text-white text-xs">${overallPct}%</span>
</p>
</div>
`;
const units = Object.entries(map).sort((a,b) => a[0].localeCompare(b[0]));
units.forEach(([unit,data]) => {
const pct = data.spools ? ((data.received/data.spools)*100).toFixed(1) : "0.0";
html += `
<div class="bg-gray-900 rounded-xl p-5 shadow border border-gray-800">
<h3 class="text-lg font-semibold mb-2">${unit}</h3>
<p class="text-gray-300">Total Spools: <span class="font-bold">${data.spools}</span></p>
<p class="text-gray-300">Forecast Ship: <span>${data.forecast}</span></p>
<p class="text-gray-300">Shipped: <span>${data.shipped}</span></p>
<p class="text-gray-300">Received: <span>${data.received}</span></p>
<p class="mt-3">Progress:
<span class="px-3 py-1 rounded-full bg-emerald-600 text-white text-xs">${pct}%</span>
</p>
</div>
`;
});
container.innerHTML = html;
}
// Status tables
function updateStatusTables(rows) {
buildStatusByUnit(rows);
buildStatusByFabricator(rows);
}
function buildStatusByUnit(rows) {
const wrapper = document.getElementById("table-unit-wrapper");
wrapper.innerHTML = "";
const map = {};
let total = { spools:0, delivered:0, remShip:0, installed:0, remInstall:0 };
rows.forEach(r => {
const unit = (r["Unit"] || "Unknown").toString().trim() || "Unknown";
if (!map[unit]) map[unit] = { spools:0, delivered:0, installed:0 };
map[unit].spools++;
total.spools++;
const received = (r["Received"] || "").toString().trim();
const status = getStatus(r);
if (received) { map[unit].delivered++; total.delivered++; }
if (status === "Installed") { map[unit].installed++; total.installed++; }
});
const units = Object.entries(map).sort((a,b) => a[0].localeCompare(b[0]));
let html = `<table class="min-w-full border-collapse">
<thead class="text-xs text-gray-400 border-b border-gray-800">
<tr>
<th class="py-2 pr-3 text-left">Unit</th>
<th class="py-2 pr-3 text-right">Total Spools</th>
<th class="py-2 pr-3 text-right">Delivered</th>
<th class="py-2 pr-3 text-right">% Delivered</th>
<th class="py-2 pr-3 text-right">Rem to Ship</th>
<th class="py-2 pr-3 text-right">Installed</th>
<th class="py-2 pr-3 text-right">% Installed</th>
<th class="py-2 pr-0 text-right">Rem to Install</th>
</tr>
</thead>
<tbody class="text-xs md:text-sm">`;
units.forEach(([unit,data]) => {
const remShip = data.spools - data.delivered;
const remInstall = data.spools - data.installed;
const pctDelivered = data.spools ? (data.delivered/data.spools*100).toFixed(1) : "0.0";
const pctInstalled = data.spools ? (data.installed/data.spools*100).toFixed(1) : "0.0";
html += `
<tr class="border-b border-gray-900">
<td class="py-1.5 pr-3">${unit}</td>
<td class="py-1.5 pr-3 text-right">${data.spools}</td>
<td class="py-1.5 pr-3 text-right">${data.delivered}</td>
<td class="py-1.5 pr-3 text-right">
<div class="flex items-center gap-2 justify-end">
<span>${pctDelivered}%</span>
<div class="w-20 bg-gray-800 rounded-full h-2 overflow-hidden">
<div class="h-2 bg-lime-400" style="width:${pctDelivered}%;"></div>
</div>
</div>
</td>
<td class="py-1.5 pr-3 text-right">${remShip}</td>
<td class="py-1.5 pr-3 text-right">${data.installed}</td>
<td class="py-1.5 pr-3 text-right">
<div class="flex items-center gap-2 justify-end">
<span>${pctInstalled}%</span>
<div class="w-20 bg-gray-800 rounded-full h-2 overflow-hidden">
<div class="h-2 bg-sky-400" style="width:${pctInstalled}%;"></div>
</div>
</div>
</td>
<td class="py-1.5 pr-0 text-right">${remInstall}</td>
</tr>`;
});
total.remShip = total.spools - total.delivered;
total.remInstall = total.spools - total.installed;
const tPctDel = total.spools ? (total.delivered/total.spools*100).toFixed(1) : "0.0";
const tPctInst = total.spools ? (total.installed/total.spools*100).toFixed(1) : "0.0";
html += `
</tbody>
<tfoot class="text-xs md:text-sm font-semibold border-t border-gray-800">
<tr>
<td class="py-2 pr-3">Total</td>
<td class="py-2 pr-3 text-right">${total.spools}</td>
<td class="py-2 pr-3 text-right">${total.delivered}</td>
<td class="py-2 pr-3 text-right">${tPctDel}%</td>
<td class="py-2 pr-3 text-right">${total.remShip}</td>
<td class="py-2 pr-3 text-right">${total.installed}</td>
<td class="py-2 pr-3 text-right">${tPctInst}%</td>
<td class="py-2 pr-0 text-right">${total.remInstall}</td>
</tr>
</tfoot>
</table>`;
wrapper.innerHTML = html;
}
function buildStatusByFabricator(rows) {
const wrapper = document.getElementById("table-fab-wrapper");
wrapper.innerHTML = "";
const map = {};
let total = { spools:0, delivered:0, remShip:0 };
rows.forEach(r => {
let fab = (r["Fabrication"] || "Unknown").toString().trim() || "Unknown";
fab = fab.replace(/\u00A0/g," ").replace(/\s+/g," ").toLowerCase();
fab = fab.replace(/\b\w/g, c => c.toUpperCase());
if (!map[fab]) map[fab] = { spools:0, delivered:0, remShip:0, dates:[] };
map[fab].spools++;
total.spools++;
const received = (r["Received"] || "").toString().trim();
if (received) { map[fab].delivered++; total.delivered++; }
const fDate = (r["Forcast Ship"] || "").toString().trim();
if (fDate) map[fab].dates.push(fDate);
});
Object.values(map).forEach(m => {
m.remShip = m.spools - m.delivered;
});
total.remShip = total.spools - total.delivered;
let html = `<table class="min-w-full border-collapse">
<thead class="text-xs text-gray-400 border-b border-gray-800">
<tr>
<th class="py-2 pr-3 text-left">Fabricator</th>
<th class="py-2 pr-3 text-right">Total</th>
<th class="py-2 pr-3 text-right">Delivered</th>
<th class="py-2 pr-3 text-right">Rem to Ship</th>
<th class="py-2 pr-0 text-right">Final Ship</th>
</tr>
</thead>
<tbody class="text-xs md:text-sm">`;
const fabs = Object.entries(map).sort((a,b) => a[0].localeCompare(b[0]));
fabs.forEach(([fab,data]) => {
const finalShip = data.dates.length ? data.dates.sort().slice(-1)[0] : "";
html += `
<tr class="border-b border-gray-900">
<td class="py-1.5 pr-3">${fab}</td>
<td class="py-1.5 pr-3 text-right">${data.spools}</td>
<td class="py-1.5 pr-3 text-right">${data.delivered}</td>
<td class="py-1.5 pr-3 text-right">${data.remShip}</td>
<td class="py-1.5 pr-0 text-right">${finalShip}</td>
</tr>`;
});
html += `</tbody>
<tfoot class="text-xs md:text-sm font-semibold border-t border-gray-800">
<tr>
<td class="py-2 pr-3">Total</td>
<td class="py-2 pr-3 text-right">${total.spools}</td>
<td class="py-2 pr-3 text-right">${total.delivered}</td>
<td class="py-2 pr-3 text-right">${total.remShip}</td>
<td class="py-2 pr-0 text-right"></td>
</tr>
</tfoot>
</table>`;
wrapper.innerHTML = html;
}
// Heatmap
function updateHeatmap(rows) {
const wrapper = document.getElementById("heatmap-wrapper");
wrapper.innerHTML = "";
const unitsSet = new Set();
const fabsSet = new Set();
const cell = {};
const totalRow = {};
const totalCol = {};
rows.forEach(r => {
let fab = (r["Fabrication"] || "Unknown").toString().trim() || "Unknown";
fab = fab.replace(/\u00A0/g," ").replace(/\s+/g," ").toLowerCase();
fab = fab.replace(/\b\w/g, c => c.toUpperCase());
let unit = (r["Unit"] || "Unknown").toString().trim() || "Unknown";
const received = (r["Received"] || "").toString().trim();
if (received) return;
unitsSet.add(unit);
fabsSet.add(fab);
const key = fab + "|" + unit;
cell[key] = (cell[key] || 0) + 1;
});
const units = [...unitsSet].sort();
const fabs = [...fabsSet].sort();
fabs.forEach(fab => {
let rowTotal = 0;
units.forEach(unit => {
const key = fab + "|" + unit;
const v = cell[key] || 0;
rowTotal += v;
totalCol[unit] = (totalCol[unit] || 0) + v;
});
totalRow[fab] = rowTotal;
});
const grandTotal = Object.values(totalRow).reduce((a,b) => a+b,0);
const maxVal = Math.max(1, ...Object.values(cell));
function bgFor(v) {
if (v === 0) return "bg-gray-900";
const intensity = v / maxVal;
if (intensity < 0.25) return "bg-emerald-900";
if (intensity < 0.5) return "bg-emerald-800";
if (intensity < 0.75) return "bg-emerald-700";
return "bg-emerald-600";
}
let html = `<table class="border-collapse min-w-full">
<thead class="text-xs text-gray-400">
<tr>
<th class="py-2 px-3 text-left border-b border-gray-800">Fabricator</th>`;
units.forEach(u => {
html += `<th class="py-2 px-3 text-center border-b border-gray-800">${u}</th>`;
});
html += `<th class="py-2 px-3 text-center border-b border-gray-800">Total</th></tr></thead><tbody class="text-xs md:text-sm">`;
fabs.forEach(fab => {
html += `<tr class="border-b border-gray-900">
<td class="py-1.5 px-3">${fab}</td>`;
units.forEach(unit => {
const key = fab + "|" + unit;
const v = cell[key] || 0;
html += `<td class="py-1.5 px-1 text-center">
<div class="inline-flex items-center justify-center rounded-md ${bgFor(v)} px-2 py-0.5 min-w-[36px]">
<span class="text-xs">${v}</span>
</div>
</td>`;
});
html += `<td class="py-1.5 px-3 text-center font-semibold">${totalRow[fab] || 0}</td></tr>`;
});
html += `</tbody>
<tfoot class="text-xs md:text-sm font-semibold border-t border-gray-800">
<tr>
<td class="py-2 px-3">Total</td>`;
units.forEach(unit => {
const v = totalCol[unit] || 0;
html += `<td class="py-2 px-1 text-center">${v}</td>`;
});
html += `<td class="py-2 px-3 text-center">${grandTotal}</td></tr></tfoot></table>`;
wrapper.innerHTML = html;
}
// Chart helpers
function groupIsoByField(rows, field) {
const map = new Map();
rows.forEach(r => {
const iso = (r["Iso Number"] || "").toString().trim();
if (!iso) return;
let key = (r[field] || "Unknown").toString().trim() || "Unknown";
if (!map.has(key)) map.set(key, new Set());
map.get(key).add(iso);
});
return [...map.entries()].map(([label,set]) => ({label, count:set.size}));
}
function groupSpoolByField(rows, field) {
const map = new Map();
rows.forEach(r => {
const spool = (r["Spool Number"] || "").toString().trim();
if (!spool) return;
let key = (r[field] || "Unknown").toString().trim() || "Unknown";
if (!map.has(key)) map.set(key, new Set());
map.get(key).add(spool);
});
return [...map.entries()].map(([label,set]) => ({label, count:set.size}));
}
function groupSpoolsInstalledRemainingByUnit(rows) {
const map = new Map();
rows.forEach(r => {
const spool = (r["Spool Number"] || "").toString().trim();
if (!spool) return;
const unit = (r["Unit"] || "Unknown").toString().trim() || "Unknown";
if (!map.has(unit)) {
map.set(unit, { all: new Set(), installed: new Set() });
}
const entry = map.get(unit);
entry.all.add(spool);
if (getStatus(r) === "Installed") {
entry.installed.add(spool);
}
});
return [...map.entries()].map(([unit, entry]) => {
const installed = entry.installed.size;
const total = entry.all.size;
const remaining = total - installed;
return { label: unit, installed, remaining };
});
}
function updateCharts(rows) {
let unit = groupIsoByField(rows, "Unit");
let zone = groupIsoByField(rows, "Zone");
let nde = groupIsoByField(rows, "NDE Class");
let spoolUnit = groupSpoolByField(rows, "Unit");
let spoolInstallUnit = groupSpoolsInstalledRemainingByUnit(rows);
// sort ascending by count
unit.sort((a,b) => a.count - b.count);
zone.sort((a,b) => a.count - b.count);
nde.sort((a,b) => a.count - b.count);
spoolUnit.sort((a,b) => a.count - b.count);
spoolInstallUnit.sort((a,b) => (a.installed + a.remaining) - (b.installed + b.remaining));
renderBarChart("chart-unit", unit);
renderBarChart("chart-zone", zone);
renderBarChart("chart-nde", nde);
renderBarChart("chart-spool-unit", spoolUnit);
renderBarChartDual("chart-spool-unit-install", spoolInstallUnit);
}
function renderBarChart(id, data) {
const canvas = document.getElementById(id);
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (charts[id]) charts[id].destroy();
charts[id] = new Chart(ctx, {
type: "bar",
data: {
labels: data.map(d => d.label),
datasets: [{
data: data.map(d => d.count),
backgroundColor: "rgba(56, 189, 248, 0.8)"
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: "#e5e7eb" }, grid: { display:false } },
y: { beginAtZero:true, ticks:{ color:"#9ca3af", precision:0 }, grid:{ color:"#111827" } }
}
}
});
}
function renderBarChartDual(id, data) {
const canvas = document.getElementById(id);
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (charts[id]) charts[id].destroy();
charts[id] = new Chart(ctx, {
type: "bar",
data: {
labels: data.map(d => d.label),
datasets: [
{
label: "Installed",
data: data.map(d => d.installed),
backgroundColor: "rgba(16, 185, 129, 0.9)"
},
{
label: "Remaining",
data: data.map(d => d.remaining),
backgroundColor: "rgba(239, 68, 68, 0.8)"
}
]
},
options: {
responsive: true,
plugins: {
legend: { display: true, labels: { color: "#e5e7eb" } }
},
scales: {
x: { ticks: { color: "#e5e7eb" }, grid: { display:false } },
y: { beginAtZero:true, ticks:{ color:"#9ca3af", precision:0 }, grid:{ color:"#111827" } }
}
}
});
}
// Report download (CSV-style summary)
function downloadReport() {
if (!filteredIso || !filteredIso.length) {
showError("No data available to download. Load a CSV first.");
return;
}
const rowsIso = filteredIso;
const rowsAll = filteredAll || [];
const isoSet = new Set();
const spoolSet = new Set();
const lineSet = new Set();
let delivered = 0;
let installed = 0;
let insulated = 0;
rowsIso.forEach(r => {
const iso = (r["Iso Number"] || "").toString().trim();
if (iso) isoSet.add(iso);
const spool = (r["Spool Number"] || "").toString().trim();
if (spool) spoolSet.add(spool);
const line = (r["Line Number"] || "").toString().trim();
if (line) lineSet.add(line);
const received = (r["Received"] || "").toString().trim();
if (received) delivered++;
if (getStatus(r) === "Installed") installed++;
if (r.Insulated === "Yes") insulated++;
});
rowsAll.forEach(r => {
const line = (r["Line Number"] || "").toString().trim();
if (line) lineSet.add(line);
});
const totalSpools = spoolSet.size;
const remShip = totalSpools - delivered;
const remInstall = totalSpools - installed;
const unitMap = {};
rowsIso.forEach(r => {
const unit = (r["Unit"] || "Unknown").toString().trim() || "Unknown";
if (!unitMap[unit]) unitMap[unit] = { spools:0, delivered:0, installed:0 };
unitMap[unit].spools++;
const received = (r["Received"] || "").toString().trim();
const status = getStatus(r);
if (received) unitMap[unit].delivered++;
if (status === "Installed") unitMap[unit].installed++;
});
const fabMap = {};
rowsIso.forEach(r => {
let fab = (r["Fabrication"] || "Unknown").toString().trim() || "Unknown";
fab = fab.replace(/\u00A0/g," ").replace(/\s+/g," ").toLowerCase();
fab = fab.replace(/\b\w/g, c => c.toUpperCase());
if (!fabMap[fab]) fabMap[fab] = { spools:0, delivered:0, remShip:0 };
fabMap[fab].spools++;
const received = (r["Received"] || "").toString().trim();
if (received) fabMap[fab].delivered++;
});
Object.values(fabMap).forEach(m => { m.remShip = m.spools - m.delivered; });
const lines = [];
lines.push("ISO Scope Tracker Report");
lines.push("Generated," + new Date().toISOString());
lines.push("");
lines.push("High-Level KPIs");
lines.push("Metric,Value");
lines.push("Total ISOs," + isoSet.size);
lines.push("Total Spools," + totalSpools);
lines.push("Unique Lines," + lineSet.size);
lines.push("Delivered (Received)," + delivered);
lines.push("Remaining to Ship," + (remShip < 0 ? 0 : remShip));
lines.push("Remaining to Install," + (remInstall < 0 ? 0 : remInstall));
lines.push("Insulated," + insulated);
lines.push("");
lines.push("Status by Unit");
lines.push("Unit,Total Spools,Delivered,Installed");
Object.keys(unitMap).sort().forEach(u => {
const d = unitMap[u];
lines.push([u,d.spools,d.delivered,d.installed].join(","));
});
lines.push("");
lines.push("Status by Fabricator");
lines.push("Fabricator,Total Spools,Delivered,Rem to Ship");
Object.keys(fabMap).sort().forEach(f => {
const d = fabMap[f];
lines.push([f,d.spools,d.delivered,d.remShip].join(","));
});
const blob = new Blob([lines.join("\n")], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "ISO_Scope_Report.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Completion Summary
function updateCompletionTable(rows) {
const isoInstalled = new Set();
const isoPainted = new Set();
const isoInsulated = new Set();
const isoAll = new Set();
function isYes(value) {
if (!value) return false;
const v = value.toString().trim().toLowerCase();
return v === "yes";
}
rows.forEach(r => {
const iso = (r["Iso Number"] || "").toString().trim();
if (!iso) return;
isoAll.add(iso);
if (getStatus(r) === "Installed") isoInstalled.add(iso);
if (isYes(r.Painted)) isoPainted.add(iso);
if (isYes(r.Insulated)) isoInsulated.add(iso);
});
const installed = isoInstalled.size;
const painted = isoPainted.size;
const insulated = isoInsulated.size;
const total = isoAll.size || 1;
document.getElementById("completion-table-wrapper").innerHTML = `
<table class="min-w-full border-collapse">
<thead class="text-xs text-gray-400 border-b border-gray-800">
<tr>
<th class="py-2 pr-3 text-left">Metric</th>
<th class="py-2 pr-3 text-right">Count</th>
<th class="py-2 pr-0 text-right">% of Filtered ISOs</th>
</tr>
</thead>
<tbody class="text-xs md:text-sm">
<tr class="border-b border-gray-900">
<td class="py-1.5 pr-3">Installed</td>
<td class="py-1.5 pr-3 text-right">${installed}</td>
<td class="py-1.5 pr-0 text-right">${(installed/total*100).toFixed(1)}%</td>
</tr>
<tr class="border-b border-gray-900">
<td class="py-1.5 pr-3">Painted</td>
<td class="py-1.5 pr-3 text-right">${painted}</td>
<td class="py-1.5 pr-0 text-right">${(painted/total*100).toFixed(1)}%</td>
</tr>
<tr>
<td class="py-1.5 pr-3">Insulated</td>
<td class="py-1.5 pr-3 text-right">${insulated}</td>
<td class="py-1.5 pr-0 text-right">${(insulated/total*100).toFixed(1)}%</td>
</tr>
</tbody>
</table>
`;
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=Kaliman-1981/tempspooltracker" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>