/** * QUIXZOOM Zoomer API (Simple - no dependencies) * * Endpoints för Zoomer-hantering: * - CRUD för Zoomers * - Onboarding progress * - Quality scores * - Mission assignment * - Regional coordinators */ const http = require('http'); const url = require('url'); // Mock database const zoomers = new Map(); const missions = new Map(); // Demo data const demoZoomer = { id: 'zoomer_bangkok_001', name: 'Somchai', email: 'somchai@example.com', phone: '+66-81-234-5678', location: 'Bangkok, Sukhumvit', level: 4, status: 'trusted', role: 'regional_coordinator', area: 'Sukhumvit', createdAt: '2026-06-15', scores: { overall: 0.84, gps: [3, 4, 3, 5, 4, 3, 4, 5, 3, 4], image: [0.9, 0.85, 0.9, 0.8, 0.9, 0.85, 0.9, 0.9, 0.85, 0.9], corrections: 2, approvedObservations: 45, totalObservations: 50, confirmations: 8, responseTime: [300, 450, 200, 600, 350, 400, 250, 500, 300, 350], completionRate: [1, 1, 1, 1, 0, 1, 1, 1, 1, 1], }, }; zoomers.set(demoZoomer.id, demoZoomer); function calculateQualityScore(zoomer) { const scores = zoomer.scores; const avgGps = scores.gps.length > 0 ? scores.gps.reduce((a, b) => a + b, 0) / scores.gps.length : 999; const gpsScore = Math.max(0, 1 - (avgGps / 10)); const avgImage = scores.image.length > 0 ? scores.image.reduce((a, b) => a + b, 0) / scores.image.length : 0; const correctionRate = scores.totalObservations > 0 ? scores.corrections / scores.totalObservations : 0; const correctionScore = Math.max(0, 1 - correctionRate); const approvalRate = scores.totalObservations > 0 ? scores.approvedObservations / scores.totalObservations : 0; const confirmationScore = Math.min(1, scores.confirmations / 10); const avgResponse = scores.responseTime.length > 0 ? scores.responseTime.reduce((a, b) => a + b, 0) / scores.responseTime.length : 3600; const responseScore = Math.max(0, 1 - (avgResponse / 3600)); const avgCompletion = scores.completionRate.length > 0 ? scores.completionRate.reduce((a, b) => a + b, 0) / scores.completionRate.length : 0; const overall = gpsScore * 0.20 + avgImage * 0.20 + correctionScore * 0.15 + approvalRate * 0.20 + confirmationScore * 0.10 + responseScore * 0.10 + avgCompletion * 0.05; return { overall: Math.round(overall * 100) / 100, breakdown: { gps: Math.round(gpsScore * 100) / 100, image: Math.round(avgImage * 100) / 100, corrections: Math.round(correctionScore * 100) / 100, approval: Math.round(approvalRate * 100) / 100, confirmations: Math.round(confirmationScore * 100) / 100, response: Math.round(responseScore * 100) / 100, completion: Math.round(avgCompletion * 100) / 100, }, }; } const server = http.createServer((req, res) => { const parsedUrl = url.parse(req.url, true); const path = parsedUrl.pathname; const method = req.method; res.setHeader('Content-Type', 'application/json'); res.setHeader('Access-Control-Allow-Origin', '*'); // GET /api/v1/zoomers if (path === '/api/v1/zoomers' && method === 'GET') { const results = Array.from(zoomers.values()); res.writeHead(200); res.end(JSON.stringify({ success: true, count: results.length, zoomers: results, })); return; } // GET /api/v1/zoomers/:id const zoomerMatch = path.match(/^\/api\/v1\/zoomers\/([^\/]+)$/); if (zoomerMatch && method === 'GET') { const zoomer = zoomers.get(zoomerMatch[1]); if (zoomer) { res.writeHead(200); res.end(JSON.stringify({ success: true, zoomer })); } else { res.writeHead(404); res.end(JSON.stringify({ success: false, error: 'Zoomer not found' })); } return; } // GET /api/v1/zoomers/:id/score const scoreMatch = path.match(/^\/api\/v1\/zoomers\/([^\/]+)\/score$/); if (scoreMatch && method === 'GET') { const zoomer = zoomers.get(scoreMatch[1]); if (zoomer) { res.writeHead(200); res.end(JSON.stringify({ success: true, score: calculateQualityScore(zoomer), })); } else { res.writeHead(404); res.end(JSON.stringify({ success: false, error: 'Zoomer not found' })); } return; } // GET /api/v1/stats if (path === '/api/v1/stats' && method === 'GET') { const allZoomers = Array.from(zoomers.values()); const stats = { total: allZoomers.length, byLevel: { 0: allZoomers.filter(z => z.level === 0).length, 1: allZoomers.filter(z => z.level === 1).length, 2: allZoomers.filter(z => z.level === 2).length, 3: allZoomers.filter(z => z.level === 3).length, 4: allZoomers.filter(z => z.level === 4).length, }, byStatus: { onboarding: allZoomers.filter(z => z.status === 'onboarding').length, active: allZoomers.filter(z => z.status === 'active').length, trusted: allZoomers.filter(z => z.status === 'trusted').length, }, coordinators: allZoomers.filter(z => z.role === 'regional_coordinator').length, avgScore: allZoomers.length > 0 ? allZoomers.reduce((sum, z) => sum + calculateQualityScore(z).overall, 0) / allZoomers.length : 0, }; res.writeHead(200); res.end(JSON.stringify({ success: true, stats })); return; } // GET /api/v1/coordinators if (path === '/api/v1/coordinators' && method === 'GET') { const coordinators = Array.from(zoomers.values()) .filter(z => z.role === 'regional_coordinator'); res.writeHead(200); res.end(JSON.stringify({ success: true, count: coordinators.length, coordinators, })); return; } // Health check if (path === '/health' && method === 'GET') { res.writeHead(200); res.end(JSON.stringify({ status: 'ok', service: 'zoomer-api' })); return; } // 404 res.writeHead(404); res.end(JSON.stringify({ success: false, error: 'Not found' })); }); const PORT = 3003; server.listen(PORT, () => { console.log(`╔════════════════════════════════════════════════════════════╗`); console.log(`║ ZOOMER API RUNNING ON PORT ${PORT} ║`); console.log(`╚════════════════════════════════════════════════════════════╝\n`); console.log('Endpoints:'); console.log(' GET /api/v1/zoomers - List all zoomers'); console.log(' GET /api/v1/zoomers/:id - Get zoomer details'); console.log(' GET /api/v1/zoomers/:id/score - Get quality score'); console.log(' GET /api/v1/stats - System stats'); console.log(' GET /api/v1/coordinators - List coordinators'); console.log(' GET /health - Health check'); console.log('\nDemo zoomer: zoomer_bangkok_001'); });