Admin-Bereich, PWA-Manifest und Deploy-Setup
- Admin: Login, Dashboard, Protokoll, Stationen mit QR-Links - PWA: vite-plugin-pwa mit Workbox Offline-Caching - SvelteKit adapter-node + Dockerfile für DS-Deployment - docker-compose.yml mit app + pocketbase Services - Makefile: make deploy Befehl
This commit is contained in:
parent
f2615c9e07
commit
18570a42f0
15 changed files with 6042 additions and 12 deletions
148
app/src/routes/admin/dashboard/+page.svelte
Normal file
148
app/src/routes/admin/dashboard/+page.svelte
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<script lang="ts">
|
||||
import { pb } from '$lib/pb';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
ok: 'OK', abweichung: 'Abweichung', kritisch: 'Kritisch'
|
||||
};
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
ok: '#16a34a', abweichung: '#ea580c', kritisch: '#dc2626'
|
||||
};
|
||||
|
||||
let logs = $state<any[]>([]);
|
||||
let stats = $state({ total: 0, ok: 0, abweichung: 0, kritisch: 0 });
|
||||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const tenantId = (pb.authStore.record as any)?.tenant;
|
||||
|
||||
const result = await pb.collection('check_logs').getList(1, 50, {
|
||||
filter: `tenant = "${tenantId}" && created >= "${today.toISOString()}"`,
|
||||
expand: 'station',
|
||||
sort: '-created'
|
||||
});
|
||||
|
||||
logs = result.items;
|
||||
stats.total = result.totalItems;
|
||||
stats.ok = logs.filter(l => l.status === 'ok').length;
|
||||
stats.abweichung = logs.filter(l => l.status === 'abweichung').length;
|
||||
stats.kritisch = logs.filter(l => l.status === 'kritisch').length;
|
||||
loading = false;
|
||||
});
|
||||
|
||||
function formatTime(iso: string) {
|
||||
return new Date(iso).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Dashboard — checkflo</title></svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<h1>Dashboard</h1>
|
||||
<p class="date">{new Date().toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' })}</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span class="stat-num">{stats.total}</span>
|
||||
<span class="stat-label">Checks heute</span>
|
||||
</div>
|
||||
<div class="stat stat-ok">
|
||||
<span class="stat-num">{stats.ok}</span>
|
||||
<span class="stat-label">In Ordnung</span>
|
||||
</div>
|
||||
<div class="stat stat-warn">
|
||||
<span class="stat-num">{stats.abweichung}</span>
|
||||
<span class="stat-label">Abweichungen</span>
|
||||
</div>
|
||||
<div class="stat stat-crit">
|
||||
<span class="stat-num">{stats.kritisch}</span>
|
||||
<span class="stat-label">Kritisch</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Heutige Einträge</h2>
|
||||
|
||||
{#if loading}
|
||||
<p class="hint">Lädt…</p>
|
||||
{:else if logs.length === 0}
|
||||
<p class="hint">Noch keine Einträge heute.</p>
|
||||
{:else}
|
||||
<div class="log-list">
|
||||
{#each logs as log}
|
||||
<div class="log-item">
|
||||
<div class="log-status" style="background: {STATUS_COLOR[log.status]}20; color: {STATUS_COLOR[log.status]}">
|
||||
{STATUS_LABEL[log.status]}
|
||||
</div>
|
||||
<div class="log-info">
|
||||
<span class="log-station">{log.expand?.station?.name ?? '—'}</span>
|
||||
{#if log.temperature}
|
||||
<span class="log-temp">{log.temperature} °C</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="log-meta">
|
||||
<span>{log.checked_by}</span>
|
||||
<span class="log-time">{formatTime(log.created)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page { max-width: 800px; }
|
||||
h1 { font-size: 1.6rem; font-weight: 800; color: #0B1023; }
|
||||
h2 { font-size: 1.1rem; font-weight: 700; color: #0B1023; margin: 2rem 0 1rem; }
|
||||
.date { color: #888; margin-top: 0.25rem; margin-bottom: 2rem; }
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.stat {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
border-left: 3px solid #e2e8f0;
|
||||
}
|
||||
.stat-ok { border-color: #16a34a; }
|
||||
.stat-warn { border-color: #ea580c; }
|
||||
.stat-crit { border-color: #dc2626; }
|
||||
|
||||
.stat-num { display: block; font-size: 2rem; font-weight: 800; color: #0B1023; line-height: 1; }
|
||||
.stat-label { font-size: 0.8rem; color: #888; margin-top: 0.25rem; display: block; }
|
||||
|
||||
.hint { color: #aaa; font-size: 0.95rem; }
|
||||
|
||||
.log-list { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.log-item {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
.log-status {
|
||||
padding: 0.25rem 0.7rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.log-info { flex: 1; display: flex; flex-direction: column; gap: 0.1rem; }
|
||||
.log-station { font-weight: 600; font-size: 0.95rem; }
|
||||
.log-temp { font-size: 0.85rem; color: #555; }
|
||||
.log-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 0.1rem; font-size: 0.85rem; color: #888; }
|
||||
.log-time { font-size: 0.8rem; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.stats { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue