Initial commit - Fire alarm management application
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
256
app/templates/materials.html
Normal file
256
app/templates/materials.html
Normal file
@@ -0,0 +1,256 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Materials - Fire Alarm Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1><i class="bi bi-box-seam"></i> Materials Tracking</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">Total Items</h5>
|
||||
<h2 id="totalItems">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-warning text-dark">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Pending Order</h5>
|
||||
<h2 id="pendingOrder">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Ordered</h5>
|
||||
<h2 id="ordered">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Received</h5>
|
||||
<h2 id="received">-</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Search part number...">
|
||||
</div>
|
||||
<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="statusFilter" class="form-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="pending">Pending Order</option>
|
||||
<option value="ordered">Ordered</option>
|
||||
<option value="received">Received</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Materials 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>Part #</th>
|
||||
<th>Qty Needed</th>
|
||||
<th>Ordered</th>
|
||||
<th>Received</th>
|
||||
<th>Received Qty</th>
|
||||
<th>Received By</th>
|
||||
<th>Delivered Qty</th>
|
||||
<th>Delivered To</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="materialsTable">
|
||||
<tr>
|
||||
<td colspan="10" class="text-center">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let allMaterials = [];
|
||||
let allJobs = [];
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
// Load 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 all materials
|
||||
allMaterials = [];
|
||||
for (const job of allJobs) {
|
||||
const materialsResponse = await fetch(`/api/jobs/${job.id}/materials`);
|
||||
const materials = await materialsResponse.json();
|
||||
materials.forEach(m => {
|
||||
m.job = job;
|
||||
allMaterials.push(m);
|
||||
});
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
renderMaterials(allMaterials);
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
document.getElementById('totalItems').textContent = allMaterials.length;
|
||||
document.getElementById('pendingOrder').textContent = allMaterials.filter(m => !m.ordered).length;
|
||||
document.getElementById('ordered').textContent = allMaterials.filter(m => m.ordered && !m.received).length;
|
||||
document.getElementById('received').textContent = allMaterials.filter(m => m.received).length;
|
||||
}
|
||||
|
||||
function renderMaterials(materials) {
|
||||
const tbody = document.getElementById('materialsTable');
|
||||
|
||||
if (materials.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="text-center">No materials found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = materials.map(m => `
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/jobs/${m.job.id}">${m.job.job_number}</a>
|
||||
<br><small class="text-muted">${m.job.job_name}</small>
|
||||
</td>
|
||||
<td><strong>${m.part_number}</strong></td>
|
||||
<td>${m.quantity}</td>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" ${m.ordered ? 'checked' : ''}
|
||||
onchange="updateMaterial(${m.id}, 'ordered', this.checked)">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" ${m.received ? 'checked' : ''}
|
||||
onchange="updateMaterial(${m.id}, 'received', this.checked)">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" style="width: 80px;"
|
||||
value="${m.received_qty || 0}"
|
||||
onchange="updateMaterial(${m.id}, 'received_qty', parseInt(this.value))">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm" style="width: 100px;"
|
||||
value="${m.received_by || ''}"
|
||||
onchange="updateMaterial(${m.id}, 'received_by', this.value)">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" style="width: 80px;"
|
||||
value="${m.delivered_qty || 0}"
|
||||
onchange="updateMaterial(${m.id}, 'delivered_qty', parseInt(this.value))">
|
||||
</td>
|
||||
<td>${m.delivered_to || '-'}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteMaterial(${m.id})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function updateMaterial(materialId, field, value) {
|
||||
try {
|
||||
await fetch(`/api/materials/${materialId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ [field]: value })
|
||||
});
|
||||
|
||||
// Update local data
|
||||
const material = allMaterials.find(m => m.id === materialId);
|
||||
if (material) {
|
||||
material[field] = value;
|
||||
}
|
||||
updateSummary();
|
||||
} catch (error) {
|
||||
console.error('Error updating material:', error);
|
||||
alert('Error updating material');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMaterial(materialId) {
|
||||
if (!confirm('Delete this material?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/materials/${materialId}`, { method: 'DELETE' });
|
||||
allMaterials = allMaterials.filter(m => m.id !== materialId);
|
||||
updateSummary();
|
||||
filterMaterials();
|
||||
} catch (error) {
|
||||
console.error('Error deleting material:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function filterMaterials() {
|
||||
const search = document.getElementById('searchInput').value.toLowerCase();
|
||||
const jobId = document.getElementById('jobFilter').value;
|
||||
const status = document.getElementById('statusFilter').value;
|
||||
|
||||
let filtered = allMaterials.filter(m => {
|
||||
const matchSearch = !search || (m.part_number && m.part_number.toLowerCase().includes(search));
|
||||
const matchJob = !jobId || m.job.id == jobId;
|
||||
const matchStatus = !status ||
|
||||
(status === 'pending' && !m.ordered) ||
|
||||
(status === 'ordered' && m.ordered && !m.received) ||
|
||||
(status === 'received' && m.received);
|
||||
|
||||
return matchSearch && matchJob && matchStatus;
|
||||
});
|
||||
|
||||
renderMaterials(filtered);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadData();
|
||||
|
||||
document.getElementById('searchInput').addEventListener('input', filterMaterials);
|
||||
document.getElementById('jobFilter').addEventListener('change', filterMaterials);
|
||||
document.getElementById('statusFilter').addEventListener('change', filterMaterials);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user