Add Node.js/Express backend with PostgreSQL and wire frontend to API
- Server: Express.js with 13 API route files (auth, regulations, contacts, calendar, truck stops, bridges, weigh stations, alerts, load board, escort locator, orders, documents, contributions) - Database: PostgreSQL with Prisma ORM, 15 models covering all modules - Auth: JWT + bcrypt with role-based access control (driver/carrier/escort/admin) - Geospatial: Haversine distance filtering on truck stops, bridges, escorts - Seed script: Imports all existing mock data (51 states, contacts, equipment, truck stops, bridges, weigh stations, alerts, seasonal restrictions) - Frontend: All 10 data-driven pages now fetch from /api instead of mock-data.js - API client (api.js): Compatibility layer that transforms API responses to match existing frontend rendering code, minimizing page-level changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
263
documents.html
263
documents.html
@@ -105,155 +105,158 @@
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="mock-data.js"></script>
|
||||
<script src="mock-data-extended.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="nav.js"></script>
|
||||
<script>
|
||||
renderNav('documents');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────
|
||||
const today = new Date();
|
||||
const MS_PER_DAY = 86400000;
|
||||
(async () => {
|
||||
const MOCK_DOCUMENTS = await PilotEdge.getDocuments();
|
||||
|
||||
function daysUntil(dateStr) {
|
||||
return Math.ceil((new Date(dateStr) - today) / MS_PER_DAY);
|
||||
}
|
||||
// ── Helpers ──────────────────────────────────────────
|
||||
const today = new Date();
|
||||
const MS_PER_DAY = 86400000;
|
||||
|
||||
function fmtDate(dateStr) {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', { year:'numeric', month:'short', day:'numeric' });
|
||||
}
|
||||
|
||||
const typeBadge = {
|
||||
permit: 'bg-blue-100 text-blue-700',
|
||||
insurance: 'bg-green-100 text-green-700',
|
||||
certification: 'bg-purple-100 text-purple-700',
|
||||
registration: 'bg-slate-200 text-slate-700'
|
||||
};
|
||||
|
||||
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
|
||||
|
||||
// ── Stats ───────────────────────────────────────────
|
||||
function renderStats(docs) {
|
||||
const total = docs.length;
|
||||
const active = docs.filter(d => d.status === 'active').length;
|
||||
const expired = docs.filter(d => d.status === 'expired').length;
|
||||
const expiring = docs.filter(d => d.status === 'active' && daysUntil(d.expiryDate) <= 30 && daysUntil(d.expiryDate) > 0).length;
|
||||
|
||||
const items = [
|
||||
{ label:'Total Documents', value:total, color:'bg-slate-900 text-white' },
|
||||
{ label:'Active Permits', value:active, color:'bg-green-600 text-white' },
|
||||
{ label:'Expiring Soon', value:expiring, color:'bg-amber-500 text-white' },
|
||||
{ label:'Expired', value:expired, color:'bg-red-600 text-white' }
|
||||
];
|
||||
|
||||
document.getElementById('stats-bar').innerHTML = items.map(s => `
|
||||
<div class="${s.color} rounded-2xl shadow-lg p-5 text-center">
|
||||
<div class="text-3xl font-bold">${s.value}</div>
|
||||
<div class="text-sm mt-1 opacity-90">${s.label}</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
// ── Expiry Warnings ─────────────────────────────────
|
||||
function renderExpiryWarnings(docs) {
|
||||
const soon = docs.filter(d => d.status === 'active' && daysUntil(d.expiryDate) <= 30 && daysUntil(d.expiryDate) > 0);
|
||||
const el = document.getElementById('expiry-warnings');
|
||||
if (!soon.length) { el.innerHTML = ''; return; }
|
||||
el.innerHTML = `
|
||||
<div class="bg-amber-50 border border-amber-300 rounded-2xl p-5">
|
||||
<h3 class="font-bold text-amber-800 text-lg mb-3">⚠️ Expiring Within 30 Days</h3>
|
||||
<div class="space-y-2">
|
||||
${soon.map(d => `
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between bg-white rounded-xl px-4 py-3 shadow-sm border border-amber-200">
|
||||
<div>
|
||||
<span class="font-semibold text-slate-900">${d.name}</span>
|
||||
<span class="inline-block ml-2 text-xs font-medium px-2 py-0.5 rounded-full ${typeBadge[d.type]}">${capitalize(d.type)}</span>
|
||||
</div>
|
||||
<span class="text-amber-700 font-medium text-sm mt-1 sm:mt-0">Expires ${fmtDate(d.expiryDate)} (${daysUntil(d.expiryDate)} day${daysUntil(d.expiryDate) === 1 ? '' : 's'})</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Document Cards ──────────────────────────────────
|
||||
function renderDocuments(docs) {
|
||||
const list = document.getElementById('document-list');
|
||||
const empty = document.getElementById('empty-state');
|
||||
|
||||
if (!docs.length) {
|
||||
list.innerHTML = '';
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
function daysUntil(dateStr) {
|
||||
return Math.ceil((new Date(dateStr) - today) / MS_PER_DAY);
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
|
||||
list.innerHTML = docs.map(d => {
|
||||
const statusBadge = d.status === 'active'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-red-100 text-red-700';
|
||||
return `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-5 sm:p-6 hover:shadow-xl transition-shadow border border-slate-100">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||||
<h3 class="font-bold text-slate-900 text-base truncate">${d.name}</h3>
|
||||
<span class="inline-block text-xs font-medium px-2.5 py-0.5 rounded-full ${typeBadge[d.type]}">${capitalize(d.type)}</span>
|
||||
<span class="inline-block text-xs font-medium px-2.5 py-0.5 rounded-full ${statusBadge}">${capitalize(d.status)}</span>
|
||||
function fmtDate(dateStr) {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', { year:'numeric', month:'short', day:'numeric' });
|
||||
}
|
||||
|
||||
const typeBadge = {
|
||||
permit: 'bg-blue-100 text-blue-700',
|
||||
insurance: 'bg-green-100 text-green-700',
|
||||
certification: 'bg-purple-100 text-purple-700',
|
||||
registration: 'bg-slate-200 text-slate-700'
|
||||
};
|
||||
|
||||
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
|
||||
|
||||
// ── Stats ───────────────────────────────────────────
|
||||
function renderStats(docs) {
|
||||
const total = docs.length;
|
||||
const active = docs.filter(d => d.status === 'active').length;
|
||||
const expired = docs.filter(d => d.status === 'expired').length;
|
||||
const expiring = docs.filter(d => d.status === 'active' && daysUntil(d.expiryDate) <= 30 && daysUntil(d.expiryDate) > 0).length;
|
||||
|
||||
const items = [
|
||||
{ label:'Total Documents', value:total, color:'bg-slate-900 text-white' },
|
||||
{ label:'Active Permits', value:active, color:'bg-green-600 text-white' },
|
||||
{ label:'Expiring Soon', value:expiring, color:'bg-amber-500 text-white' },
|
||||
{ label:'Expired', value:expired, color:'bg-red-600 text-white' }
|
||||
];
|
||||
|
||||
document.getElementById('stats-bar').innerHTML = items.map(s => `
|
||||
<div class="${s.color} rounded-2xl shadow-lg p-5 text-center">
|
||||
<div class="text-3xl font-bold">${s.value}</div>
|
||||
<div class="text-sm mt-1 opacity-90">${s.label}</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
// ── Expiry Warnings ─────────────────────────────────
|
||||
function renderExpiryWarnings(docs) {
|
||||
const soon = docs.filter(d => d.status === 'active' && daysUntil(d.expiryDate) <= 30 && daysUntil(d.expiryDate) > 0);
|
||||
const el = document.getElementById('expiry-warnings');
|
||||
if (!soon.length) { el.innerHTML = ''; return; }
|
||||
el.innerHTML = `
|
||||
<div class="bg-amber-50 border border-amber-300 rounded-2xl p-5">
|
||||
<h3 class="font-bold text-amber-800 text-lg mb-3">⚠️ Expiring Within 30 Days</h3>
|
||||
<div class="space-y-2">
|
||||
${soon.map(d => `
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between bg-white rounded-xl px-4 py-3 shadow-sm border border-amber-200">
|
||||
<div>
|
||||
<span class="font-semibold text-slate-900">${d.name}</span>
|
||||
<span class="inline-block ml-2 text-xs font-medium px-2 py-0.5 rounded-full ${typeBadge[d.type]}">${capitalize(d.type)}</span>
|
||||
</div>
|
||||
<span class="text-amber-700 font-medium text-sm mt-1 sm:mt-0">Expires ${fmtDate(d.expiryDate)} (${daysUntil(d.expiryDate)} day${daysUntil(d.expiryDate) === 1 ? '' : 's'})</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Document Cards ──────────────────────────────────
|
||||
function renderDocuments(docs) {
|
||||
const list = document.getElementById('document-list');
|
||||
const empty = document.getElementById('empty-state');
|
||||
|
||||
if (!docs.length) {
|
||||
list.innerHTML = '';
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
|
||||
list.innerHTML = docs.map(d => {
|
||||
const statusBadge = d.status === 'active'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-red-100 text-red-700';
|
||||
return `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-5 sm:p-6 hover:shadow-xl transition-shadow border border-slate-100">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||||
<h3 class="font-bold text-slate-900 text-base truncate">${d.name}</h3>
|
||||
<span class="inline-block text-xs font-medium px-2.5 py-0.5 rounded-full ${typeBadge[d.type]}">${capitalize(d.type)}</span>
|
||||
<span class="inline-block text-xs font-medium px-2.5 py-0.5 rounded-full ${statusBadge}">${capitalize(d.status)}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-1 text-sm text-slate-500">
|
||||
${d.state ? `<span>📍 ${d.state}</span>` : ''}
|
||||
<span>📤 Uploaded ${fmtDate(d.uploadDate)}</span>
|
||||
<span>📅 Expires ${fmtDate(d.expiryDate)}</span>
|
||||
<span>💾 ${d.fileSize}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-1 text-sm text-slate-500">
|
||||
${d.state ? `<span>📍 ${d.state}</span>` : ''}
|
||||
<span>📤 Uploaded ${fmtDate(d.uploadDate)}</span>
|
||||
<span>📅 Expires ${fmtDate(d.expiryDate)}</span>
|
||||
<span>💾 ${d.fileSize}</span>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<button onclick="alert('Viewing: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors">View</button>
|
||||
<button onclick="alert('Downloading: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-amber-400 text-amber-700 hover:bg-amber-50 transition-colors">Download</button>
|
||||
<button onclick="alert('Delete requested for: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-red-300 text-red-600 hover:bg-red-50 transition-colors">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<button onclick="alert('Viewing: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors">View</button>
|
||||
<button onclick="alert('Downloading: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-amber-400 text-amber-700 hover:bg-amber-50 transition-colors">Download</button>
|
||||
<button onclick="alert('Delete requested for: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-red-300 text-red-600 hover:bg-red-50 transition-colors">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Filtering ────────────────────────────────────────
|
||||
function applyFilters() {
|
||||
const query = document.getElementById('search-input').value.toLowerCase();
|
||||
const type = document.getElementById('type-filter').value;
|
||||
const status = document.getElementById('status-filter').value;
|
||||
// ── Filtering ────────────────────────────────────────
|
||||
function applyFilters() {
|
||||
const query = document.getElementById('search-input').value.toLowerCase();
|
||||
const type = document.getElementById('type-filter').value;
|
||||
const status = document.getElementById('status-filter').value;
|
||||
|
||||
const filtered = MOCK_DOCUMENTS.filter(d => {
|
||||
if (query && !d.name.toLowerCase().includes(query) && !d.id.toLowerCase().includes(query)) return false;
|
||||
if (type !== 'all' && d.type !== type) return false;
|
||||
if (status !== 'all' && d.status !== status) return false;
|
||||
return true;
|
||||
});
|
||||
renderDocuments(filtered);
|
||||
}
|
||||
const filtered = MOCK_DOCUMENTS.filter(d => {
|
||||
if (query && !d.name.toLowerCase().includes(query) && !d.id.toLowerCase().includes(query)) return false;
|
||||
if (type !== 'all' && d.type !== type) return false;
|
||||
if (status !== 'all' && d.status !== status) return false;
|
||||
return true;
|
||||
});
|
||||
renderDocuments(filtered);
|
||||
}
|
||||
|
||||
document.getElementById('search-input').addEventListener('input', applyFilters);
|
||||
document.getElementById('type-filter').addEventListener('change', applyFilters);
|
||||
document.getElementById('status-filter').addEventListener('change', applyFilters);
|
||||
document.getElementById('search-input').addEventListener('input', applyFilters);
|
||||
document.getElementById('type-filter').addEventListener('change', applyFilters);
|
||||
document.getElementById('status-filter').addEventListener('change', applyFilters);
|
||||
|
||||
// ── Upload Modal ─────────────────────────────────────
|
||||
function openUploadModal() {
|
||||
const modal = document.getElementById('upload-modal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
// ── Upload Modal ─────────────────────────────────────
|
||||
function openUploadModal() {
|
||||
const modal = document.getElementById('upload-modal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeUploadModal() {
|
||||
const modal = document.getElementById('upload-modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
function closeUploadModal() {
|
||||
const modal = document.getElementById('upload-modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
|
||||
// ── Initial Render ───────────────────────────────────
|
||||
renderStats(MOCK_DOCUMENTS);
|
||||
renderExpiryWarnings(MOCK_DOCUMENTS);
|
||||
renderDocuments(MOCK_DOCUMENTS);
|
||||
// ── Initial Render ───────────────────────────────────
|
||||
renderStats(MOCK_DOCUMENTS);
|
||||
renderExpiryWarnings(MOCK_DOCUMENTS);
|
||||
renderDocuments(MOCK_DOCUMENTS);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user