Files
PilotEdge/documents.html
Daniel Kovalevich f917fb8014 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>
2026-03-30 15:43:27 -04:00

263 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Vault | PilotEdge</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-50 min-h-screen flex flex-col">
<div id="main-nav"></div>
<div id="poc-banner"></div>
<!-- Page Header -->
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
<div class="max-w-7xl mx-auto">
<h1 class="text-3xl md:text-4xl font-bold mb-3">Document Vault</h1>
<p class="text-lg text-gray-400 max-w-3xl">Securely store and manage your permits, insurance certificates, and professional certifications — all in one place.</p>
</div>
</section>
<!-- Main Content -->
<main class="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10">
<!-- Stats Bar -->
<div id="stats-bar" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"></div>
<!-- Expiry Warnings -->
<div id="expiry-warnings" class="mb-8"></div>
<!-- Filter Bar -->
<div class="bg-white rounded-2xl shadow-lg p-4 sm:p-6 mb-8">
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
<input id="search-input" type="text" placeholder="Search documents…"
class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none transition-colors">
</div>
<select id="type-filter"
class="border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none transition-colors">
<option value="all">All Types</option>
<option value="permit">Permit</option>
<option value="insurance">Insurance</option>
<option value="certification">Certification</option>
<option value="registration">Registration</option>
</select>
<select id="status-filter"
class="border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none transition-colors">
<option value="all">All Statuses</option>
<option value="active">Active</option>
<option value="expired">Expired</option>
</select>
<button onclick="openUploadModal()"
class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-5 py-2.5 rounded-lg transition-colors shadow-md hover:shadow-lg text-sm whitespace-nowrap">
+ Upload Document
</button>
</div>
</div>
<!-- Document List -->
<div id="document-list" class="grid gap-4"></div>
<!-- Empty State -->
<div id="empty-state" class="hidden text-center py-16">
<p class="text-5xl mb-4">📄</p>
<p class="text-slate-500 text-lg">No documents match your filters.</p>
</div>
</main>
<!-- Upload Modal -->
<div id="upload-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50 p-4">
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 relative">
<button onclick="closeUploadModal()" class="absolute top-4 right-4 text-slate-400 hover:text-slate-600 text-xl leading-none">&times;</button>
<h2 class="text-xl font-bold text-slate-900 mb-1">Upload Document</h2>
<p class="text-sm text-amber-600 mb-5">⚠️ POC demo — file upload not functional.</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Document Type</label>
<select id="upload-type"
class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none">
<option value="permit">Permit</option>
<option value="insurance">Insurance</option>
<option value="certification">Certification</option>
<option value="registration">Registration</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Document Name</label>
<input id="upload-name" type="text" placeholder="e.g. TX Single Trip Permit"
class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Choose File</label>
<div class="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center text-slate-400 text-sm">
Drag &amp; drop or click to browse<br>
<span class="text-xs">(disabled in POC)</span>
</div>
</div>
<button onclick="alert('POC demo — upload not functional.'); closeUploadModal();"
class="w-full bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-2.5 rounded-lg transition-colors shadow-md text-sm">
Upload
</button>
</div>
</div>
</div>
<div id="main-footer"></div>
<script src="api.js"></script>
<script src="nav.js"></script>
<script>
renderNav('documents');
renderBanner();
renderFooter();
(async () => {
const MOCK_DOCUMENTS = await PilotEdge.getDocuments();
// ── Helpers ──────────────────────────────────────────
const today = new Date();
const MS_PER_DAY = 86400000;
function daysUntil(dateStr) {
return Math.ceil((new Date(dateStr) - today) / MS_PER_DAY);
}
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 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('');
}
// ── 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);
}
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');
}
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);
})();
</script>
</body>
</html>