257 lines
8.6 KiB
HTML
257 lines
8.6 KiB
HTML
{% 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 %}
|