183 lines
7.4 KiB
HTML
183 lines
7.4 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Schedule - Fire Alarm Management{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h1><i class="bi bi-calendar3"></i> Schedule Overview</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<select id="jobFilter" class="form-select">
|
|
<option value="">All Jobs</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select id="phaseTypeFilter" class="form-select">
|
|
<option value="">All Phase Types</option>
|
|
<option value="rough_in">Rough-in</option>
|
|
<option value="trim">Trim</option>
|
|
<option value="commissioning">Commissioning</option>
|
|
<option value="final">Final</option>
|
|
<option value="turnover">Turnover</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select id="statusFilter" class="form-select">
|
|
<option value="">All Status</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="in_progress">In Progress</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Schedule Cards by Job -->
|
|
<div id="scheduleContainer" class="row">
|
|
<div class="col text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let allJobs = [];
|
|
let allPhases = {};
|
|
|
|
async function loadSchedule() {
|
|
try {
|
|
// Load all jobs
|
|
const jobsResponse = await fetch('/api/jobs');
|
|
allJobs = await jobsResponse.json();
|
|
|
|
// Populate job filter
|
|
const jobFilter = document.getElementById('jobFilter');
|
|
allJobs.forEach(job => {
|
|
const option = document.createElement('option');
|
|
option.value = job.id;
|
|
option.textContent = `${job.job_number} - ${job.job_name}`;
|
|
jobFilter.appendChild(option);
|
|
});
|
|
|
|
// Load phases for each job
|
|
for (const job of allJobs) {
|
|
const phasesResponse = await fetch(`/api/jobs/${job.id}/phases`);
|
|
allPhases[job.id] = await phasesResponse.json();
|
|
}
|
|
|
|
renderSchedule();
|
|
} catch (error) {
|
|
console.error('Error loading schedule:', error);
|
|
document.getElementById('scheduleContainer').innerHTML =
|
|
'<div class="col"><div class="alert alert-danger">Error loading schedule</div></div>';
|
|
}
|
|
}
|
|
|
|
function renderSchedule() {
|
|
const container = document.getElementById('scheduleContainer');
|
|
const jobFilter = document.getElementById('jobFilter').value;
|
|
const phaseTypeFilter = document.getElementById('phaseTypeFilter').value;
|
|
const statusFilter = document.getElementById('statusFilter').value;
|
|
|
|
let jobs = jobFilter ? allJobs.filter(j => j.id == jobFilter) : allJobs;
|
|
|
|
if (jobs.length === 0) {
|
|
container.innerHTML = '<div class="col"><div class="alert alert-info">No jobs found</div></div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = jobs.map(job => {
|
|
let phases = allPhases[job.id] || [];
|
|
|
|
if (phaseTypeFilter) {
|
|
phases = phases.filter(p => p.phase_type === phaseTypeFilter);
|
|
}
|
|
if (statusFilter) {
|
|
phases = phases.filter(p =>
|
|
(statusFilter === 'completed' && p.completed) ||
|
|
(statusFilter === 'in_progress' && !p.completed)
|
|
);
|
|
}
|
|
|
|
if (phases.length === 0 && (phaseTypeFilter || statusFilter)) {
|
|
return '';
|
|
}
|
|
|
|
const phaseTypes = ['rough_in', 'trim', 'commissioning', 'final', 'turnover'];
|
|
const phaseLabels = {
|
|
'rough_in': 'Rough-in',
|
|
'trim': 'Trim',
|
|
'commissioning': 'Commissioning',
|
|
'final': 'Final',
|
|
'turnover': 'Turnover'
|
|
};
|
|
|
|
return `
|
|
<div class="col-12 mb-4">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span>
|
|
<strong>${job.job_number}</strong> - ${job.job_name}
|
|
<span class="badge bg-info">${(job.percent_complete * 100).toFixed(0)}% Complete</span>
|
|
</span>
|
|
<a href="/jobs/${job.id}" class="btn btn-sm btn-outline-primary">View Job</a>
|
|
</div>
|
|
<div class="card-body">
|
|
${phases.length === 0 ? '<p class="text-muted">No phases scheduled yet</p>' : `
|
|
<div class="row">
|
|
${phaseTypes.map(type => {
|
|
const typePhases = phases.filter(p => p.phase_type === type);
|
|
if (typePhases.length === 0) return '';
|
|
return `
|
|
<div class="col-md-4 mb-3">
|
|
<h6 class="text-uppercase text-muted">${phaseLabels[type]}</h6>
|
|
${typePhases.map(p => `
|
|
<div class="card mb-2 ${p.completed ? 'border-success' : ''}">
|
|
<div class="card-body py-2 px-3">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span>Phase ${p.phase_number}</span>
|
|
<span class="badge ${p.completed ? 'bg-success' : 'bg-secondary'}">
|
|
${p.completed ? 'Done' : 'Pending'}
|
|
</span>
|
|
</div>
|
|
<small class="text-muted">
|
|
${p.points ? p.points + ' pts' : ''}
|
|
${p.start_date ? ' | Start: ' + p.start_date : ''}
|
|
${p.due_date ? ' | Due: ' + p.due_date : ''}
|
|
</small>
|
|
${p.men_on_site ? '<br><small>Men: ' + p.men_on_site + '</small>' : ''}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
`}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
if (!container.innerHTML.trim()) {
|
|
container.innerHTML = '<div class="col"><div class="alert alert-info">No matching phases found</div></div>';
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadSchedule();
|
|
|
|
document.getElementById('jobFilter').addEventListener('change', renderSchedule);
|
|
document.getElementById('phaseTypeFilter').addEventListener('change', renderSchedule);
|
|
document.getElementById('statusFilter').addEventListener('change', renderSchedule);
|
|
});
|
|
</script>
|
|
{% endblock %}
|