Feature: Browser-Mockup interaktiv — Dashboard/Protokoll/Stationen klickbar mit Live-Daten

This commit is contained in:
rene 2026-05-17 17:01:22 +02:00
parent be22dde08f
commit b2d97efe3f

View file

@ -10,9 +10,11 @@
const STATUS_LABEL: Record<string, string> = { ok: 'OK', abweichung: 'Abweichung', kritisch: 'Kritisch' };
const STATUS_CLASS: Record<string, string> = { ok: 'ok', abweichung: 'warn', kritisch: 'crit' };
let activeView = $state<'dashboard' | 'protokoll' | 'stationen'>('dashboard');
let liveLogs = $state<any[]>([]);
let liveStats = $state({ total: 0, ok: 0, warn: 0, crit: 0 });
let lastUpdate = $state('');
let liveStations = $state<any[]>([]);
async function fetchLiveData() {
try {
@ -20,19 +22,26 @@
today.setHours(0, 0, 0, 0);
const dateStr = today.toISOString().slice(0, 19).replace('T', ' ');
const result = await pb.collection('check_logs').getList(1, 3, {
const [logsResult, stationsResult] = await Promise.all([
pb.collection('check_logs').getList(1, 6, {
filter: `tenant = '${DEMO_TENANT}' && created >= '${dateStr}'`,
expand: 'station',
sort: '-created'
});
}),
pb.collection('stations').getFullList({
filter: `tenant = '${DEMO_TENANT}' && active = true`,
sort: 'name'
})
]);
liveLogs = result.items;
liveStats.total = result.totalItems;
liveStats.ok = result.items.filter((l: any) => l.status === 'ok').length;
liveStats.warn = result.items.filter((l: any) => l.status === 'abweichung').length;
liveStats.crit = result.items.filter((l: any) => l.status === 'kritisch').length;
liveLogs = logsResult.items;
liveStats.total = logsResult.totalItems;
liveStats.ok = logsResult.items.filter((l: any) => l.status === 'ok').length;
liveStats.warn = logsResult.items.filter((l: any) => l.status === 'abweichung').length;
liveStats.crit = logsResult.items.filter((l: any) => l.status === 'kritisch').length;
lastUpdate = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
} catch { /* public endpoint, sollte immer klappen */ }
liveStations = stationsResult;
} catch { }
}
onMount(() => {
@ -124,10 +133,13 @@
<div class="browser-screen">
<div class="dash-sidebar">
<img src={hafnerLogo} alt="Hafner Prüftechnik" class="dash-brand-logo" />
<div class="dash-nav-item active schmidt">Dashboard</div>
<div class="dash-nav-item">Protokoll</div>
<div class="dash-nav-item">Stationen</div>
<button class="dash-nav-item {activeView === 'dashboard' ? 'active schmidt' : ''}" onclick={() => activeView = 'dashboard'}>Dashboard</button>
<button class="dash-nav-item {activeView === 'protokoll' ? 'active schmidt' : ''}" onclick={() => activeView = 'protokoll'}>Protokoll</button>
<button class="dash-nav-item {activeView === 'stationen' ? 'active schmidt' : ''}" onclick={() => activeView = 'stationen'}>Stationen</button>
</div>
<!-- DASHBOARD -->
{#if activeView === 'dashboard'}
<div class="dash-main">
<div class="dash-title">Dashboard</div>
<div class="dash-date">
@ -135,25 +147,13 @@
{#if lastUpdate}<span class="dash-refresh">· {lastUpdate}</span>{/if}
</div>
<div class="dash-stats">
<div class="dash-stat">
<div class="dash-num">{liveStats.total}</div>
<div class="dash-label">Heute</div>
</div>
<div class="dash-stat ok">
<div class="dash-num">{liveStats.ok}</div>
<div class="dash-label">OK</div>
</div>
<div class="dash-stat warn">
<div class="dash-num">{liveStats.warn}</div>
<div class="dash-label">Abw.</div>
</div>
<div class="dash-stat crit">
<div class="dash-num">{liveStats.crit}</div>
<div class="dash-label">Kritisch</div>
</div>
<div class="dash-stat"><div class="dash-num">{liveStats.total}</div><div class="dash-label">Heute</div></div>
<div class="dash-stat ok"><div class="dash-num">{liveStats.ok}</div><div class="dash-label">OK</div></div>
<div class="dash-stat warn"><div class="dash-num">{liveStats.warn}</div><div class="dash-label">Abw.</div></div>
<div class="dash-stat crit"><div class="dash-num">{liveStats.crit}</div><div class="dash-label">Kritisch</div></div>
</div>
<div class="dash-entries-title">Letzte Einträge</div>
{#each liveLogs as log}
{#each liveLogs.slice(0,4) as log}
<div class="dash-entry">
<span class="entry-badge {STATUS_CLASS[log.status] ?? 'ok'}">{STATUS_LABEL[log.status] ?? log.status}</span>
<span class="entry-station">{log.expand?.station?.name ?? '—'}</span>
@ -164,6 +164,51 @@
<div class="dash-empty">Noch keine Einträge heute</div>
{/each}
</div>
{/if}
<!-- PROTOKOLL -->
{#if activeView === 'protokoll'}
<div class="dash-main">
<div class="dash-title">Protokoll</div>
<div class="dash-date">{new Date().toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })}</div>
<table class="proto-table">
<thead><tr><th>Station</th><th>Status</th><th>Temp.</th><th>Zeit</th></tr></thead>
<tbody>
{#each liveLogs as log}
<tr>
<td class="proto-station">{log.expand?.station?.name ?? '—'}</td>
<td><span class="entry-badge {STATUS_CLASS[log.status] ?? 'ok'}">{STATUS_LABEL[log.status]}</span></td>
<td>{log.temperature ? log.temperature + '°' : '—'}</td>
<td class="proto-time">{formatTime(log.created)}</td>
</tr>
{:else}
<tr><td colspan="4" class="dash-empty">Keine Einträge</td></tr>
{/each}
</tbody>
</table>
</div>
{/if}
<!-- STATIONEN -->
{#if activeView === 'stationen'}
<div class="dash-main">
<div class="dash-title">Stationen</div>
<div class="dash-date">{liveStations.length} aktive Stationen</div>
<div class="stations-grid">
{#each liveStations as s}
<div class="station-card">
<div class="station-card-icon"></div>
<div class="station-card-name">{s.name}</div>
{#if s.target_temp_min || s.target_temp_max}
<div class="station-card-temp">{s.target_temp_min}° {s.target_temp_max}°C</div>
{/if}
</div>
{:else}
<div class="dash-empty">Keine Stationen</div>
{/each}
</div>
</div>
{/if}
</div>
</div>
@ -414,11 +459,15 @@
border-radius: 5px;
font-size: 0.65rem;
color: #6b7a9a;
background: none;
border: none;
cursor: pointer;
text-align: left;
width: 100%;
transition: all 0.15s;
}
.dash-nav-item.active {
background: rgba(249,115,22,0.15);
color: #F97316;
}
.dash-nav-item:hover { color: #fff; background: rgba(255,255,255,0.06); }
.dash-nav-item.active { background: rgba(249,115,22,0.15); color: #F97316; }
.dash-main {
flex: 1;
@ -450,6 +499,20 @@
.dash-refresh { font-size: 0.55rem; color: #aaa; }
.dash-entries-title { font-size: 0.7rem; font-weight: 700; color: #0B1023; margin-bottom: 0.4rem; }
.dash-empty { font-size: 0.65rem; color: #aaa; padding: 0.5rem 0; }
/* Protokoll-Tabelle */
.proto-table { width: 100%; border-collapse: collapse; font-size: 0.6rem; margin-top: 0.5rem; }
.proto-table th { background: #F5F7FA; padding: 0.3rem 0.4rem; text-align: left; color: #888; font-size: 0.55rem; text-transform: uppercase; }
.proto-table td { padding: 0.35rem 0.4rem; border-bottom: 1px solid #f0f0f0; }
.proto-station { font-weight: 600; color: #0B1023; max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.proto-time { color: #aaa; }
/* Stationen-Grid */
.stations-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; margin-top: 0.5rem; }
.station-card { background: #F5F7FA; border-radius: 6px; padding: 0.5rem; }
.station-card-icon { font-size: 1rem; margin-bottom: 0.2rem; }
.station-card-name { font-size: 0.6rem; font-weight: 700; color: #0B1023; }
.station-card-temp { font-size: 0.55rem; color: #888; margin-top: 0.1rem; }
.dash-entry {
background: #fff;
border-radius: 6px;