2025-12-11 10:16:43 +05:30
|
|
|
{% extends "base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
2026-02-02 12:27:27 +05:30
|
|
|
<div class="container-fluid py-4" style="background-color: #f8f9fa;">
|
|
|
|
|
<h3 class="mb-4 fw-bold text-uppercase">Abstract Excavation Dashboard</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>
|
2026-01-13 15:22:53 +05:30
|
|
|
</div>
|
2026-02-02 12:27:27 +05:30
|
|
|
<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>
|
2026-01-13 15:22:53 +05:30
|
|
|
</div>
|
2026-02-02 12:27:27 +05:30
|
|
|
<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>
|
2026-01-13 15:22:53 +05:30
|
|
|
</div>
|
2026-01-13 13:21:06 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-02 12:27:27 +05:30
|
|
|
<!-- 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>
|
2026-01-13 15:22:53 +05:30
|
|
|
</div>
|
2026-01-13 13:21:06 +05:30
|
|
|
</div>
|
|
|
|
|
|
2026-02-02 12:27:27 +05:30
|
|
|
<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>
|
2026-01-13 15:22:53 +05:30
|
|
|
</div>
|
2026-01-13 13:21:06 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-24 13:41:52 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-13 13:21:06 +05:30
|
|
|
|
2026-01-24 13:41:52 +05:30
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
2026-01-13 15:22:53 +05:30
|
|
|
|
2026-01-24 13:41:52 +05:30
|
|
|
<script>
|
2026-02-02 12:27:27 +05:30
|
|
|
let comparisonChart;
|
2026-01-13 13:21:06 +05:30
|
|
|
|
2026-02-02 12:27:27 +05:30
|
|
|
// 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
|
2026-01-24 13:41:52 +05:30
|
|
|
data: {
|
2026-02-02 12:27:27 +05:30
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
]
|
2026-01-24 13:41:52 +05:30
|
|
|
},
|
2026-02-02 12:27:27 +05:30
|
|
|
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' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-24 13:41:52 +05:30
|
|
|
});
|
2026-02-02 12:27:27 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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>';
|
2026-01-24 13:41:52 +05:30
|
|
|
|
2026-02-02 12:27:27 +05:30
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
console.log(`📊 Filter values: Table="${tableType}", Subcon="${subcon}", RA="${ra}"`);
|
|
|
|
|
|
|
|
|
|
// If still on default values, don't load
|
|
|
|
|
if (subcon === 'All' && ra === 'Cumulative') {
|
|
|
|
|
console.warn("⚠️ Please select filters first");
|
|
|
|
|
return;
|
2026-01-24 13:41:52 +05:30
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:27:27 +05:30
|
|
|
// 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`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2026-01-13 15:22:53 +05:30
|
|
|
|
2026-02-02 12:27:27 +05:30
|
|
|
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 %}
|