/**
* 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
? `
`
: `
${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 = `
${_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 = ``;
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 };
})();