Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Commissioning Management Dashboard</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
body { | |
font-family: 'Inter', sans-serif; | |
background-color: #f8fafc; | |
} | |
.card-hover { | |
transition: all 0.3s ease; | |
} | |
.card-hover:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 10px 20px rgba(0,0,0,0.1); | |
} | |
.animate-fadeIn { | |
animation: fadeIn 0.5s ease-in; | |
} | |
@keyframes fadeIn { | |
from { opacity: 0; } | |
to { opacity: 1; } | |
} | |
.progress-bar { | |
height: 6px; | |
border-radius: 3px; | |
background-color: #e2e8f0; | |
} | |
.progress-bar-fill { | |
height: 100%; | |
border-radius: 3px; | |
transition: width 0.6s ease; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50"> | |
<div class="container mx-auto px-4 py-8"> | |
<header class="mb-8"> | |
<h1 class="text-3xl font-bold text-gray-800">Commissioning Management Dashboard</h1> | |
<p class="text-gray-600">Real-time tracking of form status, item progress, and punch list clearance</p> | |
</header> | |
<!-- Section 1: Summary Cards --> | |
<section class="mb-12 animate-fadeIn"> | |
<h2 class="text-xl font-semibold mb-6 text-gray-700">Key Performance Indicators</h2> | |
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" id="kpiCards"> | |
<!-- Cards will be dynamically inserted here --> | |
<div class="bg-white rounded-xl shadow-sm p-6 card-hover flex items-center"> | |
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> | |
<span class="ml-4 text-gray-500">Loading KPIs...</span> | |
</div> | |
</div> | |
</section> | |
<!-- Section 2: Item Progress by Discipline --> | |
<section class="mb-12 animate-fadeIn"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 class="text-xl font-semibold text-gray-700">Item Progress by Discipline</h2> | |
<div class="flex space-x-2"> | |
<button class="px-3 py-1 bg-blue-50 text-blue-600 rounded-md text-sm font-medium">Export</button> | |
<button class="px-3 py-1 bg-blue-50 text-blue-600 rounded-md text-sm font-medium">Filter</button> | |
</div> | |
</div> | |
<div class="bg-white rounded-xl shadow-sm overflow-hidden mb-8"> | |
<div class="overflow-x-auto"> | |
<table class="min-w-full divide-y divide-gray-200" id="itemProgressTable"> | |
<thead class="bg-gray-50"> | |
<tr> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Discipline</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Items</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Done</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">In Progress</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Hold</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Remain</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Progress</th> | |
</tr> | |
</thead> | |
<tbody class="bg-white divide-y divide-gray-200" id="itemProgressBody"> | |
<!-- Data will be dynamically inserted here --> | |
<tr> | |
<td colspan="7" class="px-6 py-4 text-center text-gray-500"> | |
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div> | |
<span class="ml-2">Loading item progress data...</span> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
<div class="bg-white rounded-xl shadow-sm p-6"> | |
<canvas id="itemProgressChart"></canvas> | |
</div> | |
</section> | |
<!-- Section 3: Punch List Status by Discipline --> | |
<section class="animate-fadeIn"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 class="text-xl font-semibold text-gray-700">Punch List Status by Discipline</h2> | |
<div class="flex space-x-2"> | |
<button class="px-3 py-1 bg-blue-50 text-blue-600 rounded-md text-sm font-medium">Export</button> | |
<button class="px-3 py-1 bg-blue-50 text-blue-600 rounded-md text-sm font-medium">Filter</button> | |
</div> | |
</div> | |
<div class="bg-white rounded-xl shadow-sm overflow-hidden mb-8"> | |
<div class="overflow-x-auto"> | |
<table class="min-w-full divide-y divide-gray-200" id="punchStatusTable"> | |
<thead class="bg-gray-50"> | |
<tr> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Discipline</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Punch</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cleared</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">In Progress</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ready For Approve</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Remain</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Progress</th> | |
</tr> | |
</thead> | |
<tbody class="bg-white divide-y divide-gray-200" id="punchStatusBody"> | |
<!-- Data will be dynamically inserted here --> | |
<tr> | |
<td colspan="7" class="px-6 py-4 text-center text-gray-500"> | |
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div> | |
<span class="ml-2">Loading punch status data...</span> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
<div class="bg-white rounded-xl shadow-sm p-6"> | |
<canvas id="punchStatusChart"></canvas> | |
</div> | |
</section> | |
</div> | |
<script> | |
// Global variables to store chart instances | |
let itemProgressChart; | |
let punchStatusChart; | |
// Sample data (as fallback) | |
const sampleDisciplineData = `Total Subsystems: 45 | |
Form A: 12 | |
Form B: 8 | |
Form C: 15 | |
Form D: 10 | |
Mechanical|120|80|20|10|10 | |
Electrical|95|60|15|10|10 | |
Civil|80|50|15|5|10 | |
Instrumentation|65|40|10|5|10 | |
Piping|110|70|20|10|10`; | |
const samplePunchData = `Mechanical|45|20|10|5|10 | |
Electrical|35|15|8|5|7 | |
Civil|25|10|5|3|7 | |
Instrumentation|20|8|4|3|5 | |
Piping|40|18|10|5|7`; | |
// Fetch data with CORS proxy | |
async function fetchWithProxy(url) { | |
try { | |
const proxyUrl = 'https://cors-anywhere.herokuapp.com/'; | |
const response = await fetch(proxyUrl + url); | |
return await response.text(); | |
} catch (error) { | |
console.error('Error using proxy:', error); | |
// Return sample data if proxy fails | |
if (url.includes('discipline')) return sampleDisciplineData; | |
if (url.includes('punch')) return samplePunchData; | |
return ''; | |
} | |
} | |
// Fetch data from GitHub | |
async function fetchData() { | |
try { | |
// Fetch discipline data | |
const disciplineText = await fetchWithProxy('https://raw.githubusercontent.com/akarimvand/hos/refs/heads/main/discipline.txt'); | |
const disciplineData = parseDisciplineData(disciplineText); | |
// Fetch punch data | |
const punchText = await fetchWithProxy('https://raw.githubusercontent.com/akarimvand/hos/refs/heads/main/punch.txt'); | |
const punchData = parsePunchData(punchText); | |
// Update the UI with the fetched data | |
updateKPICards(disciplineData.kpi); | |
updateItemProgressTable(disciplineData.items); | |
updatePunchStatusTable(punchData); | |
createItemProgressChart(disciplineData.items); | |
createPunchStatusChart(punchData); | |
} catch (error) { | |
console.error('Error fetching data:', error); | |
showError('Failed to load data. Using sample data instead.'); | |
// Use sample data as fallback | |
const disciplineData = parseDisciplineData(sampleDisciplineData); | |
const punchData = parsePunchData(samplePunchData); | |
updateKPICards(disciplineData.kpi); | |
updateItemProgressTable(disciplineData.items); | |
updatePunchStatusTable(punchData); | |
createItemProgressChart(disciplineData.items); | |
createPunchStatusChart(punchData); | |
} | |
} | |
// Parse discipline data from text | |
function parseDisciplineData(text) { | |
const lines = text.split('\n'); | |
const data = { | |
kpi: {}, | |
items: [] | |
}; | |
// Parse KPI data (first few lines) | |
for (let i = 0; i < 5; i++) { | |
if (lines[i]) { | |
const [key, value] = lines[i].split(':').map(item => item.trim()); | |
data.kpi[key] = parseInt(value); | |
} | |
} | |
// Parse item progress data (remaining lines) | |
for (let i = 5; i < lines.length; i++) { | |
if (lines[i].trim()) { | |
const parts = lines[i].split('|').map(item => item.trim()); | |
if (parts.length >= 6) { | |
data.items.push({ | |
discipline: parts[0], | |
total: parseInt(parts[1]), | |
done: parseInt(parts[2]), | |
inProgress: parseInt(parts[3]), | |
hold: parseInt(parts[4]), | |
remain: parseInt(parts[5]) | |
}); | |
} | |
} | |
} | |
return data; | |
} | |
// Parse punch data from text | |
function parsePunchData(text) { | |
const lines = text.split('\n'); | |
const data = []; | |
for (let i = 0; i < lines.length; i++) { | |
if (lines[i].trim()) { | |
const parts = lines[i].split('|').map(item => item.trim()); | |
if (parts.length >= 6) { | |
data.push({ | |
discipline: parts[0], | |
total: parseInt(parts[1]), | |
cleared: parseInt(parts[2]), | |
inProgress: parseInt(parts[3]), | |
readyForApprove: parseInt(parts[4]), | |
remain: parseInt(parts[5]) | |
}); | |
} | |
} | |
} | |
return data; | |
} | |
// Update KPI cards | |
function updateKPICards(kpiData) { | |
const kpiCards = document.getElementById('kpiCards'); | |
const cards = [ | |
{ | |
title: 'Total Subsystems', | |
value: kpiData['Total Subsystems'] || 0, | |
icon: 'fas fa-layer-group', | |
color: 'bg-blue-100', | |
textColor: 'text-blue-600' | |
}, | |
{ | |
title: 'Completed Form A', | |
value: kpiData['Form A'] || 0, | |
icon: 'fas fa-file-alt', | |
color: 'bg-green-100', | |
textColor: 'text-green-600' | |
}, | |
{ | |
title: 'Completed Form B', | |
value: kpiData['Form B'] || 0, | |
icon: 'fas fa-file-invoice', | |
color: 'bg-orange-100', | |
textColor: 'text-orange-600' | |
}, | |
{ | |
title: 'Completed Form C', | |
value: kpiData['Form C'] || 0, | |
icon: 'fas fa-file-signature', | |
color: 'bg-purple-100', | |
textColor: 'text-purple-600' | |
} | |
]; | |
kpiCards.innerHTML = cards.map(card => ` | |
<div class="bg-white rounded-xl shadow-sm p-6 card-hover"> | |
<div class="flex items-center"> | |
<div class="p-3 rounded-lg ${card.color} ${card.textColor} mr-4"> | |
<i class="${card.icon} text-xl"></i> | |
</div> | |
<div> | |
<p class="text-sm font-medium text-gray-500">${card.title}</p> | |
<h3 class="text-2xl font-bold text-gray-800">${card.value}</h3> | |
</div> | |
</div> | |
</div> | |
`).join(''); | |
} | |
// Update item progress table | |
function updateItemProgressTable(items) { | |
const tableBody = document.getElementById('itemProgressBody'); | |
tableBody.innerHTML = items.map(item => { | |
const progress = ((item.done / item.total) * 100).toFixed(1); | |
return ` | |
<tr class="hover:bg-gray-50"> | |
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${item.discipline}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.total}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.done}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.inProgress}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.hold}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.remain}</td> | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<div class="flex items-center"> | |
<div class="w-16 mr-2"> | |
<div class="progress-bar"> | |
<div class="progress-bar-fill bg-blue-500" style="width: ${progress}%"></div> | |
</div> | |
</div> | |
<span class="text-xs text-gray-500">${progress}%</span> | |
</div> | |
</td> | |
</tr> | |
`; | |
}).join(''); | |
} | |
// Update punch status table | |
function updatePunchStatusTable(punchData) { | |
const tableBody = document.getElementById('punchStatusBody'); | |
tableBody.innerHTML = punchData.map(item => { | |
const progress = ((item.cleared / item.total) * 100).toFixed(1); | |
return ` | |
<tr class="hover:bg-gray-50"> | |
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${item.discipline}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.total}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.cleared}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.inProgress}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.readyForApprove}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.remain}</td> | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<div class="flex items-center"> | |
<div class="w-16 mr-2"> | |
<div class="progress-bar"> | |
<div class="progress-bar-fill bg-green-500" style="width: ${progress}%"></div> | |
</div> | |
</div> | |
<span class="text-xs text-gray-500">${progress}%</span> | |
</div> | |
</td> | |
</tr> | |
`; | |
}).join(''); | |
} | |
// Create item progress chart | |
function createItemProgressChart(items) { | |
const ctx = document.getElementById('itemProgressChart').getContext('2d'); | |
// Destroy previous chart if it exists | |
if (itemProgressChart) { | |
itemProgressChart.destroy(); | |
} | |
const labels = items.map(item => item.discipline); | |
const doneData = items.map(item => item.done); | |
const inProgressData = items.map(item => item.inProgress); | |
const holdData = items.map(item => item.hold); | |
const remainData = items.map(item => item.remain); | |
itemProgressChart = new Chart(ctx, { | |
type: 'bar', | |
data: { | |
labels: labels, | |
datasets: [ | |
{ | |
label: 'Done', | |
data: doneData, | |
backgroundColor: '#10B981', | |
stack: 'Stack 0' | |
}, | |
{ | |
label: 'In Progress', | |
data: inProgressData, | |
backgroundColor: '#3B82F6', | |
stack: 'Stack 0' | |
}, | |
{ | |
label: 'Hold', | |
data: holdData, | |
backgroundColor: '#F59E0B', | |
stack: 'Stack 0' | |
}, | |
{ | |
label: 'Remain', | |
data: remainData, | |
backgroundColor: '#EF4444', | |
stack: 'Stack 0' | |
} | |
] | |
}, | |
options: { | |
responsive: true, | |
plugins: { | |
title: { | |
display: true, | |
text: 'Item Progress Breakdown by Discipline', | |
font: { | |
size: 16 | |
} | |
}, | |
legend: { | |
position: 'bottom', | |
}, | |
tooltip: { | |
mode: 'index', | |
intersect: false | |
} | |
}, | |
scales: { | |
x: { | |
stacked: true, | |
grid: { | |
display: false | |
} | |
}, | |
y: { | |
stacked: true, | |
beginAtZero: true, | |
ticks: { | |
precision: 0 | |
} | |
} | |
}, | |
animation: { | |
duration: 1000 | |
} | |
} | |
}); | |
} | |
// Create punch status chart | |
function createPunchStatusChart(punchData) { | |
const ctx = document.getElementById('punchStatusChart').getContext('2d'); | |
// Destroy previous chart if it exists | |
if (punchStatusChart) { | |
punchStatusChart.destroy(); | |
} | |
const labels = punchData.map(item => item.discipline); | |
const clearedData = punchData.map(item => item.cleared); | |
const inProgressData = punchData.map(item => item.inProgress); | |
const readyData = punchData.map(item => item.readyForApprove); | |
const remainData = punchData.map(item => item.remain); | |
punchStatusChart = new Chart(ctx, { | |
type: 'bar', | |
data: { | |
labels: labels, | |
datasets: [ | |
{ | |
label: 'Cleared', | |
data: clearedData, | |
backgroundColor: '#10B981', | |
stack: 'Stack 0' | |
}, | |
{ | |
label: 'In Progress', | |
data: inProgressData, | |
backgroundColor: '#3B82F6', | |
stack: 'Stack 0' | |
}, | |
{ | |
label: 'Ready For Approve', | |
data: readyData, | |
backgroundColor: '#8B5CF6', | |
stack: 'Stack 0' | |
}, | |
{ | |
label: 'Remain', | |
data: remainData, | |
backgroundColor: '#EF4444', | |
stack: 'Stack 0' | |
} | |
] | |
}, | |
options: { | |
indexAxis: 'y', | |
responsive: true, | |
plugins: { | |
title: { | |
display: true, | |
text: 'Punch List Status by Discipline', | |
font: { | |
size: 16 | |
} | |
}, | |
legend: { | |
position: 'bottom', | |
}, | |
tooltip: { | |
mode: 'index', | |
intersect: false | |
} | |
}, | |
scales: { | |
x: { | |
stacked: true, | |
grid: { | |
display: false | |
} | |
}, | |
y: { | |
stacked: true, | |
beginAtZero: true, | |
grid: { | |
display: false | |
} | |
} | |
}, | |
animation: { | |
duration: 1000 | |
} | |
} | |
}); | |
} | |
// Show error message | |
function showError(message) { | |
const kpiCards = document.getElementById('kpiCards'); | |
kpiCards.innerHTML = ` | |
<div class="col-span-4 bg-red-50 border-l-4 border-red-500 p-4"> | |
<div class="flex"> | |
<div class="flex-shrink-0"> | |
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> | |
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> | |
</svg> | |
</div> | |
<div class="ml-3"> | |
<p class="text-sm text-red-700">${message}</p> | |
</div> | |
</div> | |
</div> | |
`; | |
} | |
// Initialize the dashboard when the page loads | |
document.addEventListener('DOMContentLoaded', fetchData); | |
</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-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Akarimvand/vxzfc" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |