From 1a4d1cf73c25a7d3a936085c7ebbd9858551c38c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 16 Oct 2025 10:58:57 +0200 Subject: [PATCH] fixing verification for both a team registration and a indivual turnament registration --- api/src/models/tournament.rs | 55 +++- api/src/routes/tournaments.rs | 303 +++++++++++++----- .../(admin)/admin/tournament/+page.svelte | 155 ++++++--- .../tournament/[slug]/+page.svelte | 66 ++-- 4 files changed, 415 insertions(+), 164 deletions(-) diff --git a/api/src/models/tournament.rs b/api/src/models/tournament.rs index c930270..6bed3f1 100644 --- a/api/src/models/tournament.rs +++ b/api/src/models/tournament.rs @@ -177,8 +177,8 @@ impl TournamentSignupConfig { } self.entry_fields = normalize_signup_fields(self.entry_fields); - ensure_attendance_id_field(&mut self.entry_fields); self.participant_fields = normalize_signup_fields(self.participant_fields); + ensure_attendance_field_for_mode(&mut self); self } @@ -218,26 +218,53 @@ fn normalize_signup_fields(mut fields: Vec) -> Vec) { +fn remove_attendance_id_field( + fields: &mut Vec, +) -> Option { let mut attendance_index = None; - for (index, field) in fields.iter_mut().enumerate() { + for (index, field) in fields.iter().enumerate() { if field.id == ATTENDANCE_ID_FIELD_ID { attendance_index = Some(index); - sanitize_attendance_id_field(field); break; } } - match attendance_index { - Some(index) => { - if index != 0 { - let field = fields.remove(index); - fields.insert(0, field); - } - } - None => { - fields.insert(0, default_attendance_id_field()); - } + attendance_index.map(|index| { + let mut field = fields.remove(index); + sanitize_attendance_id_field(&mut field); + field + }) +} + +fn insert_attendance_field_front( + fields: &mut Vec, + mut field: TournamentSignupField, +) { + sanitize_attendance_id_field(&mut field); + fields.insert(0, field); +} + +fn ensure_attendance_field_for_mode(config: &mut TournamentSignupConfig) { + let mut attendance = remove_attendance_id_field(&mut config.entry_fields) + .or_else(|| remove_attendance_id_field(&mut config.participant_fields)) + .unwrap_or_else(default_attendance_id_field); + + if config.mode == "team" { + config + .entry_fields + .retain(|field| field.id != ATTENDANCE_ID_FIELD_ID); + config + .participant_fields + .retain(|field| field.id != ATTENDANCE_ID_FIELD_ID); + insert_attendance_field_front(&mut config.participant_fields, attendance); + } else { + config + .entry_fields + .retain(|field| field.id != ATTENDANCE_ID_FIELD_ID); + config + .participant_fields + .retain(|field| field.id != ATTENDANCE_ID_FIELD_ID); + insert_attendance_field_front(&mut config.entry_fields, attendance); } } diff --git a/api/src/routes/tournaments.rs b/api/src/routes/tournaments.rs index 6bc973e..00f8d10 100644 --- a/api/src/routes/tournaments.rs +++ b/api/src/routes/tournaments.rs @@ -16,7 +16,7 @@ use rocket::serde::json::Json; use rocket::Route; use serde_json::{Map, Value}; use sqlx::{Postgres, Transaction}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; pub fn routes() -> Vec { rocket::routes![ @@ -1178,47 +1178,122 @@ pub async fn create_registration_by_slug( )); } - let attendance_id_value = entry_values - .get(ATTENDANCE_ID_FIELD_ID) - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - .ok_or_else(|| ApiError::bad_request("Ange ditt deltagar-ID från närvarolistan."))?; + let is_team = config.mode == "team"; + let mut lead_participant: Option<(i32, Person)> = None; - let attendance_id: i32 = attendance_id_value.parse().map_err(|_| { - ApiError::bad_request("Deltagar-ID måste vara ett heltal från närvarolistan.") + if is_team { + entry_values.remove(ATTENDANCE_ID_FIELD_ID); + let mut seen_attendance_ids: HashSet = HashSet::new(); + + for (index, values) in participant_values.iter_mut().enumerate() { + let attendance_raw = values + .get(ATTENDANCE_ID_FIELD_ID) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + ApiError::bad_request(format!( + "Spelare #{number}: ange deltagar-ID från närvarolistan.", + number = index + 1 + )) + })?; + + let attendance_id: i32 = attendance_raw.parse().map_err(|_| { + ApiError::bad_request(format!( + "Spelare #{number}: deltagar-ID måste vara ett heltal från närvarolistan.", + number = index + 1 + )) + })?; + + if !seen_attendance_ids.insert(attendance_id) { + return Err(ApiError::bad_request(format!( + "Spelare #{number}: deltagar-ID används redan av en annan spelare i laget.", + number = index + 1 + ))); + } + + let person = sqlx::query_as::<_, Person>( + r#" + SELECT + id, + first_name, + last_name, + grade, + parent_name, + parent_phone_number, + checked_in, + inside, + visitor, + sleeping_spot + FROM persons + WHERE id = $1 + "#, + ) + .bind(attendance_id) + .fetch_optional(&mut *tx) + .await? + .ok_or_else(|| { + ApiError::bad_request(format!( + "Spelare #{number}: deltagar-ID:t finns inte i närvarolistan.", + number = index + 1 + )) + })?; + + values.insert( + ATTENDANCE_ID_FIELD_ID.to_string(), + attendance_id.to_string(), + ); + + if lead_participant.is_none() { + lead_participant = Some((attendance_id, person.clone())); + } + } + } else { + let attendance_id_value = entry_values + .get(ATTENDANCE_ID_FIELD_ID) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .ok_or_else(|| ApiError::bad_request("Ange ditt deltagar-ID från närvarolistan."))?; + + let attendance_id: i32 = attendance_id_value.parse().map_err(|_| { + ApiError::bad_request("Deltagar-ID måste vara ett heltal från närvarolistan.") + })?; + + let person = sqlx::query_as::<_, Person>( + r#" + SELECT + id, + first_name, + last_name, + grade, + parent_name, + parent_phone_number, + checked_in, + inside, + visitor, + sleeping_spot + FROM persons + WHERE id = $1 + "#, + ) + .bind(attendance_id) + .fetch_optional(&mut *tx) + .await? + .ok_or_else(|| { + ApiError::bad_request("Det angivna deltagar-ID:t finns inte i närvarolistan.") + })?; + + entry_values.insert( + ATTENDANCE_ID_FIELD_ID.to_string(), + attendance_id.to_string(), + ); + lead_participant = Some((attendance_id, person)); + } + + let (lead_attendance_id, lead_person) = lead_participant.ok_or_else(|| { + ApiError::bad_request("Minst en deltagare med deltagar-ID krävs för anmälan.") })?; - let person = sqlx::query_as::<_, Person>( - r#" - SELECT - id, - first_name, - last_name, - grade, - parent_name, - parent_phone_number, - checked_in, - inside, - visitor, - sleeping_spot - FROM persons - WHERE id = $1 - "#, - ) - .bind(attendance_id) - .fetch_optional(&mut *tx) - .await? - .ok_or_else(|| { - ApiError::bad_request("Det angivna deltagar-ID:t finns inte i närvarolistan.") - })?; - - let canonical_attendance = attendance_id.to_string(); - entry_values.insert( - ATTENDANCE_ID_FIELD_ID.to_string(), - canonical_attendance.clone(), - ); - - let entry_label = build_entry_label(attendance_id, &person, &config, &entry_values); + let entry_label = build_entry_label(lead_attendance_id, &lead_person, &config, &entry_values); for field in &config.entry_fields { if !field.unique { @@ -1656,46 +1731,122 @@ pub async fn update_registration_by_slug( )); } - let attendance_id_value = entry_values - .get(ATTENDANCE_ID_FIELD_ID) - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - .ok_or_else(|| ApiError::bad_request("Ange ditt deltagar-ID från närvarolistan."))?; + let is_team = config.mode == "team"; + let mut lead_participant: Option<(i32, Person)> = None; - let attendance_id: i32 = attendance_id_value.parse().map_err(|_| { - ApiError::bad_request("Deltagar-ID måste vara ett heltal från närvarolistan.") + if is_team { + entry_values.remove(ATTENDANCE_ID_FIELD_ID); + let mut seen_attendance_ids: HashSet = HashSet::new(); + + for (index, values) in participant_values.iter_mut().enumerate() { + let attendance_raw = values + .get(ATTENDANCE_ID_FIELD_ID) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + ApiError::bad_request(format!( + "Spelare #{number}: ange deltagar-ID från närvarolistan.", + number = index + 1 + )) + })?; + + let attendance_id: i32 = attendance_raw.parse().map_err(|_| { + ApiError::bad_request(format!( + "Spelare #{number}: deltagar-ID måste vara ett heltal från närvarolistan.", + number = index + 1 + )) + })?; + + if !seen_attendance_ids.insert(attendance_id) { + return Err(ApiError::bad_request(format!( + "Spelare #{number}: deltagar-ID används redan av en annan spelare i laget.", + number = index + 1 + ))); + } + + let person = sqlx::query_as::<_, Person>( + r#" + SELECT + id, + first_name, + last_name, + grade, + parent_name, + parent_phone_number, + checked_in, + inside, + visitor, + sleeping_spot + FROM persons + WHERE id = $1 + "#, + ) + .bind(attendance_id) + .fetch_optional(&mut *tx) + .await? + .ok_or_else(|| { + ApiError::bad_request(format!( + "Spelare #{number}: deltagar-ID:t finns inte i närvarolistan.", + number = index + 1 + )) + })?; + + values.insert( + ATTENDANCE_ID_FIELD_ID.to_string(), + attendance_id.to_string(), + ); + + if lead_participant.is_none() { + lead_participant = Some((attendance_id, person.clone())); + } + } + } else { + let attendance_id_value = entry_values + .get(ATTENDANCE_ID_FIELD_ID) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .ok_or_else(|| ApiError::bad_request("Ange ditt deltagar-ID från närvarolistan."))?; + + let attendance_id: i32 = attendance_id_value.parse().map_err(|_| { + ApiError::bad_request("Deltagar-ID måste vara ett heltal från närvarolistan.") + })?; + + let person = sqlx::query_as::<_, Person>( + r#" + SELECT + id, + first_name, + last_name, + grade, + parent_name, + parent_phone_number, + checked_in, + inside, + visitor, + sleeping_spot + FROM persons + WHERE id = $1 + "#, + ) + .bind(attendance_id) + .fetch_optional(&mut *tx) + .await? + .ok_or_else(|| { + ApiError::bad_request("Det angivna deltagar-ID:t finns inte i närvarolistan.") + })?; + + entry_values.insert( + ATTENDANCE_ID_FIELD_ID.to_string(), + attendance_id.to_string(), + ); + lead_participant = Some((attendance_id, person)); + } + + let (lead_attendance_id, lead_person) = lead_participant.ok_or_else(|| { + ApiError::bad_request("Minst en deltagare med deltagar-ID krävs för anmälan.") })?; - let person = sqlx::query_as::<_, Person>( - r#" - SELECT - id, - first_name, - last_name, - grade, - parent_name, - parent_phone_number, - checked_in, - inside, - visitor, - sleeping_spot - FROM persons - WHERE id = $1 - "#, - ) - .bind(attendance_id) - .fetch_optional(&mut *tx) - .await? - .ok_or_else(|| { - ApiError::bad_request("Det angivna deltagar-ID:t finns inte i närvarolistan.") - })?; - - entry_values.insert( - ATTENDANCE_ID_FIELD_ID.to_string(), - attendance_id.to_string(), - ); - - let entry_label = build_entry_label(attendance_id, &person, &config, &entry_values); + let entry_label = build_entry_label(lead_attendance_id, &lead_person, &config, &entry_values); for field in &config.entry_fields { if !field.unique { diff --git a/web/src/routes/(admin)/admin/tournament/+page.svelte b/web/src/routes/(admin)/admin/tournament/+page.svelte index 46dd15c..e324de7 100644 --- a/web/src/routes/(admin)/admin/tournament/+page.svelte +++ b/web/src/routes/(admin)/admin/tournament/+page.svelte @@ -130,69 +130,107 @@ }; } - function ensureAttendanceField(fields: SignupFieldForm[]): SignupFieldForm[] { - let attendanceField: SignupFieldForm | null = null; - const others: SignupFieldForm[] = []; + function sanitizeAttendanceField(field?: SignupFieldForm | null): SignupFieldForm { + const label = field?.label?.trim() || ATTENDANCE_FIELD_LABEL; + const placeholder = field?.placeholder?.trim() || ATTENDANCE_FIELD_PLACEHOLDER; + return { + id: ATTENDANCE_FIELD_ID, + label, + field_type: 'text', + required: true, + placeholder, + unique: true + }; + } - for (const field of fields) { - if (isAttendanceField(field)) { - const label = field.label.trim() || ATTENDANCE_FIELD_LABEL; - const placeholder = field.placeholder.trim() || ATTENDANCE_FIELD_PLACEHOLDER; - attendanceField = { - id: ATTENDANCE_FIELD_ID, - label, - field_type: 'text', - required: true, - placeholder, - unique: true - }; - } else { - others.push(field); + function removeAttendanceField(fields: SignupFieldForm[]): { + fields: SignupFieldForm[]; + attendance: SignupFieldForm | null; + } { + const index = fields.findIndex(isAttendanceField); + if (index === -1) { + return { fields: [...fields], attendance: null }; + } + + const next = [...fields]; + const [field] = next.splice(index, 1); + return { fields: next, attendance: sanitizeAttendanceField(field) }; + } + + function ensureAttendancePlacement(signup: SignupConfigForm) { + const entryResult = removeAttendanceField(signup.entry_fields); + const participantResult = removeAttendanceField(signup.participant_fields); + let attendance = + entryResult.attendance ?? participantResult.attendance ?? createAttendanceField(); + + if (signup.mode === 'team') { + const participantFields = [...participantResult.fields]; + if (!participantFields.some((field) => !isAttendanceField(field))) { + participantFields.push(createSignupField('Spelarinfo')); + } + signup.entry_fields = [...entryResult.fields]; + signup.participant_fields = [sanitizeAttendanceField(attendance), ...participantFields]; + } else { + const entryFields = [...entryResult.fields]; + if (!entryFields.some((field) => !isAttendanceField(field))) { + entryFields.push(createSignupField('Spelarnamn')); + } + signup.entry_fields = [sanitizeAttendanceField(attendance), ...entryFields]; + signup.participant_fields = participantResult.fields.filter( + (field) => !isAttendanceField(field) + ); + } + + const firstEntry = signup.entry_fields.find((field) => !isAttendanceField(field)); + if (firstEntry) { + const trimmed = firstEntry.placeholder.trim(); + firstEntry.placeholder = trimmed || (signup.mode === 'team' ? 'Lag namn' : 'Spelarnamn'); + if (signup.mode === 'team' && firstEntry.label.trim().length === 0) { + firstEntry.label = 'Lag'; } } - - if (!attendanceField) { - attendanceField = createAttendanceField(); - } - - return [attendanceField, ...others]; } function createDefaultSignup(): SignupConfigForm { - return { + const signup: SignupConfigForm = { mode: 'solo', team_size: { min: 1, max: 1 }, - entry_fields: ensureAttendanceField([createSignupField('Lag / spelarnamn')]), + entry_fields: [createSignupField('Spelarnamn')], participant_fields: [] }; + ensureAttendancePlacement(signup); + return signup; } function cloneSignupConfig(config: TournamentSignupConfig | null | undefined): SignupConfigForm { if (!config) return createDefaultSignup(); - let entry_fields = ensureAttendanceField((config.entry_fields ?? []).map(cloneSignupField)); - let participant_fields = (config.participant_fields ?? []).map(cloneSignupField); - if (entry_fields.length === 1) { - entry_fields = ensureAttendanceField([ - ...entry_fields, - createSignupField('Lag / spelarnamn') - ]); - } - if (participant_fields.length === 0) { - participant_fields.push(createSignupField('Spelare')); - } - const mode = config.mode === 'team' ? 'team' : 'solo'; - if (mode === 'solo') { - participant_fields = []; - } - return { - mode, + + const signup: SignupConfigForm = { + mode: config.mode === 'team' ? 'team' : 'solo', team_size: { min: config.team_size?.min ?? 1, max: config.team_size?.max ?? 1 }, - entry_fields, - participant_fields + entry_fields: (config.entry_fields ?? []).map(cloneSignupField), + participant_fields: (config.participant_fields ?? []).map(cloneSignupField) }; + + ensureAttendancePlacement(signup); + + if ( + signup.mode === 'team' && + !signup.participant_fields.some((field) => !isAttendanceField(field)) + ) { + signup.participant_fields = [...signup.participant_fields, createSignupField('Spelarinfo')]; + ensureAttendancePlacement(signup); + } + + if (signup.mode !== 'team' && !signup.entry_fields.some((field) => !isAttendanceField(field))) { + signup.entry_fields = [...signup.entry_fields, createSignupField('Spelarnamn')]; + ensureAttendancePlacement(signup); + } + + return signup; } function signupFieldKey(index: number, field?: SignupFieldForm) { @@ -201,17 +239,15 @@ } function addEntryField(form: TournamentForm) { - form.signup.entry_fields = ensureAttendanceField([ - ...form.signup.entry_fields, - createSignupField('Nytt fält') - ]); + form.signup.entry_fields = [...form.signup.entry_fields, createSignupField('Nytt fält')]; + ensureAttendancePlacement(form.signup); } function removeEntryField(form: TournamentForm, index: number) { const target = form.signup.entry_fields[index]; if (!target || isAttendanceField(target)) return; - const remaining = form.signup.entry_fields.filter((_, idx) => idx !== index); - form.signup.entry_fields = ensureAttendanceField(remaining); + form.signup.entry_fields = form.signup.entry_fields.filter((_, idx) => idx !== index); + ensureAttendancePlacement(form.signup); } function addParticipantField(form: TournamentForm) { @@ -219,13 +255,16 @@ ...form.signup.participant_fields, createSignupField('Spelarinfo') ]; + ensureAttendancePlacement(form.signup); } function removeParticipantField(form: TournamentForm, index: number) { - if (form.signup.participant_fields.length <= 1) return; + const target = form.signup.participant_fields[index]; + if (!target || isAttendanceField(target)) return; form.signup.participant_fields = form.signup.participant_fields.filter( (_, idx) => idx !== index ); + ensureAttendancePlacement(form.signup); } function setSignupMode(form: TournamentForm, mode: 'solo' | 'team') { @@ -243,6 +282,7 @@ form.signup.participant_fields = [createSignupField('Spelare')]; } } + ensureAttendancePlacement(form.signup); } function setTeamSize(form: TournamentForm, key: 'min' | 'max', value: string) { @@ -362,8 +402,17 @@ max = 1; } - const entry_fields = ensureAttendanceField(signup.entry_fields).map(normalizeField); - const participant_fields = mode === 'team' ? signup.participant_fields.map(normalizeField) : []; + const draft: SignupConfigForm = { + mode, + team_size: { min, max }, + entry_fields: signup.entry_fields.map((field) => ({ ...field })), + participant_fields: signup.participant_fields.map((field) => ({ ...field })) + }; + + ensureAttendancePlacement(draft); + + const entry_fields = draft.entry_fields.map(normalizeField); + const participant_fields = mode === 'team' ? draft.participant_fields.map(normalizeField) : []; return { mode, diff --git a/web/src/routes/(tournament)/tournament/[slug]/+page.svelte b/web/src/routes/(tournament)/tournament/[slug]/+page.svelte index c5276e9..fbf46e6 100644 --- a/web/src/routes/(tournament)/tournament/[slug]/+page.svelte +++ b/web/src/routes/(tournament)/tournament/[slug]/+page.svelte @@ -188,34 +188,58 @@ signup.error = ''; signup.success = ''; - const attendanceValue = (signup.entry[ATTENDANCE_FIELD_ID] ?? '').trim(); - if (!attendanceValue) { - signup.error = 'Ange ditt deltagar-ID från närvarolistan.'; - return; + const isTeam = signupConfig.mode === 'team'; + + if (isTeam) { + if (signup.participants.length < minParticipants) { + signup.error = + minParticipants === 1 + ? 'Lägg till minst en spelare.' + : `Lägg till minst ${minParticipants} spelare.`; + return; + } + + for (let index = 0; index < signup.participants.length; index += 1) { + const participant = signup.participants[index]; + const raw = (participant[ATTENDANCE_FIELD_ID] ?? '').trim(); + if (!raw) { + signup.error = `Spelare ${index + 1}: ange deltagar-ID från närvarolistan.`; + return; + } + if (!/^\d+$/.test(raw)) { + signup.error = `Spelare ${index + 1}: deltagar-ID får endast innehålla siffror.`; + return; + } + const numeric = Number.parseInt(raw, 10); + if (!Number.isFinite(numeric) || numeric <= 0) { + signup.error = `Spelare ${index + 1}: ange ett giltigt deltagar-ID.`; + return; + } + participant[ATTENDANCE_FIELD_ID] = String(numeric); + } + } else { + const attendanceValue = (signup.entry[ATTENDANCE_FIELD_ID] ?? '').trim(); + if (!attendanceValue) { + signup.error = 'Ange ditt deltagar-ID från närvarolistan.'; + return; + } + if (!/^\d+$/.test(attendanceValue)) { + signup.error = 'Deltagar-ID får endast innehålla siffror.'; + return; + } + const attendanceNumeric = Number.parseInt(attendanceValue, 10); + if (!Number.isFinite(attendanceNumeric) || attendanceNumeric <= 0) { + signup.error = 'Ange ett giltigt deltagar-ID.'; + return; + } + signup.entry[ATTENDANCE_FIELD_ID] = String(attendanceNumeric); } - if (!/^\d+$/.test(attendanceValue)) { - signup.error = 'Deltagar-ID får endast innehålla siffror.'; - return; - } - const attendanceNumeric = Number.parseInt(attendanceValue, 10); - if (!Number.isFinite(attendanceNumeric) || attendanceNumeric <= 0) { - signup.error = 'Ange ett giltigt deltagar-ID.'; - return; - } - signup.entry[ATTENDANCE_FIELD_ID] = String(attendanceNumeric); if (signup.participants.length === 0) { signup.error = 'Lägg till minst en spelare.'; return; } - if (signupConfig.mode === 'team') { - if (signup.participants.length < minParticipants) { - signup.error = `Lägg till minst ${minParticipants} spelare.`; - return; - } - } - signup.submitting = true; try { const payload = buildSignupPayload();