/** * pc-live | Parkuhr Dashboard * Integration with qb-parkuhr (data via qb-parkuhr:getPanelData callback) */ const ParkuhrApp = (() => { const WIN_ID = 'app-parkuhr'; let _data = null; // { devices[], typeTariffs{}, currency } let _selectedDevice = null; // currently shown device or null = overview const TYPE_META = { machine: { label: 'Automat', icon: '🏧', color: 'var(--accent)' }, meter_small: { label: 'Parkuhr (S)', icon: 'πŸ…Ώ', color: 'var(--success)' }, meter_big: { label: 'Parkuhr (L)', icon: 'πŸ…Ώ', color: 'var(--warning)' }, }; /* ── Format helpers ──────────────────────────────────────── */ function fmtMoney(val, cur = '$') { const n = parseFloat(val) || 0; return `${cur}${n.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } function fmtDuration(min) { if (min >= 10080) return `${min / 10080} W`; if (min >= 1440) return `${min / 1440} T`; if (min >= 60) return `${min / 60} Std.`; return `${min} Min.`; } function fmtPos(pos) { if (!pos) return 'β€”'; return `${Math.round(pos.x)}, ${Math.round(pos.y)}`; } function fmtTime(ts) { if (!ts) return 'β€”'; try { return new Date(ts).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } catch { return String(ts); } } /* ── KPI card ────────────────────────────────────────────── */ function kpiCard(icon, label, value, color = 'var(--text-muted)') { return `
${icon} ${label}
${value}
`; } /* ── Overview page ───────────────────────────────────────── */ function renderOverview() { if (!_data) { return `
⏳

Daten werden geladen…

`; } const devices = _data.devices || []; const cur = _data.currency || '$'; const totalRevenue = devices.reduce((s, d) => s + (parseFloat(d.revenue) || 0), 0); const totalSales = devices.reduce((s, d) => s + (d.sales?.length || 0), 0); const machineCount = devices.filter(d => d.type === 'machine').length; const meterCount = devices.filter(d => d.type !== 'machine').length; // Merge & sort all sales across devices, show last 12 const allSales = []; for (const dev of devices) { for (const s of (dev.sales || []).slice(-8)) { allSales.push({ ...s, devId: dev.id, devType: dev.type }); } } allSales.sort((a, b) => new Date(b.ts) - new Date(a.ts)); const recent = allSales.slice(0, 12); return `
${kpiCard('πŸ’°', 'Gesamteinnahmen', fmtMoney(totalRevenue, cur), 'var(--accent)')} ${kpiCard('πŸ“‹', 'Transaktionen', totalSales)} ${kpiCard('🏧', 'Automaten', machineCount)} ${kpiCard('πŸ…Ώ', 'Parkuhren', meterCount)}
${devices.length > 0 ? `
${devices.map(d => { const meta = TYPE_META[d.type] || { icon: 'πŸ“', label: d.type, color: 'var(--text-muted)' }; const rev = parseFloat(d.revenue) || 0; const pct = totalRevenue > 0 ? Math.round((rev / totalRevenue) * 100) : 0; return `
${meta.icon} #${d.id}
${fmtMoney(rev, cur)}
`; }).join('')}
` : ''}
${recent.length === 0 ? `

Keine Transaktionen vorhanden

` : `
${recent.map(s => { const meta = TYPE_META[s.devType] || { icon: 'πŸ“' }; return `
${fmtTime(s.ts)} ${meta.icon} #${s.devId} ${s.label || 'β€”'} ${s.price != null ? fmtMoney(s.price, cur) : 'β€”'}
`; }).join('')}
` }
`; } /* ── Device detail page ──────────────────────────────────── */ function renderDevice(device) { const cur = _data?.currency || '$'; const meta = TYPE_META[device.type] || { icon: 'πŸ“', label: device.type, color: 'var(--accent)' }; const sales = (device.sales || []).slice(-20).reverse(); const isMach = device.type === 'machine'; // Tariff list: device-specific for machines, type-wide for meters const tariffList = isMach ? (device.tariffs || []) : (_data?.typeTariffs?.[device.type] || []); return `
${meta.icon}
GerΓ€t #${device.id}
${meta.label}  Β·  ${device.model || 'β€”'}
${fmtMoney(device.revenue, cur)}
Kassenstand
πŸ“ ${fmtPos(device.pos)} πŸ‘€ ${device.placed_by || 'β€”'} πŸ“‹ ${device.sales?.length || 0} VerkΓ€ufe (gespeichert)
${tariffList.length === 0 ? `Keine Tarife definiert` : tariffList.map(t => { const dur = t.duration ?? t.duration_min; return `
${fmtDuration(dur)}
${fmtMoney(t.price, cur)}
${isMach ? ` ` : ''}
`; }).join('')}
${isMach ? `
` : ''}
${sales.length === 0 ? `

Keine EintrΓ€ge

` : `
${sales.map(s => `
${fmtTime(s.ts)} ${s.label || 'β€”'} ${s.price != null ? fmtMoney(s.price, cur) : 'β€”'}
`).join('')}
`}
`; } /* ── Full window render ──────────────────────────────────── */ function _refresh() { const body = WindowManager.getBody(WIN_ID); if (!body) return; const devices = _data?.devices || []; const cur = _data?.currency || '$'; body.innerHTML = `
GerΓ€te (${devices.length})
${devices.length === 0 ? `
Keine GerΓ€te
` : devices.map(d => { const m = TYPE_META[d.type] || { icon: 'πŸ“', label: d.type }; const sel = _selectedDevice?.id === d.id; return ` `; }).join('')}
${_selectedDevice === null ? renderOverview() : renderDevice(_selectedDevice)}
`; } /* ── Public actions ──────────────────────────────────────── */ function _select(deviceId) { _selectedDevice = deviceId === null ? null : (_data?.devices || []).find(d => d.id === deviceId) || null; _refresh(); } function _saveTariff(deviceId) { const dur = parseInt(document.getElementById('tariff-dur')?.value); const price = parseFloat(document.getElementById('tariff-price')?.value); if (isNaN(dur) || isNaN(price) || price < 0) return; fetchNui('saveParkuhrTariff', { deviceId, duration: dur, price }).then(_reload); } function _deleteTariff(deviceId, duration) { fetchNui('deleteParkuhrTariff', { deviceId, duration }).then(_reload); } function _reload() { const body = WindowManager.getBody(WIN_ID); if (!body) return; body.innerHTML = `
⏳

Aktualisiere…

`; fetchNui('getParkuhrPanel', {}).then(data => { _data = data || { devices: [], typeTariffs: {}, currency: '$' }; // Re-select from fresh data if (_selectedDevice) { _selectedDevice = (_data.devices || []).find(d => d.id === _selectedDevice.id) || null; } _refresh(); }); } /* ── Open ────────────────────────────────────────────────── */ function open() { const created = WindowManager.create({ id: WIN_ID, title: 'Parkuhr Dashboard', icon: 'πŸ…Ώ', width: 860, height: 540, content: '', }); if (!created) return; _data = null; _selectedDevice = null; _refresh(); fetchNui('getParkuhrPanel', {}).then(data => { _data = data || { devices: [], typeTariffs: {}, currency: '$' }; _refresh(); }); } return { open, _select, _saveTariff, _deleteTariff, _reload }; })();