238 lines
8.2 KiB
HTML
238 lines
8.2 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Jobs - Fire Alarm Management{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h1><i class="bi bi-list-task"></i> Jobs</h1>
|
|
</div>
|
|
<div class="col-auto">
|
|
<a href="{{ url_for('main.job_new') }}" class="btn btn-primary">
|
|
<i class="bi bi-plus-lg"></i> New Job
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<input type="text" id="searchInput" class="form-control" placeholder="Search jobs...">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select id="statusFilter" class="form-select">
|
|
<option value="">All Status</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="in_progress">In Progress</option>
|
|
<option value="not_started">Not Started</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select id="vendorFilter" class="form-select">
|
|
<option value="">All Vendors</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select id="pmFilter" class="form-select">
|
|
<option value="">All PMs</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Jobs Table -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Job #</th>
|
|
<th>Name</th>
|
|
<th>Location</th>
|
|
<th>PM</th>
|
|
<th>Vendor</th>
|
|
<th>Budget</th>
|
|
<th>Progress</th>
|
|
<th>Units</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="jobsTable">
|
|
<tr>
|
|
<td colspan="9" class="text-center">Loading...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Confirm Delete</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
Are you sure you want to delete this job?
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-danger" id="confirmDelete">Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let allJobs = [];
|
|
let deleteJobId = null;
|
|
|
|
async function loadJobs() {
|
|
try {
|
|
const response = await fetch('/api/jobs');
|
|
allJobs = await response.json();
|
|
populateFilters();
|
|
renderJobs(allJobs);
|
|
} catch (error) {
|
|
console.error('Error loading jobs:', error);
|
|
document.getElementById('jobsTable').innerHTML =
|
|
'<tr><td colspan="9" class="text-center text-danger">Error loading jobs</td></tr>';
|
|
}
|
|
}
|
|
|
|
function populateFilters() {
|
|
const vendors = [...new Set(allJobs.map(j => j.fire_vendor).filter(v => v))];
|
|
const pms = [...new Set(allJobs.map(j => j.pm_assigned).filter(p => p))];
|
|
|
|
const vendorSelect = document.getElementById('vendorFilter');
|
|
vendors.forEach(v => {
|
|
const option = document.createElement('option');
|
|
option.value = v;
|
|
option.textContent = v;
|
|
vendorSelect.appendChild(option);
|
|
});
|
|
|
|
const pmSelect = document.getElementById('pmFilter');
|
|
pms.forEach(p => {
|
|
const option = document.createElement('option');
|
|
option.value = p;
|
|
option.textContent = p;
|
|
pmSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
function renderJobs(jobs) {
|
|
const tbody = document.getElementById('jobsTable');
|
|
|
|
if (jobs.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="9" class="text-center">No jobs found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = jobs.map(job => {
|
|
const completion = (job.percent_complete || 0) * 100;
|
|
return `
|
|
<tr>
|
|
<td><strong>${job.job_number || '-'}</strong></td>
|
|
<td>${job.job_name}</td>
|
|
<td>${job.location || '-'}</td>
|
|
<td>${job.pm_assigned || '-'}</td>
|
|
<td>${job.fire_vendor || '-'}</td>
|
|
<td>$${(job.fire_alarm_budget || 0).toLocaleString()}</td>
|
|
<td style="min-width: 150px;">
|
|
<div class="progress" style="height: 20px;">
|
|
<div class="progress-bar ${getProgressClass(completion)}" role="progressbar"
|
|
style="width: ${completion}%;" aria-valuenow="${completion}"
|
|
aria-valuemin="0" aria-valuemax="100">
|
|
${completion.toFixed(0)}%
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${job.number_of_units || '-'}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="/jobs/${job.id}" class="btn btn-outline-primary" title="View">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
<a href="/jobs/${job.id}/edit" class="btn btn-outline-secondary" title="Edit">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<button class="btn btn-outline-danger" title="Delete" onclick="showDeleteModal(${job.id})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function getProgressClass(completion) {
|
|
if (completion >= 100) return 'bg-success';
|
|
if (completion > 50) return 'bg-info';
|
|
if (completion > 0) return 'bg-warning';
|
|
return 'bg-secondary';
|
|
}
|
|
|
|
function filterJobs() {
|
|
const search = document.getElementById('searchInput').value.toLowerCase();
|
|
const status = document.getElementById('statusFilter').value;
|
|
const vendor = document.getElementById('vendorFilter').value;
|
|
const pm = document.getElementById('pmFilter').value;
|
|
|
|
let filtered = allJobs.filter(job => {
|
|
const matchSearch = !search ||
|
|
(job.job_number && job.job_number.toLowerCase().includes(search)) ||
|
|
(job.job_name && job.job_name.toLowerCase().includes(search)) ||
|
|
(job.location && job.location.toLowerCase().includes(search));
|
|
|
|
const completion = (job.percent_complete || 0);
|
|
const matchStatus = !status ||
|
|
(status === 'completed' && completion >= 1) ||
|
|
(status === 'in_progress' && completion > 0 && completion < 1) ||
|
|
(status === 'not_started' && completion === 0);
|
|
|
|
const matchVendor = !vendor || job.fire_vendor === vendor;
|
|
const matchPM = !pm || job.pm_assigned === pm;
|
|
|
|
return matchSearch && matchStatus && matchVendor && matchPM;
|
|
});
|
|
|
|
renderJobs(filtered);
|
|
}
|
|
|
|
function showDeleteModal(jobId) {
|
|
deleteJobId = jobId;
|
|
new bootstrap.Modal(document.getElementById('deleteModal')).show();
|
|
}
|
|
|
|
async function deleteJob() {
|
|
if (!deleteJobId) return;
|
|
|
|
try {
|
|
await fetch(`/api/jobs/${deleteJobId}`, { method: 'DELETE' });
|
|
bootstrap.Modal.getInstance(document.getElementById('deleteModal')).hide();
|
|
loadJobs();
|
|
} catch (error) {
|
|
console.error('Error deleting job:', error);
|
|
alert('Error deleting job');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadJobs();
|
|
|
|
document.getElementById('searchInput').addEventListener('input', filterJobs);
|
|
document.getElementById('statusFilter').addEventListener('change', filterJobs);
|
|
document.getElementById('vendorFilter').addEventListener('change', filterJobs);
|
|
document.getElementById('pmFilter').addEventListener('change', filterJobs);
|
|
document.getElementById('confirmDelete').addEventListener('click', deleteJob);
|
|
});
|
|
</script>
|
|
{% endblock %}
|