Files
boc/quixzoom-capture-pipeline/zoomer-onboarding/admin-panel.html
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

527 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QUIXZOOM Zoomer Admin</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
color: #333;
}
.header {
background: #1a1a2e;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 { font-size: 1.5rem; }
.stats-bar {
display: flex;
gap: 2rem;
padding: 1rem 2rem;
background: white;
border-bottom: 1px solid #ddd;
}
.stat {
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #1a1a2e;
}
.stat-label {
font-size: 0.875rem;
color: #666;
}
.container {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.filter-btn {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.filter-btn.active {
background: #1a1a2e;
color: white;
}
.zoomer-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1rem;
}
.zoomer-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.zoomer-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 1rem;
}
.zoomer-name {
font-size: 1.25rem;
font-weight: bold;
}
.zoomer-level {
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
}
.level-1 { background: #e3f2fd; color: #1976d2; }
.level-2 { background: #fff3e0; color: #f57c00; }
.level-3 { background: #e8f5e9; color: #388e3c; }
.level-4 { background: #f3e5f5; color: #7b1fa2; }
.zoomer-meta {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
font-size: 0.875rem;
color: #666;
}
.score-bar {
margin-bottom: 1rem;
}
.score-label {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.25rem;
}
.score-track {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s;
}
.score-good { background: #4caf50; }
.score-medium { background: #ff9800; }
.score-bad { background: #f44336; }
.actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
}
.btn-primary {
background: #1a1a2e;
color: white;
}
.btn-secondary {
background: #f5f5f5;
color: #333;
}
.btn-success {
background: #4caf50;
color: white;
}
.detail-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
justify-content: center;
align-items: center;
z-index: 100;
}
.detail-modal.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 2rem;
max-width: 800px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.detail-section h3 {
margin-bottom: 0.5rem;
font-size: 1rem;
}
.detail-section table {
width: 100%;
border-collapse: collapse;
}
.detail-section td {
padding: 0.5rem;
border-bottom: 1px solid #eee;
}
.detail-section td:first-child {
color: #666;
}
</style>
</head>
<body>
<div class="header">
<h1>QUIXZOOM Zoomer Admin</h1>
<div>v1.1</div>
</div>
<div class="stats-bar">
<div class="stat">
<div class="stat-value" id="total-zoomers">0</div>
<div class="stat-label">Total Zoomers</div>
</div>
<div class="stat">
<div class="stat-value" id="active-zoomers">0</div>
<div class="stat-label">Active</div>
</div>
<div class="stat">
<div class="stat-value" id="trusted-zoomers">0</div>
<div class="stat-label">Trusted</div>
</div>
<div class="stat">
<div class="stat-value" id="coordinators">0</div>
<div class="stat-label">Coordinators</div>
</div>
<div class="stat">
<div class="stat-value" id="avg-score">0</div>
<div class="stat-label">Avg Quality</div>
</div>
</div>
<div class="container">
<div class="filters">
<button class="filter-btn active" data-filter="all">Alla</button>
<button class="filter-btn" data-filter="onboarding">Onboarding</button>
<button class="filter-btn" data-filter="active">Active</button>
<button class="filter-btn" data-filter="trusted">Trusted</button>
<button class="filter-btn" data-filter="coordinator">Coordinators</button>
</div>
<div class="zoomer-grid" id="zoomer-grid"></div>
</div>
<div class="detail-modal" id="detail-modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-name">Zoomer Details</h2>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="detail-grid" id="detail-grid"></div>
</div>
</div>
<script>
// Mock data - replace with API call
const zoomers = [
{
id: 'zoomer_001',
name: 'Somchai',
email: 'somchai@example.com',
phone: '+66-81-234-5678',
location: 'Bangkok, Sukhumvit',
level: 4,
status: 'trusted',
role: 'regional_coordinator',
area: 'Sukhumvit',
joined: '2026-06-15',
lastActive: '2026-06-28',
observations: 234,
missions: 45,
score: {
overall: 0.84,
gps: 0.62,
image: 0.88,
corrections: 0.96,
approval: 0.90,
confirmations: 0.80,
response: 0.90,
completion: 0.90
}
},
{
id: 'zoomer_002',
name: 'Siriwan',
email: 'siriwan@example.com',
phone: '+66-82-345-6789',
location: 'Bangkok, Silom',
level: 3,
status: 'active',
role: 'zoomer',
joined: '2026-06-20',
lastActive: '2026-06-27',
observations: 89,
missions: 12,
score: {
overall: 0.72,
gps: 0.55,
image: 0.75,
corrections: 0.85,
approval: 0.80,
confirmations: 0.60,
response: 0.85,
completion: 0.75
}
},
{
id: 'zoomer_003',
name: 'Prasert',
email: 'prasert@example.com',
phone: '+66-83-456-7890',
location: 'Bangkok, Siam',
level: 2,
status: 'onboarding',
role: 'zoomer',
joined: '2026-06-25',
lastActive: '2026-06-26',
observations: 15,
missions: 1,
score: {
overall: 0.45,
gps: 0.30,
image: 0.50,
corrections: 0.70,
approval: 0.60,
confirmations: 0.20,
response: 0.50,
completion: 0.40
}
}
];
function getScoreColor(score) {
if (score >= 0.8) return 'score-good';
if (score >= 0.6) return 'score-medium';
return 'score-bad';
}
function getLevelClass(level) {
return `level-${level}`;
}
function getLevelName(level) {
const names = ['New', 'Capture Basics', 'Quality Test', 'Guided', 'Trusted'];
return names[level] || 'Unknown';
}
function renderZoomerCard(zoomer) {
return `
<div class="zoomer-card" data-status="${zoomer.status}" data-level="${zoomer.level}">
<div class="zoomer-header">
<div>
<div class="zoomer-name">${zoomer.name}</div>
<div class="zoomer-meta">
<span>${zoomer.location}</span>
<span>${zoomer.observations} obs</span>
</div>
</div>
<span class="zoomer-level ${getLevelClass(zoomer.level)}">
${getLevelName(zoomer.level)}
</span>
</div>
<div class="score-bar">
<div class="score-label">
<span>Quality Score</span>
<span>${(zoomer.score.overall * 100).toFixed(0)}%</span>
</div>
<div class="score-track">
<div class="score-fill ${getScoreColor(zoomer.score.overall)}"
style="width: ${zoomer.score.overall * 100}%"></div>
</div>
</div>
<div class="score-bar">
<div class="score-label">
<span>GPS Accuracy</span>
<span>${(zoomer.score.gps * 100).toFixed(0)}%</span>
</div>
<div class="score-track">
<div class="score-fill ${getScoreColor(zoomer.score.gps)}"
style="width: ${zoomer.score.gps * 100}%"></div>
</div>
</div>
<div class="score-bar">
<div class="score-label">
<span>Image Quality</span>
<span>${(zoomer.score.image * 100).toFixed(0)}%</span>
</div>
<div class="score-track">
<div class="score-fill ${getScoreColor(zoomer.score.image)}"
style="width: ${zoomer.score.image * 100}%"></div>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" onclick="showDetail('${zoomer.id}')">
Details
</button>
${zoomer.level >= 3 ? `
<button class="btn btn-success" onclick="promote('${zoomer.id}')">
${zoomer.level === 4 ? 'Coordinator' : 'Promote'}
</button>
` : ''}
<button class="btn btn-secondary" onclick="message('${zoomer.id}')">
Message
</button>
</div>
</div>
`;
}
function renderGrid(filter = 'all') {
const grid = document.getElementById('zoomer-grid');
const filtered = filter === 'all'
? zoomers
: zoomers.filter(z => {
if (filter === 'coordinator') return z.role === 'regional_coordinator';
return z.status === filter;
});
grid.innerHTML = filtered.map(renderZoomerCard).join('');
}
function updateStats() {
document.getElementById('total-zoomers').textContent = zoomers.length;
document.getElementById('active-zoomers').textContent = zoomers.filter(z => z.status === 'active').length;
document.getElementById('trusted-zoomers').textContent = zoomers.filter(z => z.status === 'trusted').length;
document.getElementById('coordinators').textContent = zoomers.filter(z => z.role === 'regional_coordinator').length;
const avgScore = zoomers.reduce((sum, z) => sum + z.score.overall, 0) / zoomers.length;
document.getElementById('avg-score').textContent = (avgScore * 100).toFixed(0) + '%';
}
function showDetail(zoomerId) {
const zoomer = zoomers.find(z => z.id === zoomerId);
if (!zoomer) return;
document.getElementById('modal-name').textContent = zoomer.name;
const detailGrid = document.getElementById('detail-grid');
detailGrid.innerHTML = `
<div class="detail-section">
<h3>Profile</h3>
<table>
<tr><td>ID</td><td>${zoomer.id}</td></tr>
<tr><td>Email</td><td>${zoomer.email}</td></tr>
<tr><td>Phone</td><td>${zoomer.phone}</td></tr>
<tr><td>Location</td><td>${zoomer.location}</td></tr>
<tr><td>Joined</td><td>${zoomer.joined}</td></tr>
<tr><td>Last Active</td><td>${zoomer.lastActive}</td></tr>
</table>
</div>
<div class="detail-section">
<h3>Performance</h3>
<table>
<tr><td>Observations</td><td>${zoomer.observations}</td></tr>
<tr><td>Missions</td><td>${zoomer.missions}</td></tr>
<tr><td>Quality Score</td><td>${(zoomer.score.overall * 100).toFixed(1)}%</td></tr>
<tr><td>GPS Score</td><td>${(zoomer.score.gps * 100).toFixed(1)}%</td></tr>
<tr><td>Image Score</td><td>${(zoomer.score.image * 100).toFixed(1)}%</td></tr>
<tr><td>Approval Rate</td><td>${(zoomer.score.approval * 100).toFixed(1)}%</td></tr>
</table>
</div>
<div class="detail-section">
<h3>Detailed Scores</h3>
<table>
<tr><td>GPS</td><td>${(zoomer.score.gps * 100).toFixed(1)}%</td></tr>
<tr><td>Image</td><td>${(zoomer.score.image * 100).toFixed(1)}%</td></tr>
<tr><td>Corrections</td><td>${(zoomer.score.corrections * 100).toFixed(1)}%</td></tr>
<tr><td>Approval</td><td>${(zoomer.score.approval * 100).toFixed(1)}%</td></tr>
<tr><td>Confirmations</td><td>${(zoomer.score.confirmations * 100).toFixed(1)}%</td></tr>
<tr><td>Response</td><td>${(zoomer.score.response * 100).toFixed(1)}%</td></tr>
<tr><td>Completion</td><td>${(zoomer.score.completion * 100).toFixed(1)}%</td></tr>
</table>
</div>
<div class="detail-section">
<h3>Actions</h3>
<div class="actions">
<button class="btn btn-primary" onclick="editZoomer('${zoomer.id}')">Edit</button>
<button class="btn btn-secondary" onclick="resetProgress('${zoomer.id}')">Reset Progress</button>
<button class="btn btn-secondary" style="background:#f44336;color:white" onclick="deactivate('${zoomer.id}')">Deactivate</button>
</div>
</div>
`;
document.getElementById('detail-modal').classList.add('active');
}
function closeModal() {
document.getElementById('detail-modal').classList.remove('active');
}
function promote(zoomerId) {
const zoomer = zoomers.find(z => z.id === zoomerId);
if (zoomer && zoomer.level === 3) {
zoomer.level = 4;
zoomer.status = 'trusted';
renderGrid();
updateStats();
}
}
function message(zoomerId) {
alert(`Messaging ${zoomerId} - feature coming soon`);
}
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderGrid(btn.dataset.filter);
});
});
// Initialize
renderGrid();
updateStats();
</script>
</body>
</html>