vbytes-lan-attendence/web/src/routes/+page.svelte

233 lines
6.8 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { goto } from '$app/navigation';
import type { Person } from '$lib/types';
import { onMount } from 'svelte';
import { listenToPersonEvents } from '$lib/client/person-events';
let searchQuery = $state('');
let searchResults = $state<Person[]>([]);
let visibleResults = $state<Person[]>([]);
let searchLoading = $state(false);
let searchError = $state('');
let searchInfo = $state('');
let actionInfo = $state('');
async function apiFetch(url: string, init?: RequestInit) {
const response = await fetch(url, init);
if (response.status === 401) {
await goto('/login');
return null;
}
return response;
}
async function loadDefaultList() {
searchLoading = true;
searchError = '';
actionInfo = '';
try {
const response = await apiFetch('/api/persons/checked-in?checked=false');
if (!response) return;
if (!response.ok) {
const text = await response.text();
try {
const body = JSON.parse(text);
searchError = body.message ?? 'Kunde inte hämta personer.';
} catch {
searchError = text || 'Kunde inte hämta personer.';
}
searchResults = [];
visibleResults = [];
return;
}
const data = await response.json();
searchResults = data.persons ?? [];
updateVisibleResults();
} catch (err) {
console.error('Initial load failed', err);
searchError = 'Ett oväntat fel inträffade när listan skulle hämtas.';
searchResults = [];
visibleResults = [];
} finally {
searchLoading = false;
}
}
async function handleSearch(event?: SubmitEvent) {
event?.preventDefault();
actionInfo = '';
searchError = '';
searchInfo = '';
const query = searchQuery.trim();
if (!query) {
await loadDefaultList();
return;
}
searchLoading = true;
try {
const response = await apiFetch(`/api/persons/search?q=${encodeURIComponent(query)}`);
if (!response) return;
if (!response.ok) {
const body = await response.json().catch(() => ({}));
searchError = body.message ?? 'Sökningen misslyckades.';
searchResults = [];
visibleResults = [];
return;
}
const data = await response.json();
searchResults = data.persons ?? [];
updateVisibleResults();
} catch (err) {
console.error('Search failed', err);
searchError = 'Ett oväntat fel inträffade vid sökningen.';
searchResults = [];
visibleResults = [];
} finally {
searchLoading = false;
}
}
function updatePersonList(updated: Person) {
let found = false;
searchResults = searchResults.map((person) => {
if (person.id === updated.id) {
found = true;
return updated;
}
return person;
});
if (!found && !updated.checked_in) {
searchResults = [updated, ...searchResults];
}
updateVisibleResults();
}
async function handleCheckIn(person: Person) {
actionInfo = '';
if (person.checked_in) return;
const response = await apiFetch(`/api/persons/${person.id}/checkin`, { method: 'POST' });
if (!response) return;
if (!response.ok) {
const body = await response.json().catch(() => ({}));
searchError = body.message ?? 'Kunde inte checka in personen.';
return;
}
const data = await response.json();
searchError = '';
updatePersonList(data.person);
actionInfo = 'Personen är nu incheckad.';
}
function updateVisibleResults() {
const filtered = searchResults.filter((person) => !person.checked_in);
visibleResults = filtered;
if (searchResults.length === 0) {
searchInfo = 'Ingen träff på sökningen.';
} else if (filtered.length === 0) {
searchInfo = 'Alla personer i listan är redan incheckade.';
} else {
searchInfo = '';
}
}
onMount(() => {
void loadDefaultList();
const stop = listenToPersonEvents((person) => {
updatePersonList(person);
});
return () => {
stop();
};
});
</script>
<div class="space-y-6">
<section class="rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
<h3 class="text-lg font-semibold text-slate-800">Checka in</h3>
<p class="mb-4 text-sm text-slate-500">
Listan visar automatiskt alla som inte är incheckade. Sök för att begränsa listan.
</p>
<form class="flex flex-col gap-3 sm:flex-row" onsubmit={handleSearch}>
<input
type="text"
placeholder="Exempel: Anna, 42 eller 0701234567"
bind:value={searchQuery}
class="w-full rounded-md border border-slate-300 px-3 py-2 text-slate-900 focus:border-indigo-500 focus:ring focus:ring-indigo-100 focus:outline-none"
/>
<button
type="submit"
disabled={searchLoading}
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-70"
>
{searchLoading ? 'Laddar…' : 'Sök'}
</button>
</form>
{#if searchError}
<p class="mt-4 rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{searchError}</p>
{/if}
{#if searchInfo}
<p class="mt-4 rounded-md bg-amber-50 px-3 py-2 text-sm text-amber-700">{searchInfo}</p>
{/if}
{#if actionInfo}
<p class="mt-4 rounded-md bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{actionInfo}</p>
{/if}
{#if visibleResults.length > 0}
<ul class="mt-6 space-y-4">
{#each visibleResults as person}
<li class="rounded-lg border border-slate-200 p-4 shadow-sm">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h4 class="text-base font-semibold text-slate-800">{person.name}</h4>
<p class="text-sm text-slate-500">
ID: {person.id} · Telefon: {person.phone_number}
</p>
</div>
<div class="flex flex-wrap gap-2">
<span
class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600"
>Inte incheckad</span
>
<span
class={`rounded-full px-3 py-1 text-xs font-semibold ${
person.inside ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-600'
}`}>{person.inside ? 'Inne' : 'Ute'}</span
>
</div>
</div>
<p class="mt-2 text-sm text-slate-600">Ålder: {person.age} år</p>
{#if person.under_ten}
<p
class="mt-2 rounded-md bg-amber-100 px-3 py-2 text-sm font-semibold text-amber-800"
>
VARNING: Person under 10 år kompletterande information krävs innan incheckning.
</p>
{/if}
<div class="mt-4 flex gap-3">
<button
type="button"
onclick={() => handleCheckIn(person)}
disabled={person.checked_in || searchLoading}
class={`rounded-md px-4 py-2 text-sm font-semibold transition-colors ${
person.checked_in
? 'cursor-not-allowed bg-slate-200 text-slate-600'
: 'bg-emerald-600 text-white hover:bg-emerald-700'
}`}
>
Checka in
</button>
</div>
</li>
{/each}
</ul>
{/if}
</section>
</div>