Files
Fire-alarm-managment/app/templates/dashboard.html
2026-01-19 21:57:25 -05:00

351 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}Dashboard - Fire Alarm Management{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col">
<h1><i class="bi bi-speedometer2"></i> Dashboard</h1>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-briefcase"></i> Total Jobs</h5>
<h2 class="card-text" id="totalJobs">-</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-currency-dollar"></i> Total Budget</h5>
<h2 class="card-text" id="totalBudget">-</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-percent"></i> Avg Completion</h5>
<h2 class="card-text" id="avgCompletion">-</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-dark">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-tools"></i> Labor + Material</h5>
<h2 class="card-text" id="laborMaterial">-</h2>
</div>
</div>
</div>
</div>
<!-- Charts Row 1 -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-pie-chart"></i> Jobs by Status
</div>
<div class="card-body">
<canvas id="statusChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-bar-chart"></i> Budget by Vendor
</div>
<div class="card-body">
<canvas id="vendorChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Charts Row 2 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<i class="bi bi-graph-up"></i> Job Completion Progress
</div>
<div class="card-body">
<canvas id="completionChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Charts Row 3 -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-person"></i> Jobs by Project Manager
</div>
<div class="card-body">
<canvas id="pmChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-cash-stack"></i> Budget vs Estimates
</div>
<div class="card-body">
<canvas id="budgetChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Recent Jobs Table -->
<div class="row">
<div class="col">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-clock-history"></i> Recent Jobs</span>
<a href="{{ url_for('main.jobs_list') }}" class="btn btn-sm btn-primary">View All</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Job #</th>
<th>Name</th>
<th>PM</th>
<th>Budget</th>
<th>Progress</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="recentJobsTable">
<tr>
<td colspan="6" class="text-center">Loading...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let statusChart, vendorChart, completionChart, pmChart, budgetChart;
async function loadDashboard() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
// Update summary cards
document.getElementById('totalJobs').textContent = data.total_jobs;
document.getElementById('totalBudget').textContent = formatCurrency(data.total_budget);
document.getElementById('avgCompletion').textContent = data.avg_completion.toFixed(1) + '%';
document.getElementById('laborMaterial').textContent = formatCurrency(data.total_labor + data.total_material);
// Status Pie Chart
const statusCtx = document.getElementById('statusChart').getContext('2d');
if (statusChart) statusChart.destroy();
statusChart = new Chart(statusCtx, {
type: 'doughnut',
data: {
labels: ['Completed', 'In Progress', 'Not Started'],
datasets: [{
data: [data.completed_jobs, data.in_progress_jobs, data.not_started_jobs],
backgroundColor: ['#198754', '#0d6efd', '#6c757d']
}]
},
options: {
responsive: true,
plugins: {
legend: { position: 'bottom' }
}
}
});
// Vendor Budget Bar Chart
const vendorCtx = document.getElementById('vendorChart').getContext('2d');
if (vendorChart) vendorChart.destroy();
vendorChart = new Chart(vendorCtx, {
type: 'bar',
data: {
labels: Object.keys(data.vendor_budgets),
datasets: [{
label: 'Budget ($)',
data: Object.values(data.vendor_budgets),
backgroundColor: '#0d6efd'
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: value => '$' + value.toLocaleString()
}
}
}
}
});
// Completion Progress Bar Chart
const completionCtx = document.getElementById('completionChart').getContext('2d');
if (completionChart) completionChart.destroy();
completionChart = new Chart(completionCtx, {
type: 'bar',
data: {
labels: data.jobs_completion.map(j => j.name),
datasets: [{
label: 'Completion %',
data: data.jobs_completion.map(j => j.completion),
backgroundColor: data.jobs_completion.map(j => {
if (j.completion >= 100) return '#198754';
if (j.completion > 50) return '#0d6efd';
if (j.completion > 0) return '#ffc107';
return '#6c757d';
})
}]
},
options: {
indexAxis: 'y',
responsive: true,
plugins: {
legend: { display: false }
},
scales: {
x: {
beginAtZero: true,
max: 100,
ticks: {
callback: value => value + '%'
}
}
}
}
});
// PM Jobs Chart
const pmCtx = document.getElementById('pmChart').getContext('2d');
if (pmChart) pmChart.destroy();
pmChart = new Chart(pmCtx, {
type: 'bar',
data: {
labels: Object.keys(data.pm_jobs),
datasets: [{
label: 'Number of Jobs',
data: Object.values(data.pm_jobs).map(p => p.count),
backgroundColor: '#198754'
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1 }
}
}
}
});
// Budget vs Estimates Chart
const budgetCtx = document.getElementById('budgetChart').getContext('2d');
if (budgetChart) budgetChart.destroy();
budgetChart = new Chart(budgetCtx, {
type: 'doughnut',
data: {
labels: ['Fire Alarm Budget', 'Labor Estimate', 'Material Estimate'],
datasets: [{
data: [data.total_budget, data.total_labor, data.total_material],
backgroundColor: ['#0d6efd', '#198754', '#ffc107']
}]
},
options: {
responsive: true,
plugins: {
legend: { position: 'bottom' },
tooltip: {
callbacks: {
label: function(context) {
return context.label + ': $' + context.raw.toLocaleString();
}
}
}
}
}
});
// Load recent jobs table
loadRecentJobs(data.jobs_completion);
} catch (error) {
console.error('Error loading dashboard:', error);
}
}
function loadRecentJobs(jobs) {
const tbody = document.getElementById('recentJobsTable');
if (jobs.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center">No jobs found. <a href="/jobs/new">Add your first job</a></td></tr>';
return;
}
tbody.innerHTML = jobs.slice(0, 10).map(job => `
<tr>
<td>${job.job_number || '-'}</td>
<td>${job.name}</td>
<td>${job.pm_assigned || '-'}</td>
<td>${formatCurrency(job.budget)}</td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar ${getProgressClass(job.completion)}" role="progressbar"
style="width: ${job.completion}%;" aria-valuenow="${job.completion}"
aria-valuemin="0" aria-valuemax="100">
${job.completion.toFixed(0)}%
</div>
</div>
</td>
<td>
<a href="/jobs/${job.id}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
`).join('');
}
function formatCurrency(value) {
return '$' + (value || 0).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0});
}
function getProgressClass(completion) {
if (completion >= 100) return 'bg-success';
if (completion > 50) return 'bg-info';
if (completion > 0) return 'bg-warning';
return 'bg-secondary';
}
document.addEventListener('DOMContentLoaded', loadDashboard);
</script>
{% endblock %}