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:
rene 2026-05-17 11:37:42 +02:00
parent f2615c9e07
commit 18570a42f0
15 changed files with 6042 additions and 12 deletions

View file

@ -0,0 +1,129 @@
<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 loading = $state(true);
let page = $state(1);
let totalPages = $state(1);
const PER_PAGE = 30;
onMount(() => loadPage(1));
async function loadPage(p: number) {
loading = true;
const tenantId = (pb.authStore.record as any)?.tenant;
const result = await pb.collection('check_logs').getList(p, PER_PAGE, {
filter: `tenant = "${tenantId}"`,
expand: 'station',
sort: '-created'
});
logs = result.items;
page = p;
totalPages = Math.ceil(result.totalItems / PER_PAGE);
loading = false;
}
function formatDate(iso: string) {
return new Date(iso).toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit'
});
}
</script>
<svelte:head><title>Protokoll — checkflo</title></svelte:head>
<div class="page">
<h1>Protokoll</h1>
{#if loading}
<p class="hint">Lädt…</p>
{:else if logs.length === 0}
<p class="hint">Noch keine Einträge vorhanden.</p>
{:else}
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Station</th>
<th>Status</th>
<th>Temp.</th>
<th>Person</th>
<th>Notiz</th>
<th>Datum</th>
</tr>
</thead>
<tbody>
{#each logs as log}
<tr>
<td class="td-station">{log.expand?.station?.name ?? '—'}</td>
<td>
<span class="badge" style="background:{STATUS_COLOR[log.status]}20;color:{STATUS_COLOR[log.status]}">
{STATUS_LABEL[log.status]}
</span>
</td>
<td>{log.temperature ? log.temperature + ' °C' : '—'}</td>
<td>{log.checked_by}</td>
<td class="td-notes">{log.notes || '—'}</td>
<td class="td-date">{formatDate(log.created)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#if totalPages > 1}
<div class="pagination">
<button disabled={page === 1} onclick={() => loadPage(page - 1)}>← zurück</button>
<span>Seite {page} / {totalPages}</span>
<button disabled={page === totalPages} onclick={() => loadPage(page + 1)}>weiter →</button>
</div>
{/if}
{/if}
</div>
<style>
.page { max-width: 1000px; }
h1 { font-size: 1.6rem; font-weight: 800; color: #0B1023; margin-bottom: 1.5rem; }
.hint { color: #aaa; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
th { background: #F5F7FA; padding: 0.75rem 1rem; text-align: left; font-size: 0.8rem; font-weight: 700; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
td { padding: 0.75rem 1rem; border-top: 1px solid #f0f0f0; font-size: 0.9rem; vertical-align: middle; }
tr:hover td { background: #fafafa; }
.badge { padding: 0.2rem 0.6rem; border-radius: 20px; font-size: 0.8rem; font-weight: 700; white-space: nowrap; }
.td-station { font-weight: 600; }
.td-notes { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #666; }
.td-date { white-space: nowrap; color: #888; font-size: 0.85rem; }
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
font-size: 0.9rem;
color: #666;
}
.pagination button {
padding: 0.5rem 1rem;
border: 1.5px solid #ddd;
border-radius: 6px;
background: #fff;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.15s;
}
.pagination button:hover:not(:disabled) { border-color: #F97316; color: #F97316; }
.pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
</style>