Feature: Browser-Mockup interaktiv — Dashboard/Protokoll/Stationen klickbar mit Live-Daten
This commit is contained in:
parent
be22dde08f
commit
b2d97efe3f
1 changed files with 101 additions and 38 deletions
|
|
@ -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 liveLogs = $state<any[]>([]);
|
||||
let liveStats = $state({ total: 0, ok: 0, warn: 0, crit: 0 });
|
||||
let lastUpdate = $state('');
|
||||
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, {
|
||||
filter: `tenant = '${DEMO_TENANT}' && created >= '${dateStr}'`,
|
||||
expand: 'station',
|
||||
sort: '-created'
|
||||
});
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue