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