260 lines
13 KiB
HTML
260 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">×</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 & 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="mock-data.js"></script>
|
|
<script src="mock-data-extended.js"></script>
|
|
<script src="nav.js"></script>
|
|
<script>
|
|
renderNav('documents');
|
|
renderBanner();
|
|
renderFooter();
|
|
|
|
// ── 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>
|