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> |