From 35c7430c5028778f64f4588827e0107bd462e25f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 22 Sep 2025 10:29:14 +0200 Subject: [PATCH] web updated to comply with the new datamodel and some new filtering added.. --- api/src/main.rs | 106 ++++++- api/src/models.rs | 16 + .../lib/components/edit-person-modal.svelte | 278 ++++++++++++++++++ web/src/lib/types.ts | 11 +- web/src/routes/+page.svelte | 66 ++++- web/src/routes/api/persons/[id]/+server.ts | 45 +++ .../routes/api/persons/checked-in/+server.ts | 4 + web/src/routes/checked-in/+page.svelte | 208 ++++++++++--- web/src/routes/checkout/+page.svelte | 166 +++++++++-- web/src/routes/create/+page.server.ts | 62 ++-- web/src/routes/create/+page.svelte | 193 ++++++++---- web/src/routes/inside-status/+page.svelte | 52 +++- 12 files changed, 1023 insertions(+), 184 deletions(-) create mode 100644 web/src/lib/components/edit-person-modal.svelte create mode 100644 web/src/routes/api/persons/[id]/+server.ts diff --git a/api/src/main.rs b/api/src/main.rs index 6e82206..501bcbf 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -9,14 +9,14 @@ use config::AppConfig; use error::ApiError; use models::{ LoginRequest, LoginResponse, NewPersonRequest, Person, PersonActionResponse, PersonResponse, - PersonsResponse, User, + PersonsResponse, UpdatePersonRequest, User, }; use rocket::http::{Cookie, CookieJar, SameSite, Status}; use rocket::response::stream::{Event, EventStream}; use rocket::serde::json::Json; use rocket::time::Duration; use rocket::tokio::sync::broadcast::{self, error::RecvError}; -use rocket::{get, post, routes, State}; +use rocket::{get, post, put, routes, State}; use sqlx::postgres::PgPoolOptions; use sqlx::{PgPool, QueryBuilder}; @@ -76,7 +76,8 @@ async fn main() -> Result<(), rocket::Error> { checkout_person, mark_inside, mark_outside, - create_person + create_person, + update_person ], ); @@ -399,8 +400,8 @@ async fn create_person( let grade = payload.grade; let checked_in = payload.checked_in.unwrap_or(false); let inside = payload.inside.unwrap_or(false); - let visitor = payload.visitor.unwrap_or(false); - let sleeping_spot = payload.sleeping_spot.unwrap_or(false); + let visitor = payload.visitor; + let sleeping_spot = payload.sleeping_spot; let person = match payload.id { Some(id) => sqlx::query_as::<_, Person>( @@ -492,6 +493,101 @@ async fn create_person( Ok(Json(response)) } +#[put("/", data = "")] +async fn update_person( + _user: AuthUser, + state: &State, + id: i32, + payload: Json, +) -> Result, ApiError> { + let first_name = payload.first_name.trim(); + if first_name.is_empty() { + return Err(ApiError::bad_request("Förnamn får inte vara tomt.")); + } + + let last_name = payload.last_name.trim(); + if last_name.is_empty() { + return Err(ApiError::bad_request("Efternamn får inte vara tomt.")); + } + + if payload.grade < 0 { + return Err(ApiError::bad_request("Klass måste vara noll eller högre.")); + } + + let parent_name = payload.parent_name.trim(); + if parent_name.is_empty() { + return Err(ApiError::bad_request("Vårdnadshavare krävs.")); + } + + let parent_phone_number = payload.parent_phone_number.trim(); + if parent_phone_number.is_empty() { + return Err(ApiError::bad_request("Vårdnadshavarens telefon krävs.")); + } + + let checked_in = payload.checked_in; + let inside = payload.inside; + let visitor = payload.visitor; + let sleeping_spot = payload.sleeping_spot; + + let (final_checked_in, final_inside) = match (checked_in, inside) { + (Some(false), _) => (Some(false), Some(false)), + (Some(true), Some(value)) => (Some(true), Some(value)), + (Some(true), None) => (Some(true), Some(true)), + (None, Some(value)) => (None, Some(value)), + (None, None) => (None, None), + }; + + let person = sqlx::query_as::<_, Person>( + r#" + UPDATE persons + SET first_name = $2, + last_name = $3, + grade = $4, + parent_name = $5, + parent_phone_number = $6, + checked_in = COALESCE($7, checked_in), + inside = COALESCE($8, inside), + visitor = $9, + sleeping_spot = $10 + WHERE id = $1 + RETURNING + id, + first_name, + last_name, + grade, + parent_name, + parent_phone_number, + checked_in, + inside, + visitor, + sleeping_spot + "#, + ) + .bind(id) + .bind(first_name) + .bind(last_name) + .bind(payload.grade) + .bind(parent_name) + .bind(parent_phone_number) + .bind(final_checked_in) + .bind(final_inside) + .bind(visitor) + .bind(sleeping_spot) + .fetch_optional(&state.db) + .await?; + + match person { + Some(person) => { + let response = PersonActionResponse { + person: person.into(), + }; + broadcast_person_update(state, &response); + Ok(Json(response)) + } + None => Err(ApiError::not_found("Personen hittades inte.")), + } +} + async fn update_checked_in( state: &State, id: i32, diff --git a/api/src/models.rs b/api/src/models.rs index dc31b62..2995d62 100644 --- a/api/src/models.rs +++ b/api/src/models.rs @@ -99,3 +99,19 @@ pub struct NewPersonRequest { #[serde(default)] pub sleeping_spot: Option, } + +#[derive(Debug, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct UpdatePersonRequest { + pub first_name: String, + pub last_name: String, + pub grade: i32, + pub parent_name: String, + pub parent_phone_number: String, + #[serde(default)] + pub checked_in: Option, + #[serde(default)] + pub inside: Option, + pub visitor: bool, + pub sleeping_spot: bool, +} diff --git a/web/src/lib/components/edit-person-modal.svelte b/web/src/lib/components/edit-person-modal.svelte new file mode 100644 index 0000000..4048197 --- /dev/null +++ b/web/src/lib/components/edit-person-modal.svelte @@ -0,0 +1,278 @@ + + +{#if props.open && props.person} +
+
+
+
+

Redigera person

+

ID: {props.person.id}

+
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+ + {#if errorMessage} +

{errorMessage}

+ {/if} + +
+ + +
+
+
+
+{/if} diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 075fca4..93d9aec 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -1,9 +1,12 @@ export interface Person { id: number; - name: string; - age: number; - phone_number: string; + first_name: string; + last_name: string; + grade: number; + parent_name: string; + parent_phone_number: string; checked_in: boolean; inside: boolean; - under_ten: boolean; + visitor: boolean; + sleeping_spot: boolean; } diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 77352a0..be327bb 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -3,6 +3,7 @@ import type { Person } from '$lib/types'; import { onMount } from 'svelte'; import { listenToPersonEvents } from '$lib/client/person-events'; + import EditPersonModal from '$lib/components/edit-person-modal.svelte'; let searchQuery = $state(''); let searchResults = $state([]); @@ -11,6 +12,15 @@ let searchError = $state(''); let searchInfo = $state(''); let actionInfo = $state(''); + let editor = $state<{ open: boolean; person: Person | null }>({ open: false, person: null }); + + function fullName(person: Person) { + return `${person.first_name} ${person.last_name}`.trim(); + } + + function isLowerGrade(person: Person) { + return person.grade <= 3; + } async function apiFetch(url: string, init?: RequestInit) { const response = await fetch(url, init); @@ -106,6 +116,19 @@ updateVisibleResults(); } + function openEditor(person: Person) { + editor = { open: true, person }; + } + + function closeEditor() { + editor = { open: false, person: null }; + } + + function handleEditorSaved(updated: Person) { + updatePersonList(updated); + actionInfo = 'Personen uppdaterades.'; + } + async function handleCheckIn(person: Person) { actionInfo = ''; if (person.checked_in) return; @@ -158,7 +181,7 @@
@@ -186,9 +209,12 @@
  • -

    {person.name}

    +

    {fullName(person)}

    - ID: {person.id} · Telefon: {person.phone_number} + ID: {person.id} · Klass: {person.grade} +

    +

    + Vårdnadshavare: {person.parent_name} ({person.parent_phone_number})

    @@ -201,14 +227,21 @@ person.inside ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-600' }`}>{person.inside ? 'Inne' : 'Ute'} + {#if person.visitor} + + Besöksplats + + {/if} + {#if person.sleeping_spot} + + Behöver sovplats + + {/if}
    -

    Ålder: {person.age} år

    - {#if person.under_ten} -

    - VARNING: Person under 10 år – kompletterande information krävs innan incheckning. + {#if isLowerGrade(person)} +

    + Observera: elev i årskurs 3 eller yngre – ska hem senast 22:00.

    {/if}
    @@ -224,6 +257,14 @@ > Checka in +
  • {/each} @@ -231,3 +272,10 @@ {/if} + + handleEditorSaved(event.detail)} +/> diff --git a/web/src/routes/api/persons/[id]/+server.ts b/web/src/routes/api/persons/[id]/+server.ts new file mode 100644 index 0000000..36aa9f8 --- /dev/null +++ b/web/src/routes/api/persons/[id]/+server.ts @@ -0,0 +1,45 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { proxyRequest } from '$lib/server/backend'; + +export const PUT: RequestHandler = async (event) => { + const { id } = event.params; + if (!id) { + throw error(400, 'Ogiltigt id.'); + } + + const payload = await event.request.json(); + const { response, setCookies } = await proxyRequest(event, `/persons/${id}`, { + method: 'PUT', + body: JSON.stringify(payload) + }); + + const headers = new Headers(); + for (const cookie of setCookies) { + headers.append('set-cookie', cookie); + } + + const contentType = response.headers.get('content-type'); + if (contentType) { + headers.set('content-type', contentType); + } else { + headers.set('content-type', 'application/json'); + } + + const text = await response.text(); + if (!response.ok) { + let message = 'Kunde inte uppdatera personen.'; + try { + const body = JSON.parse(text); + message = body.message ?? message; + } catch { + if (text) { + message = text; + } + } + + throw error(response.status, message); + } + + return new Response(text, { status: response.status, headers }); +}; diff --git a/web/src/routes/api/persons/checked-in/+server.ts b/web/src/routes/api/persons/checked-in/+server.ts index 7480975..b6a7d19 100644 --- a/web/src/routes/api/persons/checked-in/+server.ts +++ b/web/src/routes/api/persons/checked-in/+server.ts @@ -6,6 +6,7 @@ export const GET: RequestHandler = async (event) => { const params = new URLSearchParams(); const q = event.url.searchParams.get('q'); const status = event.url.searchParams.get('status'); + const checked = event.url.searchParams.get('checked'); if (q) { params.set('q', q); @@ -13,6 +14,9 @@ export const GET: RequestHandler = async (event) => { if (status) { params.set('status', status); } + if (checked) { + params.set('checked', checked); + } const path = params.toString() ? `/persons/checked-in?${params}` : '/persons/checked-in'; const { response, setCookies } = await proxyRequest(event, path, { method: 'GET' }); diff --git a/web/src/routes/checked-in/+page.svelte b/web/src/routes/checked-in/+page.svelte index bc96b90..9879699 100644 --- a/web/src/routes/checked-in/+page.svelte +++ b/web/src/routes/checked-in/+page.svelte @@ -4,10 +4,15 @@ import { onMount } from 'svelte'; import { listenToPersonEvents } from '$lib/client/person-events'; import { updateCollection } from '$lib/client/person-collection'; + import EditPersonModal from '$lib/components/edit-person-modal.svelte'; type StatusFilter = 'all' | 'inside' | 'outside'; type CheckedFilter = 'all' | 'checked-in' | 'not-checked-in'; + type GradeFilter = 'all' | 'lt4' | 'ge4'; +type VisitorFilter = 'all' | 'besoksplats' | 'lanplats'; + type SleepingFilter = 'all' | 'needs' | 'not-needed'; + let allPersons = $state([]); let persons = $state([]); let loading = $state(false); let errorMessage = $state(''); @@ -15,6 +20,18 @@ let searchQuery = $state(''); let statusFilter = $state('all'); let checkedFilter = $state('checked-in'); + let gradeFilter = $state('all'); + let visitorFilter = $state('all'); + let sleepingFilter = $state('all'); + let editor = $state<{ open: boolean; person: Person | null }>({ open: false, person: null }); + + function fullName(person: Person) { + return `${person.first_name} ${person.last_name}`.trim(); + } + + function isLowerGrade(person: Person) { + return person.grade <= 3; + } async function apiFetch(url: string) { const response = await fetch(url); @@ -26,44 +43,55 @@ } function matchesFilters(person: Person) { - if (checkedFilter === 'checked-in' && !person.checked_in) return false; - if (checkedFilter === 'not-checked-in' && person.checked_in) return false; - if (statusFilter === 'inside' && !person.inside) return false; - if (statusFilter === 'outside' && person.inside) return false; + if (checkedFilter === 'checked-in' && !person.checked_in) return false; + if (checkedFilter === 'not-checked-in' && person.checked_in) return false; + if (statusFilter === 'inside' && !person.inside) return false; + if (statusFilter === 'outside' && person.inside) return false; + if (gradeFilter === 'lt4' && person.grade > 3) return false; + if (gradeFilter === 'ge4' && person.grade <= 3) return false; + if (visitorFilter === 'besoksplats' && !person.visitor) return false; + if (visitorFilter === 'lanplats' && person.visitor) return false; + if (sleepingFilter === 'needs' && !person.sleeping_spot) return false; + if (sleepingFilter === 'not-needed' && person.sleeping_spot) return false; if (searchQuery.trim()) { const query = searchQuery.trim().toLowerCase(); const matchesText = - person.name.toLowerCase().includes(query) || - person.phone_number.toLowerCase().includes(query) || + `${person.first_name} ${person.last_name}`.toLowerCase().includes(query) || + person.parent_name.toLowerCase().includes(query) || + person.parent_phone_number.toLowerCase().includes(query) || person.id.toString() === query; if (!matchesText) return false; } return true; } - function applyFilteredList(list: Person[]) { - const filtered = list.filter((person) => { - if (statusFilter === 'inside' && !person.inside) return false; - if (statusFilter === 'outside' && person.inside) return false; - if (checkedFilter === 'checked-in' && !person.checked_in) return false; - if (checkedFilter === 'not-checked-in' && person.checked_in) return false; - return true; - }); + function updateVisiblePersons() { + const filtered = allPersons.filter((person) => matchesFilters(person)); persons = filtered; - if (persons.length === 0) { - infoMessage = 'Inga personer matchar kriterierna.'; - } else { - infoMessage = ''; - } + infoMessage = filtered.length === 0 ? 'Inga personer matchar kriterierna.' : ''; + } + + function applyFetchedList(list: Person[]) { + allPersons = list; + updateVisiblePersons(); } function handlePersonUpdate(person: Person) { - persons = updateCollection(persons, person, matchesFilters); - if (persons.length === 0) { - infoMessage = 'Inga personer matchar kriterierna.'; - } else { - infoMessage = ''; - } + allPersons = updateCollection(allPersons, person, () => true); + updateVisiblePersons(); + } + + function openEditor(person: Person) { + editor = { open: true, person }; + } + + function closeEditor() { + editor = { open: false, person: null }; + } + + function handleEditorSaved(person: Person) { + handlePersonUpdate(person); + infoMessage = 'Personen uppdaterades.'; } async function fetchCheckedIn() { @@ -99,16 +127,18 @@ } catch { errorMessage = text || 'Kunde inte hämta personer.'; } + allPersons = []; persons = []; return; } const data = await response.json(); const list: Person[] = data.persons ?? []; - applyFilteredList(list); + applyFetchedList(list); } catch (err) { console.error('Fetch checked-in failed', err); errorMessage = 'Ett oväntat fel inträffade när listan skulle hämtas.'; + allPersons = []; persons = []; } finally { loading = false; @@ -128,6 +158,24 @@ await fetchCheckedIn(); } + function handleGradeChange(event: Event) { + const value = (event.currentTarget as HTMLSelectElement).value as GradeFilter; + gradeFilter = value; + updateVisiblePersons(); + } + + function handleVisitorChange(event: Event) { + const value = (event.currentTarget as HTMLSelectElement).value as VisitorFilter; + visitorFilter = value; + updateVisiblePersons(); + } + + function handleSleepingChange(event: Event) { + const value = (event.currentTarget as HTMLSelectElement).value as SleepingFilter; + sleepingFilter = value; + updateVisiblePersons(); + } + onMount(() => { void fetchCheckedIn(); const stop = listenToPersonEvents((person) => { @@ -158,13 +206,16 @@ - +
    @@ -197,6 +248,51 @@
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + {/each} + + + handleEditorSaved(event.detail)} +/> diff --git a/web/src/routes/checkout/+page.svelte b/web/src/routes/checkout/+page.svelte index ab44d08..5f1ce33 100644 --- a/web/src/routes/checkout/+page.svelte +++ b/web/src/routes/checkout/+page.svelte @@ -4,6 +4,9 @@ import { onMount } from 'svelte'; import { listenToPersonEvents } from '$lib/client/person-events'; + type GradeFilter = 'all' | 'lt4' | 'ge4'; + type VisitorFilter = 'all' | 'besoksplats' | 'lanplats'; + let searchQuery = $state(''); let searchResults = $state([]); let visibleResults = $state([]); @@ -11,6 +14,51 @@ let searchError = $state(''); let searchInfo = $state(''); let actionInfo = $state(''); + let gradeFilter = $state('all'); + let visitorFilter = $state('all'); + + function fullName(person: Person) { + return `${person.first_name} ${person.last_name}`.trim(); + } + + function isLowerGrade(person: Person) { + return person.grade <= 3; + } + + function matchesFilters(person: Person) { + if (!person.checked_in) { + return false; + } + + if (gradeFilter === 'lt4' && person.grade > 3) { + return false; + } + if (gradeFilter === 'ge4' && person.grade <= 3) { + return false; + } + + if (visitorFilter === 'besoksplats' && !person.visitor) { + return false; + } + if (visitorFilter === 'lanplats' && person.visitor) { + return false; + } + + const query = searchQuery.trim().toLowerCase(); + if (query) { + const combinedName = `${person.first_name} ${person.last_name}`.toLowerCase(); + const matchesText = + combinedName.includes(query) || + person.parent_name.toLowerCase().includes(query) || + person.parent_phone_number.toLowerCase().includes(query) || + person.id.toString() === query; + if (!matchesText) { + return false; + } + } + + return true; + } async function apiFetch(url: string, init?: RequestInit) { const response = await fetch(url, init); @@ -101,7 +149,7 @@ } return person; }); - if (!found) { + if (!found && updated.checked_in) { searchResults = [updated, ...searchResults]; } updateVisibleResults(); @@ -129,18 +177,30 @@ } function updateVisibleResults() { - const filtered = searchResults.filter((person) => person.checked_in); + const filtered = searchResults.filter((person) => matchesFilters(person)); visibleResults = filtered; - if (searchResults.length === 0) { - searchInfo = 'Ingen träff på sökningen.'; + if (searchResults.length === 0 && !searchQuery.trim()) { + searchInfo = 'Inga personer hämtades.'; } else if (filtered.length === 0) { - searchInfo = 'Inga personer kan checkas ut just nu.'; + searchInfo = 'Ingen person matchar de valda filtren just nu.'; } else { searchInfo = ''; } } + function handleGradeFilterChange(event: Event) { + const value = (event.currentTarget as HTMLSelectElement).value as GradeFilter; + gradeFilter = value; + updateVisibleResults(); + } + + function handleVisitorFilterChange(event: Event) { + const value = (event.currentTarget as HTMLSelectElement).value as VisitorFilter; + visitorFilter = value; + updateVisibleResults(); + } + onMount(() => { void loadDefaultList(); const stop = listenToPersonEvents((person) => { @@ -156,22 +216,62 @@

    Checka ut

    - Sök på namn, id eller telefonnummer för att checka ut personer som är incheckade. + Sök på namn, id, vårdnadshavare eller telefonnummer för att hitta personer som ska + checkas ut.

    - - - + +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    {#if searchError}

    {searchError}

    @@ -189,9 +289,12 @@
  • -

    {person.name}

    +

    {fullName(person)}

    - ID: {person.id} · Telefon: {person.phone_number} + ID: {person.id} · Klass: {person.grade} +

    +

    + Vårdnadshavare: {person.parent_name} ({person.parent_phone_number})

    @@ -207,14 +310,21 @@ person.inside ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-600' }`}>{person.inside ? 'Inne' : 'Ute'} + {#if person.visitor} + + Besöksplats + + {/if} + {#if person.sleeping_spot} + + Behöver sovplats + + {/if}
    -

    Ålder: {person.age} år

    - {#if person.under_ten} -

    - VARNING: Person under 10 år – kompletterande information krävs innan utcheckning. + {#if isLowerGrade(person)} +

    + Observera: elev i årskurs 3 eller yngre – ska hem senast 22:00.

    {/if}
    diff --git a/web/src/routes/create/+page.server.ts b/web/src/routes/create/+page.server.ts index 90398ad..5e04426 100644 --- a/web/src/routes/create/+page.server.ts +++ b/web/src/routes/create/+page.server.ts @@ -4,50 +4,76 @@ export const actions: Actions = { default: async ({ request, fetch }) => { const formData = await request.formData(); - const name = formData.get('name')?.toString().trim() ?? ''; - const ageRaw = formData.get('age')?.toString().trim() ?? ''; - const phone = formData.get('phone_number')?.toString().trim() ?? ''; + const firstName = formData.get('first_name')?.toString().trim() ?? ''; + const lastName = formData.get('last_name')?.toString().trim() ?? ''; + const gradeRaw = formData.get('grade')?.toString().trim() ?? ''; + const parentName = formData.get('parent_name')?.toString().trim() ?? ''; + const parentPhone = formData.get('parent_phone_number')?.toString().trim() ?? ''; const manualId = formData.get('manual_id')?.toString().trim() ?? ''; const checkedIn = formData.get('checked_in') === 'on'; const inside = formData.get('inside') === 'on'; + const visitor = formData.get('visitor') === 'on'; + const sleepingSpot = formData.get('sleeping_spot') === 'on'; const values = { - name, - age: ageRaw, - phone_number: phone, + first_name: firstName, + last_name: lastName, + grade: gradeRaw, + parent_name: parentName, + parent_phone_number: parentPhone, manual_id: manualId, checked_in: checkedIn, - inside + inside, + visitor, + sleeping_spot: sleepingSpot }; - if (!name) { + if (!firstName) { return fail(400, { - errors: { name: 'Ange ett namn.' }, + errors: { first_name: 'Ange ett förnamn.' }, values }); } - const parsedAge = Number.parseInt(ageRaw, 10); - if (Number.isNaN(parsedAge) || parsedAge < 0) { + if (!lastName) { return fail(400, { - errors: { age: 'Ålder måste vara ett heltal större än eller lika med 0.' }, + errors: { last_name: 'Ange ett efternamn.' }, values }); } - if (!phone) { + const parsedGrade = Number.parseInt(gradeRaw, 10); + if (Number.isNaN(parsedGrade) || parsedGrade < 0) { return fail(400, { - errors: { phone_number: 'Ange ett telefonnummer.' }, + errors: { grade: 'Klass måste vara ett heltal större än eller lika med 0.' }, + values + }); + } + + if (!parentName) { + return fail(400, { + errors: { parent_name: 'Ange en kontaktperson.' }, + values + }); + } + + if (!parentPhone) { + return fail(400, { + errors: { parent_phone_number: 'Ange kontaktpersonens telefonnummer.' }, values }); } const payload: Record = { - name, - age: parsedAge, - phone_number: phone, + first_name: firstName, + last_name: lastName, + grade: parsedGrade, + parent_name: parentName, + parent_phone_number: parentPhone, checked_in: checkedIn, - inside + inside, + visitor, + sleeping_spot: sleepingSpot }; if (manualId.length > 0) { diff --git a/web/src/routes/create/+page.svelte b/web/src/routes/create/+page.svelte index 9aa1b4c..39f1787 100644 --- a/web/src/routes/create/+page.svelte +++ b/web/src/routes/create/+page.svelte @@ -3,29 +3,39 @@ const props = $props(); type FormValues = { - name: string; - age: string; - phone_number: string; + first_name: string; + last_name: string; + grade: string; + parent_name: string; + parent_phone_number: string; manual_id: string; checked_in: boolean; inside: boolean; + visitor: boolean; + sleeping_spot: boolean; }; type FormErrors = { - name?: string; - age?: string; - phone_number?: string; + first_name?: string; + last_name?: string; + grade?: string; + parent_name?: string; + parent_phone_number?: string; manual_id?: string; general?: string; }; const defaults: FormValues = { - name: '', - age: '', - phone_number: '', + first_name: '', + last_name: '', + grade: '', + parent_name: '', + parent_phone_number: '', manual_id: '', checked_in: false, - inside: false + inside: false, + visitor: false, + sleeping_spot: false }; const values = $derived({ @@ -44,49 +54,53 @@ Fyll i uppgifterna nedan. Om ID lämnas tomt skapas ett automatiskt.

    -
    - - - {#if errors.name} -

    {errors.name}

    - {/if} -
    - - - {#if errors.age} -

    {errors.age}

    - {/if} -
    -
    - Förnamn - {#if errors.phone_number} -

    {errors.phone_number}

    + {#if errors.first_name} +

    {errors.first_name}

    + {/if} +
    +
    + + + {#if errors.last_name} +

    {errors.last_name}

    + {/if} +
    +
    + + + {#if errors.grade} +

    {errors.grade}

    {/if}
    @@ -105,28 +119,81 @@

    {errors.manual_id}

    {/if}
    -
    +
    + - + {#if errors.parent_name} +

    {errors.parent_name}

    + {/if}
    -
    +
    + - + {#if errors.parent_phone_number} +

    {errors.parent_phone_number}

    + {/if}
    +
    + + + + +
    + {#if errors.general}

    {errors.general} diff --git a/web/src/routes/inside-status/+page.svelte b/web/src/routes/inside-status/+page.svelte index d8079da..68d3bfc 100644 --- a/web/src/routes/inside-status/+page.svelte +++ b/web/src/routes/inside-status/+page.svelte @@ -15,6 +15,14 @@ let searchQuery = $state(''); let statusFilter = $state('all'); + function fullName(person: Person) { + return `${person.first_name} ${person.last_name}`.trim(); + } + + function isLowerGrade(person: Person) { + return person.grade <= 3; + } + async function apiFetch(url: string, init?: RequestInit) { const response = await fetch(url, init); if (response.status === 401) { @@ -30,10 +38,11 @@ if (statusFilter === 'outside' && person.inside) return false; const query = searchQuery.trim().toLowerCase(); if (query) { - const matchesText = - person.name.toLowerCase().includes(query) || - person.phone_number.toLowerCase().includes(query) || - person.id.toString() === query; + const matchesText = + `${person.first_name} ${person.last_name}`.toLowerCase().includes(query) || + person.parent_name.toLowerCase().includes(query) || + person.parent_phone_number.toLowerCase().includes(query) || + person.id.toString() === query; if (!matchesText) return false; } return true; @@ -139,9 +148,10 @@ if (body.person) { const updated = body.person; handlePersonUpdate(updated); + const name = fullName(updated); actionMessage = updated.inside - ? `${updated.name} är nu markerad som inne.` - : `${updated.name} är nu markerad som ute.`; + ? `${name} är nu markerad som inne.` + : `${name} är nu markerad som ute.`; } } catch (err) { console.error('Parsing toggle response failed', err); @@ -184,7 +194,7 @@ @@ -233,8 +243,11 @@

    -

    {person.name}

    -

    ID: {person.id} · Telefon: {person.phone_number}

    +

    {fullName(person)}

    +

    ID: {person.id} · Klass: {person.grade}

    +

    + Vårdnadshavare: {person.parent_name} ({person.parent_phone_number}) +

    {person.inside ? 'Inne' : 'Ute'} + {#if person.visitor} + + Besöksplats + + {/if} + {#if person.sleeping_spot} + + Behöver sovplats + + {/if}
    -

    Ålder: {person.age} år

    - {#if person.under_ten} -

    - VARNING: Person under 10 år – kompletterande information krävs. -

    - {/if} + {#if isLowerGrade(person)} +

    + Observera: elev i årskurs 3 eller yngre – ska hem senast 22:00. +

    + {/if}