357 lines
19 KiB
HTML
357 lines
19 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Dashboard - Delphi Database{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="space-y-6">
|
|
<!-- Page Header -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-xl">
|
|
<i class="fa-solid fa-gauge-high text-lg"></i>
|
|
</div>
|
|
<h1 class="text-2xl font-bold text-neutral-900 dark:text-neutral-100">Dashboard</h1>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button onclick="openShortcutsModal()" class="bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors duration-200 flex items-center gap-2">
|
|
<i class="fa-solid fa-keyboard"></i>
|
|
<span>Shortcuts (F1)</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Stats Cards -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div class="bg-primary-600 text-white rounded-xl shadow-soft p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 bg-primary-700 rounded-lg">
|
|
<i class="fa-solid fa-users text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<h5 class="text-sm font-medium mb-1">Customers</h5>
|
|
<h2 class="text-2xl font-bold mb-0" id="customer-count">-</h2>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3 mt-2">
|
|
<a href="/customers" class="text-primary-200 hover:text-white text-xs font-medium flex items-center gap-1 transition-colors">
|
|
View all
|
|
<i class="fa-solid fa-arrow-right"></i>
|
|
</a>
|
|
<a href="/customers#phone-dir"
|
|
class="inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors"
|
|
title="Download Phone Directory">
|
|
<i class="fa-solid fa-address-book"></i>
|
|
Phone Directory
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-success-600 text-white rounded-xl shadow-soft p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 bg-success-700 rounded-lg">
|
|
<i class="fa-solid fa-folder text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<h5 class="text-sm font-medium mb-1">Active Files</h5>
|
|
<h2 class="text-2xl font-bold mb-0" id="file-count">-</h2>
|
|
</div>
|
|
</div>
|
|
<a href="/files" class="text-success-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
|
|
View all
|
|
<i class="fa-solid fa-arrow-right"></i>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="bg-info-600 text-white rounded-xl shadow-soft p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 bg-info-700 rounded-lg">
|
|
<i class="fa-solid fa-receipt text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<h5 class="text-sm font-medium mb-1">Transactions</h5>
|
|
<h2 class="text-2xl font-bold mb-0" id="transaction-count">-</h2>
|
|
</div>
|
|
</div>
|
|
<a href="/financial" class="text-info-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
|
|
View ledger
|
|
<i class="fa-solid fa-arrow-right"></i>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="bg-warning-600 text-white rounded-xl shadow-soft p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 bg-warning-700 rounded-lg">
|
|
<i class="fa-regular fa-file-lines text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<h5 class="text-sm font-medium mb-1">Documents</h5>
|
|
<h2 class="text-2xl font-bold mb-0" id="document-count">-</h2>
|
|
</div>
|
|
</div>
|
|
<a href="/documents" class="text-warning-200 hover:text-white text-xs font-medium flex items-center gap-1 mt-2 transition-colors">
|
|
View all
|
|
<i class="fa-solid fa-arrow-right"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions and Recent Activity -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="md:col-span-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
|
|
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
|
<i class="fa-solid fa-bolt"></i>
|
|
<span>Quick Actions</span>
|
|
</h5>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div class="space-y-3">
|
|
<button onclick="newCustomer()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
|
|
<i class="fa-solid fa-user-plus text-2xl text-primary-600 mb-1"></i>
|
|
<span class="font-medium">New Customer</span>
|
|
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+Shift+C</kbd>
|
|
</button>
|
|
<button onclick="newFile()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
|
|
<i class="fa-solid fa-folder-plus text-2xl text-success-600 mb-1"></i>
|
|
<span class="font-medium">New File</span>
|
|
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+Shift+F</kbd>
|
|
</button>
|
|
</div>
|
|
<div class="space-y-3">
|
|
<button onclick="newTransaction()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
|
|
<i class="fa-solid fa-circle-plus text-2xl text-info-600 mb-1"></i>
|
|
<span class="font-medium">New Transaction</span>
|
|
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+Shift+T</kbd>
|
|
</button>
|
|
<button onclick="globalSearch()" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
|
|
<i class="fa-solid fa-magnifying-glass text-2xl text-warning-600 mb-1"></i>
|
|
<span class="font-medium">Global Search</span>
|
|
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Ctrl+F</kbd>
|
|
</button>
|
|
<button onclick="window.location.href='/import'" class="w-full flex flex-col items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-900/50 hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 transition-colors duration-200">
|
|
<i class="fa-solid fa-cloud-arrow-up text-2xl text-primary-600 mb-1"></i>
|
|
<span class="font-medium">Import Data</span>
|
|
<kbd class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Alt+I</kbd>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
|
|
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
|
<i class="fa-solid fa-clock-rotate-left"></i>
|
|
<span>Recent Activity & Imports</span>
|
|
</h5>
|
|
</div>
|
|
<div class="p-6 space-y-4">
|
|
<div id="recent-imports">
|
|
<div class="flex flex-col items-center justify-center py-4 text-neutral-500 dark:text-neutral-400">
|
|
<i class="fa-solid fa-file-arrow-up text-2xl mb-2"></i>
|
|
<p>Loading recent imports...</p>
|
|
</div>
|
|
</div>
|
|
<div id="recent-activity" class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-300">
|
|
<i class="fa-regular fa-bell"></i>
|
|
<span>Live document events</span>
|
|
</div>
|
|
<div class="flex items-center gap-2 text-xs">
|
|
<span class="text-neutral-500">Connection:</span>
|
|
<span id="adminDocConnBadge"></span>
|
|
<button id="adminDocReconnectBtn" type="button" class="px-2 py-1 rounded bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 text-neutral-700 dark:text-neutral-300 border border-neutral-300 dark:border-neutral-600">Reconnect</button>
|
|
</div>
|
|
</div>
|
|
<div id="adminDocEvents" class="space-y-2" aria-live="polite"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Status -->
|
|
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
|
|
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
|
<i class="fa-solid fa-circle-info"></i>
|
|
<span>System Information</span>
|
|
</h5>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="space-y-3 text-sm">
|
|
<p class="flex justify-between">
|
|
<strong>System:</strong>
|
|
<span>Delphi Consulting Group Database System</span>
|
|
</p>
|
|
<p class="flex justify-between">
|
|
<strong>Version:</strong>
|
|
<span>1.0.0</span>
|
|
</p>
|
|
<p class="flex justify-between">
|
|
<strong>Database:</strong>
|
|
<span>SQLite</span>
|
|
</p>
|
|
</div>
|
|
<div class="space-y-3 text-sm">
|
|
<p class="flex justify-between">
|
|
<strong>Last Backup:</strong>
|
|
<span id="last-backup">Not available</span>
|
|
</p>
|
|
<p class="flex justify-between">
|
|
<strong>Database Size:</strong>
|
|
<span id="db-size">-</span>
|
|
</p>
|
|
<p class="flex justify-between">
|
|
<strong>Status:</strong>
|
|
<span id="system-status" class="px-2 py-1 bg-success-600 text-white text-xs font-medium rounded-full">Healthy</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
// Load dashboard data
|
|
async function loadDashboardData() {
|
|
try {
|
|
const response = await window.http.wrappedFetch('/api/admin/stats');
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
document.getElementById('customer-count').textContent = data.total_customers || '0';
|
|
document.getElementById('file-count').textContent = data.total_files || '0';
|
|
document.getElementById('transaction-count').textContent = data.total_transactions || '0';
|
|
document.getElementById('document-count').textContent = data.total_qdros || '0';
|
|
document.getElementById('db-size').textContent = data.database_size || '-';
|
|
document.getElementById('last-backup').textContent = data.last_backup || 'Not available';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading dashboard data:', error);
|
|
}
|
|
}
|
|
|
|
// Quick action functions
|
|
function newCustomer() {
|
|
window.location.href = '/customers';
|
|
}
|
|
|
|
function newFile() {
|
|
window.location.href = '/files';
|
|
}
|
|
|
|
function newTransaction() {
|
|
window.location.href = '/financial';
|
|
}
|
|
|
|
function globalSearch() {
|
|
window.location.href = '/search';
|
|
}
|
|
|
|
// Load data on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadDashboardData(); // Uncomment when authentication is implemented
|
|
loadRecentImports();
|
|
loadRecentActivity();
|
|
try { setupAdminNotificationCenter(); } catch (_) {}
|
|
});
|
|
async function loadRecentActivity() {
|
|
// Placeholder: existing system would populate; if an endpoint exists, hook it here.
|
|
}
|
|
|
|
function setupAdminNotificationCenter() {
|
|
const host = document.getElementById('adminDocConnBadge');
|
|
const feed = document.getElementById('adminDocEvents');
|
|
const btn = document.getElementById('adminDocReconnectBtn');
|
|
if (!host || !feed || !window.notifications) return;
|
|
|
|
const badge = window.notifications.createConnectionBadge();
|
|
host.innerHTML = '';
|
|
host.appendChild(badge.element);
|
|
|
|
const mgr = window.notifications.connectAdminDocumentStream({
|
|
onEvent: (payload) => {
|
|
window.notifications.appendEvent(feed, payload);
|
|
},
|
|
onState: (s) => badge.update(s)
|
|
});
|
|
|
|
if (btn) btn.addEventListener('click', () => { try { mgr.reconnectNow(); } catch(_) {} });
|
|
}
|
|
|
|
async function loadRecentImports() {
|
|
try {
|
|
const [statusResp, recentResp] = await Promise.all([
|
|
window.http.wrappedFetch('/api/import/status'),
|
|
window.http.wrappedFetch('/api/import/recent-batches?limit=5')
|
|
]);
|
|
if (!statusResp.ok) return;
|
|
const status = await statusResp.json();
|
|
const recent = recentResp && recentResp.ok ? (await recentResp.json()).recent || [] : [];
|
|
const entries = Object.entries(status || {});
|
|
const total = entries.reduce((sum, [, v]) => sum + (v && v.record_count ? v.record_count : 0), 0);
|
|
const top = entries
|
|
.filter(([, v]) => (v && v.record_count) > 0)
|
|
.slice(0, 6)
|
|
.map(([k, v]) => ({ name: k, count: v.record_count, table: v.table_name }));
|
|
|
|
const container = document.getElementById('recent-imports');
|
|
if (!container) return;
|
|
if (entries.length === 0 && recent.length === 0) {
|
|
container.innerHTML = '<p class="text-neutral-500">No import status available.</p>';
|
|
return;
|
|
}
|
|
const items = top.map(({ name, count }) => `
|
|
<div class="flex items-center justify-between py-1 text-sm">
|
|
<span class="font-mono">${name}</span>
|
|
<span class="inline-block px-2 py-0.5 rounded bg-neutral-100 dark:bg-neutral-700">${Number(count).toLocaleString()}</span>
|
|
</div>
|
|
`).join('');
|
|
const recentRows = (recent || []).map(r => `
|
|
<tr>
|
|
<td class="px-2 py-1"><span class="inline-block px-2 py-0.5 rounded text-xs ${r.status === 'success' ? 'bg-green-100 text-green-700' : (r.status === 'completed_with_errors' ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700')}">${r.status}</span></td>
|
|
<td class="px-2 py-1 text-xs">${r.started_at ? new Date(r.started_at).toLocaleString() : ''}</td>
|
|
<td class="px-2 py-1 text-xs">${r.finished_at ? new Date(r.finished_at).toLocaleString() : ''}</td>
|
|
<td class="px-2 py-1 text-xs">${r.successful_files}/${r.total_files}</td>
|
|
<td class="px-2 py-1 text-xs">${Number(r.total_imported || 0).toLocaleString()}</td>
|
|
</tr>
|
|
`).join('');
|
|
const html = `
|
|
<div>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h6 class="text-sm font-semibold flex items-center gap-2"><i class="fa-solid fa-file-arrow-up"></i> Recent Import Status</h6>
|
|
<a href="/import" class="text-primary-600 hover:underline text-sm">Open Import</a>
|
|
</div>
|
|
<div class="border border-neutral-200 dark:border-neutral-700 rounded-lg p-3">${items || '<p class="text-neutral-500 text-sm">No imported data yet.</p>'}</div>
|
|
<div class="mt-2 text-xs text-neutral-600 dark:text-neutral-400">Total records across tracked CSVs: <strong>${Number(total).toLocaleString()}</strong></div>
|
|
<div class="mt-3">
|
|
<h6 class="text-sm font-semibold mb-1">Last 5 Batch Uploads</h6>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-xs border border-neutral-200 dark:border-neutral-700 rounded">
|
|
<thead class="bg-neutral-50 dark:bg-neutral-800">
|
|
<tr>
|
|
<th class="px-2 py-1 text-left">Status</th>
|
|
<th class="px-2 py-1 text-left">Started</th>
|
|
<th class="px-2 py-1 text-left">Finished</th>
|
|
<th class="px-2 py-1 text-left">Files</th>
|
|
<th class="px-2 py-1 text-left">Imported</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${recentRows || '<tr><td class="px-2 py-2 text-neutral-500" colspan="5">No recent batch uploads</td></tr>'}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.innerHTML = html;
|
|
} catch (_) {}
|
|
}
|
|
</script>
|
|
{% endblock %} |