Initial commit - Fire alarm management application
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
350
app/templates/dashboard.html
Normal file
350
app/templates/dashboard.html
Normal file
@@ -0,0 +1,350 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user