webhook
This commit is contained in:
@@ -1,51 +1,82 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-2 px-md-4">
|
||||
<h4 class="mb-3 text-center text-md-start">Comparison Software Solapur (UGD) - Live Dashboard</h4>
|
||||
<div class="container-fluid py-4" style="background-color: #f8f9fa;">
|
||||
<h3 class="mb-4 fw-bold text-uppercase">Abstract Excavation Dashboard</h3>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card text-white bg-primary shadow h-100">
|
||||
<div class="card-body text-center text-md-start">
|
||||
<h6>Trenching Units</h6>
|
||||
<h3 class="fw-bold" id="card-trench">0</h3>
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body bg-white">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold">Comparison Type</label>
|
||||
<select id="filter-table" class="form-select" onchange="loadDashboardData()">
|
||||
<option value="trench">Trench Excavation</option>
|
||||
<option value="manhole">Manhole Excavation</option>
|
||||
<option value="laying">Laying</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card text-white bg-success shadow h-100">
|
||||
<div class="card-body text-center text-md-start">
|
||||
<h6>Manhole Units</h6>
|
||||
<h3 class="fw-bold" id="card-manhole">0</h3>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">Subcontractor</label>
|
||||
<select id="filter-subcon" class="form-select" onchange="loadDashboardData()">
|
||||
<option value="All">All Subcontractors</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card text-dark bg-warning shadow h-100">
|
||||
<div class="card-body text-center text-md-start">
|
||||
<h6>Laying Units</h6>
|
||||
<h3 class="fw-bold" id="card-laying">0</h3>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">RA Bill No</label>
|
||||
<select id="filter-ra" class="form-select" onchange="loadDashboardData()">
|
||||
<option value="Cumulative">Cumulative (All Bills)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end gap-2">
|
||||
<button class="btn btn-primary flex-grow-1" onclick="loadDashboardData()">🔄 Refresh</button>
|
||||
<button class="btn btn-secondary flex-grow-1" onclick="clearDashboard()">🗑️ Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-dark text-white">Live Category Bar Chart</div>
|
||||
<div class="card-body">
|
||||
<canvas id="liveBarChart" style="max-height:300px;"></canvas>
|
||||
<!-- Empty State (Shown on page load) -->
|
||||
<div id="empty-state" class="alert alert-info text-center py-5">
|
||||
<h5>📊 Select filters to display data</h5>
|
||||
<p>Choose a Subcontractor and/or RA Bill to see the excavation abstract comparison.</p>
|
||||
</div>
|
||||
|
||||
<!-- Data Display Area (Hidden by default) -->
|
||||
<div id="data-area" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-primary text-white fw-bold d-flex justify-content-between align-items-center">
|
||||
<span id="chart-title">Excavation Comparison: Client vs Subcontractor Qty</span>
|
||||
<small class=\"fw-normal\">(Horizontal Bar Chart)</small>
|
||||
</div>
|
||||
<div class=\"card-body\" style=\"position: relative; height: 700px; overflow-y: auto;\">
|
||||
<canvas id="groupedBarChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-dark text-white">Location Distribution Pie Chart</div>
|
||||
<div class="card-body">
|
||||
<canvas id="livePieChart" style="max-height:300px;"></canvas>
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-success text-white fw-bold">Excavation Abstract Table</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 500px; overflow-y: auto;">
|
||||
<table class="table table-hover mb-0" id="abstract-table">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th class="small">Soil / Depth</th>
|
||||
<th class="small">Client (m³)</th>
|
||||
<th class="small">Subcon (m³)</th>
|
||||
<th class="small">Diff</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tfoot class="table-light fw-bold position-sticky bottom-0">
|
||||
<tr id="table-totals" class="bg-light"></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,64 +86,290 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<script>
|
||||
// 2. Initialize the Bar Chart
|
||||
const barCtx = document.getElementById('liveBarChart').getContext('2d');
|
||||
let liveBarChart = new Chart(barCtx, {
|
||||
type: 'bar',
|
||||
let comparisonChart;
|
||||
|
||||
// Define color palette - ONLY 2 COLORS
|
||||
const colorPalette = {
|
||||
'client': '#003D7A', // Dark Blue for Client (RA Bill)
|
||||
'subcon': '#87CEEB' // Light Sky Blue for Subcontractor
|
||||
};
|
||||
|
||||
// 1. Function to Initialize or Update the Chart (VERTICAL BARS - 2 COLORS ONLY)
|
||||
function updateChartUI(labels, clientData, subconData) {
|
||||
const ctx = document.getElementById('groupedBarChart').getContext('2d');
|
||||
if (comparisonChart) comparisonChart.destroy();
|
||||
|
||||
comparisonChart = new Chart(ctx, {
|
||||
type: 'bar', // Vertical bar chart
|
||||
data: {
|
||||
labels: ['Trenching', 'Manholes', 'Laying'],
|
||||
datasets: [{
|
||||
label: 'Units Completed',
|
||||
data: [0, 0, 0],
|
||||
backgroundColor: ['#0d6efd', '#198754', '#ffc107']
|
||||
}]
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Client Qty (m³)',
|
||||
data: clientData,
|
||||
backgroundColor: colorPalette.client,
|
||||
borderColor: '#001F4D',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
hoverBackgroundColor: '#002A5C',
|
||||
hoverBorderWidth: 2
|
||||
},
|
||||
{
|
||||
label: 'Subcontractor Qty (m³)',
|
||||
data: subconData,
|
||||
backgroundColor: colorPalette.subcon,
|
||||
borderColor: '#4A90B8',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
hoverBackgroundColor: '#6BB3D9',
|
||||
hoverBorderWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: { responsive: true, maintainAspectRatio: false }
|
||||
options: {
|
||||
indexAxis: 'x', // Vertical bars (default)
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
font: { size: 14, weight: 'bold' },
|
||||
padding: 15,
|
||||
usePointStyle: true,
|
||||
boxWidth: 15
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: '#2c3e50',
|
||||
padding: 12,
|
||||
titleFont: { size: 13, weight: 'bold' },
|
||||
bodyFont: { size: 12 },
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1,
|
||||
displayColors: true,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) label += ': ';
|
||||
label += Number(context.parsed.y).toLocaleString('en-IN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}) + ' m³';
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: false,
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
font: { size: 11 },
|
||||
maxRotation: 45,
|
||||
minRotation: 0
|
||||
}
|
||||
},
|
||||
y: {
|
||||
stacked: false,
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: '#ecf0f1',
|
||||
drawBorder: false
|
||||
},
|
||||
ticks: {
|
||||
font: { size: 11 },
|
||||
callback: function(value) {
|
||||
return Number(value).toLocaleString('en-IN');
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Excavation Quantity (m³)',
|
||||
font: { size: 12, weight: 'bold' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Initialize the Pie Chart
|
||||
const pieCtx = document.getElementById('livePieChart').getContext('2d');
|
||||
let livePieChart = new Chart(pieCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: [], // Will be filled from SQL
|
||||
datasets: [{
|
||||
data: [],
|
||||
backgroundColor: ['#0d6efd', '#198754', '#ffc107', '#6f42c1', '#fd7e14']
|
||||
}]
|
||||
},
|
||||
options: { responsive: true, maintainAspectRatio: false }
|
||||
});
|
||||
// 2. Function to fetch unique filters (Subcontractors & RA Bills) from DB
|
||||
function loadFilters() {
|
||||
console.log("🔄 Loading filters from /dashboard/api/filters...");
|
||||
fetch('/dashboard/api/filters')
|
||||
.then(res => {
|
||||
console.log(`Response status: ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log("✓ Filter data received:", data);
|
||||
|
||||
const raSelect = document.getElementById('filter-ra');
|
||||
|
||||
// CRITICAL: This clears the "RA-01", "RA-02" you typed in manually
|
||||
raSelect.innerHTML = '<option value="Cumulative">Cumulative (All Bills)</option>';
|
||||
|
||||
// 4. Function to Fetch Live Data from your Python API
|
||||
function fetchLiveData() {
|
||||
fetch('/dashboard/api/live-stats') // This matches the route we created in the "Kitchen"
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Update the Summary Cards
|
||||
document.getElementById('card-trench').innerText = data.summary.trench;
|
||||
document.getElementById('card-manhole').innerText = data.summary.manhole;
|
||||
document.getElementById('card-laying').innerText = data.summary.laying;
|
||||
if (data.ra_bills && data.ra_bills.length > 0) {
|
||||
console.log(`Adding ${data.ra_bills.length} RA bills to dropdown`);
|
||||
data.ra_bills.forEach(billNo => {
|
||||
let opt = document.createElement('option');
|
||||
opt.value = billNo;
|
||||
opt.innerText = billNo; // This will show exactly what's in the DB
|
||||
raSelect.appendChild(opt);
|
||||
console.log(` + Added RA Bill: ${billNo}`);
|
||||
});
|
||||
} else {
|
||||
console.warn("❌ No RA bills found in response");
|
||||
}
|
||||
|
||||
// Repeat same for subcontractor dropdown
|
||||
const subconSelect = document.getElementById('filter-subcon');
|
||||
subconSelect.innerHTML = '<option value="All">All Subcontractors</option>';
|
||||
if (data.subcontractors && data.subcontractors.length > 0) {
|
||||
data.subcontractors.forEach(name => {
|
||||
let opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.innerText = name;
|
||||
subconSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
console.log("✓ Filters loaded successfully");
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("❌ Error loading filters:", err);
|
||||
});
|
||||
}
|
||||
// 3. Main function to load data and reflect in UI
|
||||
function loadDashboardData() {
|
||||
const tableType = document.getElementById('filter-table').value;
|
||||
const subcon = document.getElementById('filter-subcon').value;
|
||||
const ra = document.getElementById('filter-ra').value;
|
||||
|
||||
// Update Bar Chart
|
||||
liveBarChart.data.datasets[0].data = [
|
||||
data.summary.trench,
|
||||
data.summary.manhole,
|
||||
data.summary.laying
|
||||
];
|
||||
liveBarChart.update();
|
||||
console.log(`📊 Filter values: Table="${tableType}", Subcon="${subcon}", RA="${ra}"`);
|
||||
|
||||
// Update Pie Chart (Location stats)
|
||||
livePieChart.data.labels = Object.keys(data.locations);
|
||||
livePieChart.data.datasets[0].data = Object.values(data.locations);
|
||||
livePieChart.update();
|
||||
})
|
||||
.catch(err => console.error("Error fetching live data:", err));
|
||||
// If still on default values, don't load
|
||||
if (subcon === 'All' && ra === 'Cumulative') {
|
||||
console.warn("⚠️ Please select filters first");
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Check for updates every 10 seconds (Real-time effect)
|
||||
setInterval(fetchLiveData, 10000);
|
||||
fetchLiveData(); // Load immediately on page open
|
||||
</script>
|
||||
// Update chart title
|
||||
const tableNames = {
|
||||
'trench': 'Trench Excavation',
|
||||
'manhole': 'Manhole Excavation',
|
||||
'laying': 'Laying'
|
||||
};
|
||||
const chartTitle = document.getElementById('chart-title');
|
||||
if (chartTitle) {
|
||||
chartTitle.textContent = `${tableNames[tableType]}: Client (RA Bill) vs Subcontractor Qty`;
|
||||
}
|
||||
|
||||
{% endblock %}
|
||||
console.log(`📊 Loading dashboard data: Table="${tableType}", Subcon="${subcon}", RA="${ra}"`);
|
||||
|
||||
const url = `/dashboard/api/excavation-abstract?table_type=${encodeURIComponent(tableType)}&subcontractor=${encodeURIComponent(subcon)}&ra_bill=${encodeURIComponent(ra)}`;
|
||||
console.log(`Fetching from URL: ${url}`);
|
||||
|
||||
fetch(url)
|
||||
.then(res => {
|
||||
console.log(`Response status: ${res.status}`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP Error: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log("✓ Dashboard data received:", data);
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
console.error("❌ Response is not an array:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
console.warn("⚠️ No data returned for this filter combination");
|
||||
alert("No data found for selected filters");
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = [];
|
||||
const clientData = [];
|
||||
const subconData = [];
|
||||
const tableBody = document.querySelector("#abstract-table tbody");
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
let tClient = 0, tSub = 0, tDiff = 0;
|
||||
|
||||
data.forEach(item => {
|
||||
// Label format: "Soil Type Depth"
|
||||
const label = `${item.soil_type}\n${item.depth}`;
|
||||
labels.push(label);
|
||||
clientData.push(item.client_qty || 0);
|
||||
subconData.push(item.subcon_qty || 0);
|
||||
|
||||
tClient += item.client_qty || 0;
|
||||
tSub += item.subcon_qty || 0;
|
||||
tDiff += (item.difference || 0);
|
||||
|
||||
const diffColor = (item.difference || 0) < 0 ? 'text-danger' : 'text-success';
|
||||
tableBody.innerHTML += `
|
||||
<tr>
|
||||
<td class="small">
|
||||
<strong>${item.soil_type}</strong>
|
||||
<br>
|
||||
<span class="text-muted small">${item.depth}</span>
|
||||
</td>
|
||||
<td class="small text-primary fw-bold">${(item.client_qty || 0).toLocaleString('en-IN', {maximumFractionDigits: 2})}</td>
|
||||
<td class="small text-success fw-bold">${(item.subcon_qty || 0).toLocaleString('en-IN', {maximumFractionDigits: 2})}</td>
|
||||
<td class="small fw-bold ${diffColor}">${(item.difference || 0).toLocaleString('en-IN', {maximumFractionDigits: 2})}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
const totalDiffColor = tDiff < 0 ? 'text-danger' : 'text-success';
|
||||
document.getElementById('table-totals').innerHTML = `
|
||||
<td class="small fw-bold">TOTAL</td>
|
||||
<td class="small fw-bold text-primary">${tClient.toLocaleString('en-IN', {maximumFractionDigits: 2})}</td>
|
||||
<td class="small fw-bold text-success">${tSub.toLocaleString('en-IN', {maximumFractionDigits: 2})}</td>
|
||||
<td class="small fw-bold ${totalDiffColor}">${tDiff.toLocaleString('en-IN', {maximumFractionDigits: 2})}</td>
|
||||
`;
|
||||
|
||||
// Show data area
|
||||
document.getElementById('empty-state').style.display = 'none';
|
||||
document.getElementById('data-area').style.display = 'block';
|
||||
|
||||
updateChartUI(labels, clientData, subconData);
|
||||
console.log("✓ Chart and table updated successfully");
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("❌ Error loading dashboard data:", err);
|
||||
alert(`Failed to load dashboard data: ${err.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Clear dashboard
|
||||
function clearDashboard() {
|
||||
console.log("🗑️ Clearing dashboard...");
|
||||
document.getElementById('filter-table').value = 'trench';
|
||||
document.getElementById('filter-subcon').value = 'All';
|
||||
document.getElementById('filter-ra').value = 'Cumulative';
|
||||
document.getElementById('empty-state').style.display = 'block';
|
||||
document.getElementById('data-area').style.display = 'none';
|
||||
if (comparisonChart) comparisonChart.destroy();
|
||||
}
|
||||
|
||||
// Start: Load filters only, don't auto-load data
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("🚀 Dashboard initialized");
|
||||
loadFilters();
|
||||
// Don't auto-load data - keep dashboard blank until filters selected
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user