351 lines
12 KiB
HTML
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 %}
|