"swapping distro middle save"
This commit is contained in:
parent
89c6a5a340
commit
4e3be28cf3
17 changed files with 1723 additions and 290 deletions
|
|
@ -9,6 +9,12 @@ use rocket::serde::json::Json;
|
|||
use rocket::Route;
|
||||
use sqlx::QueryBuilder;
|
||||
|
||||
enum SearchCondition {
|
||||
Checked(bool),
|
||||
Inside(bool),
|
||||
Query(String),
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
rocket::routes![
|
||||
search_persons,
|
||||
|
|
@ -139,17 +145,13 @@ pub async fn list_checked_in(
|
|||
let mut conditions = Vec::new();
|
||||
|
||||
if let Some(checked_in) = checked {
|
||||
if checked_in {
|
||||
conditions.push("checked_in = true".to_string());
|
||||
} else {
|
||||
conditions.push("checked_in = false".to_string());
|
||||
}
|
||||
conditions.push(SearchCondition::Checked(checked_in));
|
||||
}
|
||||
|
||||
if let Some(status) = status {
|
||||
match status {
|
||||
"inside" => conditions.push("inside = true".to_string()),
|
||||
"outside" => conditions.push("inside = false".to_string()),
|
||||
"inside" => conditions.push(SearchCondition::Inside(true)),
|
||||
"outside" => conditions.push(SearchCondition::Inside(false)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,18 +164,51 @@ pub async fn list_checked_in(
|
|||
Some(trimmed.to_string())
|
||||
}
|
||||
}) {
|
||||
conditions.push(format!(
|
||||
"(CONCAT(first_name, ' ', last_name) ILIKE '%{query}%' OR parent_name ILIKE '%{query}%' OR parent_phone_number ILIKE '%{query}%')"
|
||||
));
|
||||
conditions.push(SearchCondition::Query(query));
|
||||
}
|
||||
|
||||
if !conditions.is_empty() {
|
||||
query_builder.push(" WHERE ");
|
||||
for (index, condition) in conditions.iter().enumerate() {
|
||||
if index > 0 {
|
||||
query_builder.push(" AND ");
|
||||
let mut first = true;
|
||||
for condition in &conditions {
|
||||
if first {
|
||||
query_builder.push(" WHERE ");
|
||||
first = false;
|
||||
} else {
|
||||
query_builder.push(" AND ");
|
||||
}
|
||||
|
||||
match condition {
|
||||
SearchCondition::Checked(flag) => {
|
||||
query_builder.push("checked_in = ");
|
||||
query_builder.push_bind(*flag);
|
||||
}
|
||||
SearchCondition::Inside(flag) => {
|
||||
query_builder.push("inside = ");
|
||||
query_builder.push_bind(*flag);
|
||||
}
|
||||
SearchCondition::Query(term) => {
|
||||
let like = format!("%{}%", term);
|
||||
let starts_with = format!("{}%", term);
|
||||
let digits: String = term.chars().filter(|ch| ch.is_ascii_digit()).collect();
|
||||
|
||||
query_builder.push("(");
|
||||
query_builder.push("CAST(id AS TEXT) ILIKE ");
|
||||
query_builder.push_bind(starts_with.clone());
|
||||
query_builder.push(" OR CONCAT(first_name, ' ', last_name) ILIKE ");
|
||||
query_builder.push_bind(like.clone());
|
||||
query_builder.push(" OR parent_name ILIKE ");
|
||||
query_builder.push_bind(like.clone());
|
||||
query_builder.push(" OR parent_phone_number ILIKE ");
|
||||
query_builder.push_bind(like.clone());
|
||||
|
||||
if !digits.is_empty() {
|
||||
let digits_like = format!("%{}%", digits);
|
||||
query_builder
|
||||
.push(" OR REGEXP_REPLACE(parent_phone_number, '[^0-9]', '', 'g') ILIKE ");
|
||||
query_builder.push_bind(digits_like);
|
||||
}
|
||||
|
||||
query_builder.push(")");
|
||||
}
|
||||
query_builder.push(condition.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -198,7 +233,8 @@ pub async fn checkin_person(
|
|||
let person = sqlx::query_as::<_, Person>(
|
||||
r#"
|
||||
UPDATE persons
|
||||
SET checked_in = true
|
||||
SET checked_in = true,
|
||||
inside = true
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use crate::models::{
|
|||
TournamentSignupFieldRecord, TournamentSignupSubmission, UpdateTournamentRequest,
|
||||
};
|
||||
use crate::AppState;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use serde_json::{Map, Value};
|
||||
|
|
@ -26,6 +27,8 @@ pub fn routes() -> Vec<Route> {
|
|||
delete_tournament,
|
||||
list_registrations_by_slug,
|
||||
get_registration_detail_by_slug,
|
||||
update_registration_by_slug,
|
||||
delete_registration_by_slug,
|
||||
create_registration_by_slug
|
||||
]
|
||||
}
|
||||
|
|
@ -104,6 +107,29 @@ fn build_registration_url(slug: &str) -> String {
|
|||
format!("/tournament/{slug}")
|
||||
}
|
||||
|
||||
fn signup_fields_equal(left: &[TournamentSignupField], right: &[TournamentSignupField]) -> bool {
|
||||
if left.len() != right.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
left.iter().zip(right.iter()).all(|(a, b)| {
|
||||
a.id == b.id
|
||||
&& a.label == b.label
|
||||
&& a.field_type == b.field_type
|
||||
&& a.required == b.required
|
||||
&& a.placeholder == b.placeholder
|
||||
&& a.unique == b.unique
|
||||
})
|
||||
}
|
||||
|
||||
fn signup_fields_changed(
|
||||
existing: &TournamentSignupConfig,
|
||||
updated: &TournamentSignupConfig,
|
||||
) -> bool {
|
||||
!signup_fields_equal(&existing.entry_fields, &updated.entry_fields)
|
||||
|| !signup_fields_equal(&existing.participant_fields, &updated.participant_fields)
|
||||
}
|
||||
|
||||
async fn load_sections(
|
||||
pool: &sqlx::PgPool,
|
||||
tournament_id: i32,
|
||||
|
|
@ -403,6 +429,191 @@ async fn insert_participant_values(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_registration_detail(
|
||||
pool: &sqlx::PgPool,
|
||||
info: TournamentInfo,
|
||||
registration: TournamentRegistrationRow,
|
||||
) -> Result<TournamentRegistrationDetailResponse, ApiError> {
|
||||
let entry_value_rows = sqlx::query_as::<_, TournamentRegistrationValueRow>(
|
||||
r#"
|
||||
SELECT registration_id, signup_field_id, value
|
||||
FROM tournament_registration_values
|
||||
WHERE registration_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(registration.id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let participant_rows = sqlx::query_as::<_, TournamentParticipantRow>(
|
||||
r#"
|
||||
SELECT id, registration_id, position
|
||||
FROM tournament_participants
|
||||
WHERE registration_id = $1
|
||||
ORDER BY position ASC, id ASC
|
||||
"#,
|
||||
)
|
||||
.bind(registration.id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let participant_value_rows = if participant_rows.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let participant_ids: Vec<i32> = participant_rows.iter().map(|row| row.id).collect();
|
||||
sqlx::query_as::<_, TournamentParticipantValueRow>(
|
||||
r#"
|
||||
SELECT participant_id, signup_field_id, value
|
||||
FROM tournament_participant_values
|
||||
WHERE participant_id = ANY($1)
|
||||
"#,
|
||||
)
|
||||
.bind(&participant_ids)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
};
|
||||
|
||||
let field_records = load_signup_field_records(pool, info.id).await?;
|
||||
let mut field_by_id: HashMap<i32, &TournamentSignupFieldRecord> = HashMap::new();
|
||||
for record in &field_records {
|
||||
field_by_id.insert(record.id, record);
|
||||
}
|
||||
|
||||
let mut entry_map = Map::new();
|
||||
for row in entry_value_rows {
|
||||
if let Some(field) = field_by_id.get(&row.signup_field_id) {
|
||||
if field.scope == "entry" {
|
||||
entry_map.insert(field.field_key.clone(), Value::String(row.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut participant_value_map: HashMap<i32, Map<String, Value>> = HashMap::new();
|
||||
for row in participant_value_rows {
|
||||
if let Some(field) = field_by_id.get(&row.signup_field_id) {
|
||||
if field.scope == "participant" {
|
||||
let map = participant_value_map
|
||||
.entry(row.participant_id)
|
||||
.or_insert_with(Map::new);
|
||||
map.insert(field.field_key.clone(), Value::String(row.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut participant_array = Vec::new();
|
||||
for participant in participant_rows {
|
||||
let values = participant_value_map
|
||||
.remove(&participant.id)
|
||||
.map(Value::Object)
|
||||
.unwrap_or_else(|| Value::Object(Map::new()));
|
||||
participant_array.push(values);
|
||||
}
|
||||
|
||||
let registration_item = TournamentRegistrationItem {
|
||||
id: registration.id,
|
||||
created_at: registration.created_at,
|
||||
entry: Value::Object(entry_map),
|
||||
participants: Value::Array(participant_array),
|
||||
};
|
||||
|
||||
let tournament = load_tournament_data(pool, info).await?;
|
||||
|
||||
Ok(TournamentRegistrationDetailResponse {
|
||||
tournament,
|
||||
registration: registration_item,
|
||||
})
|
||||
}
|
||||
|
||||
#[rocket::delete("/slug/<slug>/registrations/<registration_id>")]
|
||||
pub async fn delete_registration_by_slug(
|
||||
_user: AuthUser,
|
||||
state: &rocket::State<AppState>,
|
||||
slug: &str,
|
||||
registration_id: i32,
|
||||
) -> Result<Status, ApiError> {
|
||||
let info = sqlx::query_as::<_, TournamentInfo>(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
game,
|
||||
slug,
|
||||
tagline,
|
||||
start_at,
|
||||
location,
|
||||
description,
|
||||
contact,
|
||||
signup_mode,
|
||||
team_size_min,
|
||||
team_size_max,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM tournament_info
|
||||
WHERE slug = $1
|
||||
"#,
|
||||
)
|
||||
.bind(slug)
|
||||
.fetch_optional(&state.db)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::not_found("Turneringen hittades inte."))?;
|
||||
|
||||
let registration = sqlx::query_as::<_, TournamentRegistrationRow>(
|
||||
r#"
|
||||
SELECT id, tournament_id, entry_label, created_at
|
||||
FROM tournament_registrations
|
||||
WHERE id = $1 AND tournament_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(registration_id)
|
||||
.bind(info.id)
|
||||
.fetch_optional(&state.db)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::not_found("Anmälan hittades inte."))?;
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
|
||||
sqlx::query(
|
||||
"DELETE FROM tournament_participant_values WHERE participant_id IN (
|
||||
SELECT id FROM tournament_participants WHERE registration_id = $1
|
||||
)",
|
||||
)
|
||||
.bind(registration.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM tournament_participants WHERE registration_id = $1")
|
||||
.bind(registration.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM tournament_registration_values WHERE registration_id = $1")
|
||||
.bind(registration.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM tournament_registrations WHERE id = $1")
|
||||
.bind(registration.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
match load_tournament_data(&state.db, info).await {
|
||||
Ok(tournament) => {
|
||||
let _ = state
|
||||
.event_sender
|
||||
.send(AppEvent::TournamentUpserted { tournament });
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"Failed to load tournament data after deleting registration {registration_id}: {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Status::NoContent)
|
||||
}
|
||||
|
||||
#[rocket::get("/")]
|
||||
pub async fn list_tournaments(
|
||||
state: &rocket::State<AppState>,
|
||||
|
|
@ -619,6 +830,35 @@ pub async fn update_tournament(
|
|||
) -> Result<Json<TournamentItemResponse>, ApiError> {
|
||||
let request = payload.into_inner();
|
||||
|
||||
let existing_info = sqlx::query_as::<_, TournamentInfo>(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
game,
|
||||
slug,
|
||||
tagline,
|
||||
start_at,
|
||||
location,
|
||||
description,
|
||||
contact,
|
||||
signup_mode,
|
||||
team_size_min,
|
||||
team_size_max,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM tournament_info
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.db)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::not_found("Turneringen hittades inte."))?;
|
||||
|
||||
let existing_field_records = load_signup_field_records(&state.db, existing_info.id).await?;
|
||||
let existing_config = build_signup_config(&existing_info, &existing_field_records);
|
||||
|
||||
if request.title.trim().is_empty() {
|
||||
return Err(ApiError::bad_request("Turneringen måste ha en titel."));
|
||||
}
|
||||
|
|
@ -639,6 +879,8 @@ pub async fn update_tournament(
|
|||
let description = normalize_optional(request.description);
|
||||
let contact = normalize_optional(request.contact);
|
||||
|
||||
let fields_changed = signup_fields_changed(&existing_config, &signup_config);
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
|
||||
let info = sqlx::query_as::<_, TournamentInfo>(
|
||||
|
|
@ -700,7 +942,9 @@ pub async fn update_tournament(
|
|||
.ok_or_else(|| ApiError::not_found("Turneringen hittades inte."))?;
|
||||
|
||||
insert_sections(&mut tx, id, §ions).await?;
|
||||
insert_signup_fields(&mut tx, id, &signup_config).await?;
|
||||
if fields_changed {
|
||||
insert_signup_fields(&mut tx, id, &signup_config).await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
|
|
@ -1053,6 +1297,11 @@ pub async fn create_registration_by_slug(
|
|||
|
||||
tx.commit().await?;
|
||||
|
||||
let tournament_data = load_tournament_data(&state.db, info).await?;
|
||||
let _ = state.event_sender.send(AppEvent::TournamentUpserted {
|
||||
tournament: tournament_data,
|
||||
});
|
||||
|
||||
Ok(Json(TournamentRegistrationResponse {
|
||||
registration_id: registration.id,
|
||||
}))
|
||||
|
|
@ -1265,92 +1514,298 @@ pub async fn get_registration_detail_by_slug(
|
|||
.await?
|
||||
.ok_or_else(|| ApiError::not_found("Anmälan hittades inte."))?;
|
||||
|
||||
let entry_value_rows = sqlx::query_as::<_, TournamentRegistrationValueRow>(
|
||||
let detail = load_registration_detail(&state.db, info, registration).await?;
|
||||
|
||||
Ok(Json(detail))
|
||||
}
|
||||
|
||||
#[rocket::put("/slug/<slug>/registrations/<registration_id>", data = "<payload>")]
|
||||
pub async fn update_registration_by_slug(
|
||||
_user: AuthUser,
|
||||
state: &rocket::State<AppState>,
|
||||
slug: &str,
|
||||
registration_id: i32,
|
||||
payload: Json<TournamentSignupSubmission>,
|
||||
) -> Result<Json<TournamentRegistrationDetailResponse>, ApiError> {
|
||||
let info = sqlx::query_as::<_, TournamentInfo>(
|
||||
r#"
|
||||
SELECT registration_id, signup_field_id, value
|
||||
FROM tournament_registration_values
|
||||
WHERE registration_id = $1
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
game,
|
||||
slug,
|
||||
tagline,
|
||||
start_at,
|
||||
location,
|
||||
description,
|
||||
contact,
|
||||
signup_mode,
|
||||
team_size_min,
|
||||
team_size_max,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM tournament_info
|
||||
WHERE slug = $1
|
||||
"#,
|
||||
)
|
||||
.bind(registration.id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
.bind(slug)
|
||||
.fetch_optional(&state.db)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::not_found("Turneringen hittades inte."))?;
|
||||
|
||||
let participant_rows = sqlx::query_as::<_, TournamentParticipantRow>(
|
||||
let mut registration = sqlx::query_as::<_, TournamentRegistrationRow>(
|
||||
r#"
|
||||
SELECT id, registration_id, position
|
||||
FROM tournament_participants
|
||||
WHERE registration_id = $1
|
||||
ORDER BY position ASC, id ASC
|
||||
SELECT id, tournament_id, entry_label, created_at
|
||||
FROM tournament_registrations
|
||||
WHERE id = $1 AND tournament_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(registration.id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let participant_value_rows = if participant_rows.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let participant_ids: Vec<i32> = participant_rows.iter().map(|row| row.id).collect();
|
||||
sqlx::query_as::<_, TournamentParticipantValueRow>(
|
||||
r#"
|
||||
SELECT participant_id, signup_field_id, value
|
||||
FROM tournament_participant_values
|
||||
WHERE participant_id = ANY($1)
|
||||
"#,
|
||||
)
|
||||
.bind(&participant_ids)
|
||||
.fetch_all(&state.db)
|
||||
.await?
|
||||
};
|
||||
.bind(registration_id)
|
||||
.bind(info.id)
|
||||
.fetch_optional(&state.db)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::not_found("Anmälan hittades inte."))?;
|
||||
|
||||
let field_records = load_signup_field_records(&state.db, info.id).await?;
|
||||
let mut field_by_id: HashMap<i32, &TournamentSignupFieldRecord> = HashMap::new();
|
||||
for record in &field_records {
|
||||
field_by_id.insert(record.id, record);
|
||||
let config = build_signup_config(&info, &field_records);
|
||||
let submission = payload.into_inner();
|
||||
|
||||
validate_submission(&config, &submission)?;
|
||||
|
||||
let mut entry_values: HashMap<String, String> = HashMap::new();
|
||||
for field in &config.entry_fields {
|
||||
let value = submission
|
||||
.entry
|
||||
.get(&field.id)
|
||||
.map(|v| v.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
entry_values.insert(field.id.clone(), value);
|
||||
}
|
||||
|
||||
let mut entry_map = Map::new();
|
||||
for row in entry_value_rows {
|
||||
if let Some(field) = field_by_id.get(&row.signup_field_id) {
|
||||
if field.scope == "entry" {
|
||||
entry_map.insert(field.field_key.clone(), Value::String(row.value));
|
||||
let mut participant_values: Vec<HashMap<String, String>> = Vec::new();
|
||||
for participant in submission.participants {
|
||||
let mut map = HashMap::new();
|
||||
for field in &config.participant_fields {
|
||||
let value = participant
|
||||
.get(&field.id)
|
||||
.map(|v| v.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
map.insert(field.id.clone(), value);
|
||||
}
|
||||
participant_values.push(map);
|
||||
}
|
||||
|
||||
let mut field_map: HashMap<String, TournamentSignupFieldRecord> = HashMap::new();
|
||||
for record in field_records {
|
||||
field_map.insert(record.field_key.clone(), record);
|
||||
}
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
|
||||
let entry_label = config
|
||||
.entry_fields
|
||||
.first()
|
||||
.and_then(|field| entry_values.get(&field.id))
|
||||
.and_then(|value| trimmed(Some(value)));
|
||||
|
||||
let entry_label_requires_unique = config
|
||||
.entry_fields
|
||||
.first()
|
||||
.map(|field| field.unique)
|
||||
.unwrap_or(false);
|
||||
|
||||
if entry_label_requires_unique {
|
||||
if let Some(label) = entry_label.clone() {
|
||||
let is_duplicate = sqlx::query_scalar::<_, bool>(
|
||||
r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM tournament_registrations
|
||||
WHERE tournament_id = $1
|
||||
AND entry_label = $2
|
||||
AND id <> $3
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(info.id)
|
||||
.bind(&label)
|
||||
.bind(registration.id)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
if is_duplicate {
|
||||
return Err(ApiError::bad_request(
|
||||
"Den här spelaren eller laget är redan anmäld till turneringen.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut participant_value_map: HashMap<i32, Map<String, Value>> = HashMap::new();
|
||||
for row in participant_value_rows {
|
||||
if let Some(field) = field_by_id.get(&row.signup_field_id) {
|
||||
if field.scope == "participant" {
|
||||
let map = participant_value_map
|
||||
.entry(row.participant_id)
|
||||
.or_insert_with(Map::new);
|
||||
map.insert(field.field_key.clone(), Value::String(row.value));
|
||||
for field in &config.entry_fields {
|
||||
if !field.unique {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = entry_values.get(&field.id) {
|
||||
if value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(record) = field_map.get(&field.id) {
|
||||
let exists = sqlx::query_scalar::<_, bool>(
|
||||
r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM tournament_registration_values v
|
||||
INNER JOIN tournament_registrations r ON r.id = v.registration_id
|
||||
WHERE r.tournament_id = $1
|
||||
AND v.signup_field_id = $2
|
||||
AND v.value = $3
|
||||
AND r.id <> $4
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(info.id)
|
||||
.bind(record.id)
|
||||
.bind(value)
|
||||
.bind(registration.id)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
if exists {
|
||||
return Err(ApiError::bad_request(format!(
|
||||
"Värdet för '{label}' används redan i en annan anmälan.",
|
||||
label = field.label
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut participant_array = Vec::new();
|
||||
for participant in participant_rows {
|
||||
let values = participant_value_map
|
||||
.remove(&participant.id)
|
||||
.map(Value::Object)
|
||||
.unwrap_or_else(|| Value::Object(Map::new()));
|
||||
participant_array.push(values);
|
||||
for participant_values_map in &participant_values {
|
||||
for field in &config.participant_fields {
|
||||
if !field.unique {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = participant_values_map.get(&field.id) {
|
||||
if value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(record) = field_map.get(&field.id) {
|
||||
let exists = sqlx::query_scalar::<_, bool>(
|
||||
r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM tournament_participant_values pv
|
||||
INNER JOIN tournament_participants tp ON tp.id = pv.participant_id
|
||||
INNER JOIN tournament_registrations tr ON tr.id = tp.registration_id
|
||||
WHERE tr.tournament_id = $1
|
||||
AND pv.signup_field_id = $2
|
||||
AND pv.value = $3
|
||||
AND tr.id <> $4
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(info.id)
|
||||
.bind(record.id)
|
||||
.bind(value)
|
||||
.bind(registration.id)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
if exists {
|
||||
return Err(ApiError::bad_request(format!(
|
||||
"Värdet för '{label}' används redan i en annan anmälan.",
|
||||
label = field.label
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let registration_item = TournamentRegistrationItem {
|
||||
id: registration.id,
|
||||
created_at: registration.created_at,
|
||||
entry: Value::Object(entry_map),
|
||||
participants: Value::Array(participant_array),
|
||||
};
|
||||
sqlx::query("DELETE FROM tournament_registration_values WHERE registration_id = $1")
|
||||
.bind(registration.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let tournament = load_tournament_data(&state.db, info.clone()).await?;
|
||||
sqlx::query(
|
||||
"DELETE FROM tournament_participant_values WHERE participant_id IN (
|
||||
SELECT id FROM tournament_participants WHERE registration_id = $1
|
||||
)",
|
||||
)
|
||||
.bind(registration.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
Ok(Json(TournamentRegistrationDetailResponse {
|
||||
tournament,
|
||||
registration: registration_item,
|
||||
}))
|
||||
sqlx::query("DELETE FROM tournament_participants WHERE registration_id = $1")
|
||||
.bind(registration.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE tournament_registrations
|
||||
SET entry_label = $1
|
||||
WHERE id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(entry_label)
|
||||
.bind(registration.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for field in &config.entry_fields {
|
||||
if let Some(record) = field_map.get(&field.id) {
|
||||
if let Some(value) = entry_values.get(&field.id) {
|
||||
insert_registration_values(&mut tx, registration.id, record, value).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (position, values) in participant_values.iter().enumerate() {
|
||||
let participant = sqlx::query_as::<_, TournamentParticipantRow>(
|
||||
r#"
|
||||
INSERT INTO tournament_participants (registration_id, position)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, registration_id, position
|
||||
"#,
|
||||
)
|
||||
.bind(registration.id)
|
||||
.bind(position as i32)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for field in &config.participant_fields {
|
||||
if let Some(record) = field_map.get(&field.id) {
|
||||
if let Some(value) = values.get(&field.id) {
|
||||
insert_participant_values(&mut tx, participant.id, record, value).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
registration = sqlx::query_as::<_, TournamentRegistrationRow>(
|
||||
r#"
|
||||
SELECT id, tournament_id, entry_label, created_at
|
||||
FROM tournament_registrations
|
||||
WHERE id = $1 AND tournament_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(registration_id)
|
||||
.bind(info.id)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
let detail = load_registration_detail(&state.db, info, registration).await?;
|
||||
|
||||
let _ = state.event_sender.send(AppEvent::TournamentUpserted {
|
||||
tournament: detail.tournament.clone(),
|
||||
});
|
||||
|
||||
Ok(Json(detail))
|
||||
}
|
||||
|
|
|
|||
128
web/src/lib/client/person-search.ts
Normal file
128
web/src/lib/client/person-search.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import type { Person } from '$lib/types';
|
||||
|
||||
type NormalizedQuery = {
|
||||
text: string;
|
||||
digits: string;
|
||||
numericId: number | null;
|
||||
};
|
||||
|
||||
function normalizeQuery(raw: string): NormalizedQuery {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return { text: '', digits: '', numericId: null };
|
||||
}
|
||||
|
||||
const numericId = /^\d+$/.test(trimmed) ? Number.parseInt(trimmed, 10) : null;
|
||||
const lower = trimmed.toLowerCase();
|
||||
const digits = trimmed.replace(/\D/g, '');
|
||||
|
||||
return {
|
||||
text: lower,
|
||||
digits,
|
||||
numericId
|
||||
};
|
||||
}
|
||||
|
||||
export function personMatchesQuery(person: Person, rawQuery: string): boolean {
|
||||
const query = normalizeQuery(rawQuery);
|
||||
if (!query.text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (query.numericId !== null && person.id === query.numericId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const idString = person.id.toString();
|
||||
if (idString.includes(query.text)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const fullName = `${person.first_name} ${person.last_name}`.toLowerCase();
|
||||
if (fullName.includes(query.text)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const parentName = person.parent_name?.toLowerCase() ?? '';
|
||||
if (parentName && parentName.includes(query.text)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (query.digits) {
|
||||
const phoneDigits = (person.parent_phone_number ?? '').replace(/\D/g, '');
|
||||
if (phoneDigits.includes(query.digits)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (query.text && person.grade !== null && query.text === String(person.grade)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function scorePerson(person: Person, query: NormalizedQuery): number {
|
||||
if (!query.text) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let score = 0;
|
||||
|
||||
if (query.numericId !== null && person.id === query.numericId) {
|
||||
return 1_000;
|
||||
}
|
||||
|
||||
const idString = person.id.toString();
|
||||
if (idString.startsWith(query.text)) {
|
||||
score += 180;
|
||||
} else if (idString.includes(query.text)) {
|
||||
score += 140;
|
||||
}
|
||||
|
||||
const fullName = `${person.first_name} ${person.last_name}`.toLowerCase();
|
||||
if (fullName.includes(query.text)) {
|
||||
score += 120;
|
||||
}
|
||||
|
||||
const parentName = person.parent_name?.toLowerCase() ?? '';
|
||||
if (parentName && parentName.includes(query.text)) {
|
||||
score += 100;
|
||||
}
|
||||
|
||||
if (query.digits) {
|
||||
const phoneDigits = (person.parent_phone_number ?? '').replace(/\D/g, '');
|
||||
if (phoneDigits.includes(query.digits)) {
|
||||
score += 80;
|
||||
}
|
||||
}
|
||||
|
||||
if (person.grade !== null && query.text === String(person.grade)) {
|
||||
score += 40;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
export function sortPersonsByQuery(list: Person[], rawQuery: string): Person[] {
|
||||
const query = normalizeQuery(rawQuery);
|
||||
if (!query.text) {
|
||||
return [...list];
|
||||
}
|
||||
|
||||
return [...list].sort((a, b) => {
|
||||
const scoreA = scorePerson(a, query);
|
||||
const scoreB = scorePerson(b, query);
|
||||
if (scoreA !== scoreB) {
|
||||
return scoreB - scoreA;
|
||||
}
|
||||
return a.id - b.id;
|
||||
});
|
||||
}
|
||||
|
||||
export function filterPersonsByQuery(list: Person[], rawQuery: string): Person[] {
|
||||
if (!rawQuery.trim()) {
|
||||
return [...list];
|
||||
}
|
||||
return list.filter((person) => personMatchesQuery(person, rawQuery));
|
||||
}
|
||||
|
|
@ -9,6 +9,10 @@
|
|||
isLowerGrade,
|
||||
personIsComplete
|
||||
} from '$lib/client/person-utils';
|
||||
import {
|
||||
personMatchesQuery,
|
||||
sortPersonsByQuery
|
||||
} from '$lib/client/person-search';
|
||||
import EditPersonModal from '$lib/components/edit-person-modal.svelte';
|
||||
|
||||
let searchQuery = $state('');
|
||||
|
|
@ -158,8 +162,11 @@
|
|||
}
|
||||
|
||||
function updateVisibleResults() {
|
||||
const filtered = searchResults.filter((person) => !person.checked_in);
|
||||
visibleResults = filtered;
|
||||
const query = searchQuery.trim();
|
||||
const filtered = searchResults.filter(
|
||||
(person) => !person.checked_in && (!query || personMatchesQuery(person, query))
|
||||
);
|
||||
visibleResults = query ? sortPersonsByQuery(filtered, query) : filtered;
|
||||
|
||||
if (searchResults.length === 0) {
|
||||
searchInfo = 'Ingen träff på sökningen.';
|
||||
|
|
@ -179,6 +186,7 @@
|
|||
stop();
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { listenToPersonEvents } from '$lib/client/person-events';
|
||||
import { updateCollection } from '$lib/client/person-collection';
|
||||
import { gradeLabel, guardianLabel, isLowerGrade } from '$lib/client/person-utils';
|
||||
import { personMatchesQuery, sortPersonsByQuery } from '$lib/client/person-search';
|
||||
import EditPersonModal from '$lib/components/edit-person-modal.svelte';
|
||||
|
||||
type StatusFilter = 'all' | 'inside' | 'outside';
|
||||
|
|
@ -49,22 +50,16 @@ type VisitorFilter = 'all' | 'besoksplats' | 'lanplats';
|
|||
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.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;
|
||||
}
|
||||
if (sleepingFilter === 'not-needed' && person.sleeping_spot) return false;
|
||||
const trimmed = searchQuery.trim();
|
||||
if (trimmed && !personMatchesQuery(person, trimmed)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateVisiblePersons() {
|
||||
const filtered = allPersons.filter((person) => matchesFilters(person));
|
||||
persons = filtered;
|
||||
const query = searchQuery.trim();
|
||||
persons = query ? sortPersonsByQuery(filtered, query) : filtered;
|
||||
infoMessage = filtered.length === 0 ? 'Inga personer matchar kriterierna.' : '';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { listenToPersonEvents } from '$lib/client/person-events';
|
||||
import { gradeLabel, guardianLabel, isLowerGrade } from '$lib/client/person-utils';
|
||||
import { personMatchesQuery, sortPersonsByQuery } from '$lib/client/person-search';
|
||||
|
||||
type GradeFilter = 'all' | 'lt4' | 'ge4';
|
||||
type VisitorFilter = 'all' | 'besoksplats' | 'lanplats';
|
||||
|
|
@ -42,17 +43,9 @@
|
|||
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;
|
||||
}
|
||||
const query = searchQuery.trim();
|
||||
if (query && !personMatchesQuery(person, query)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -176,7 +169,8 @@
|
|||
|
||||
function updateVisibleResults() {
|
||||
const filtered = searchResults.filter((person) => matchesFilters(person));
|
||||
visibleResults = filtered;
|
||||
const query = searchQuery.trim();
|
||||
visibleResults = query ? sortPersonsByQuery(filtered, query) : filtered;
|
||||
|
||||
if (searchResults.length === 0 && !searchQuery.trim()) {
|
||||
searchInfo = 'Inga personer hämtades.';
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { listenToPersonEvents } from '$lib/client/person-events';
|
||||
import { updateCollection } from '$lib/client/person-collection';
|
||||
import { gradeLabel, guardianLabel, isLowerGrade } from '$lib/client/person-utils';
|
||||
import { personMatchesQuery, sortPersonsByQuery } from '$lib/client/person-search';
|
||||
|
||||
type StatusFilter = 'all' | 'inside' | 'outside';
|
||||
|
||||
|
|
@ -33,21 +34,15 @@
|
|||
if (!person.checked_in) return false;
|
||||
if (statusFilter === 'inside' && !person.inside) return false;
|
||||
if (statusFilter === 'outside' && person.inside) return false;
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
if (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;
|
||||
}
|
||||
const query = searchQuery.trim();
|
||||
if (query && !personMatchesQuery(person, query)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyFilteredList(list: Person[]) {
|
||||
const filtered = list.filter(matchesFilters);
|
||||
persons = filtered;
|
||||
const query = searchQuery.trim();
|
||||
persons = query ? sortPersonsByQuery(filtered, query) : filtered;
|
||||
if (persons.length === 0) {
|
||||
infoMessage = 'Inga incheckade personer matchar kriterierna.';
|
||||
} else {
|
||||
|
|
@ -57,6 +52,10 @@
|
|||
|
||||
function handlePersonUpdate(person: Person) {
|
||||
persons = updateCollection(persons, person, matchesFilters);
|
||||
const query = searchQuery.trim();
|
||||
if (query) {
|
||||
persons = sortPersonsByQuery(persons, query);
|
||||
}
|
||||
if (persons.length === 0) {
|
||||
infoMessage = 'Inga incheckade personer matchar kriterierna.';
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import type { TournamentRegistrationList } from '$lib/types';
|
||||
import { ApiRequestError, fetchRegistrationList, parseApiMessage } from './helpers.server';
|
||||
|
||||
const loadErrorFallback = 'Kunde inte hämta anmälningar.';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, params }) => {
|
||||
const response = await fetch(`/api/tournament/slug/${params.slug}/registrations`);
|
||||
const text = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Kunde inte hämta anmälningar.';
|
||||
try {
|
||||
const body = JSON.parse(text);
|
||||
message = body.message ?? message;
|
||||
} catch {
|
||||
if (text) message = text;
|
||||
try {
|
||||
return await fetchRegistrationList(fetch, params.slug);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiRequestError) {
|
||||
const message = parseApiMessage(err.body, loadErrorFallback);
|
||||
throw error(err.status, message);
|
||||
}
|
||||
throw error(response.status, message);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const data = JSON.parse(text) as TournamentRegistrationList;
|
||||
return {
|
||||
tournament: data.tournament,
|
||||
registrations: data.registrations
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,107 @@
|
|||
|
||||
<script lang="ts">
|
||||
import type { TournamentRegistrationList, TournamentSignupField } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { listenToTournamentEvents } from '$lib/client/tournament-events';
|
||||
import type {
|
||||
TournamentRegistrationList,
|
||||
TournamentRegistrationItem,
|
||||
TournamentSignupField
|
||||
} from '$lib/types';
|
||||
|
||||
const props = $props<{ data: TournamentRegistrationList }>();
|
||||
const data = props.data;
|
||||
const tournament = data.tournament;
|
||||
const registrations = data.registrations;
|
||||
let tournament = $state(props.data.tournament);
|
||||
let registrations = $state(props.data.registrations ?? []);
|
||||
let refreshing = $state(false);
|
||||
let loadError = $state('');
|
||||
|
||||
const entryFields = tournament.signup_config.entry_fields ?? [];
|
||||
const participantFields = tournament.signup_config.participant_fields ?? [];
|
||||
type RegistrationResponse = {
|
||||
tournament: TournamentRegistrationList['tournament'];
|
||||
registrations: TournamentRegistrationItem[];
|
||||
};
|
||||
|
||||
type RegistrationPayload = RegistrationResponse & { warning?: string };
|
||||
|
||||
function applyResult(result: RegistrationResponse) {
|
||||
tournament = result.tournament;
|
||||
registrations = result.registrations ?? [];
|
||||
}
|
||||
|
||||
async function callEndpoint(
|
||||
endpoint: string,
|
||||
payload: unknown,
|
||||
defaultMessage: string
|
||||
): Promise<RegistrationPayload> {
|
||||
if (!tournament.slug) {
|
||||
throw new Error('Turneringen saknar slug.');
|
||||
}
|
||||
|
||||
const init: RequestInit = { method: 'POST' };
|
||||
if (payload !== undefined) {
|
||||
init.headers = { 'content-type': 'application/json' };
|
||||
init.body = JSON.stringify(payload);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/admin/tournament/${tournament.slug}/registrations/${endpoint}`,
|
||||
init
|
||||
);
|
||||
const text = await response.text();
|
||||
const trimmed = text.trim();
|
||||
|
||||
if (!response.ok) {
|
||||
let message = defaultMessage;
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed?.message) {
|
||||
message = parsed.message;
|
||||
} else if (trimmed) {
|
||||
message = trimmed;
|
||||
}
|
||||
} catch {
|
||||
if (trimmed) {
|
||||
message = trimmed;
|
||||
}
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
if (!trimmed) {
|
||||
throw new Error('Tomt svar från servern.');
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(text) as RegistrationPayload;
|
||||
if (
|
||||
!data ||
|
||||
typeof data !== 'object' ||
|
||||
!('tournament' in data) ||
|
||||
!('registrations' in data)
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
return data;
|
||||
} catch {
|
||||
throw new Error('Kunde inte tolka svaret från servern.');
|
||||
}
|
||||
}
|
||||
|
||||
let editingId = $state<number | null>(null);
|
||||
let editEntry = $state<Record<string, string>>({});
|
||||
let editParticipants = $state<Record<string, string>[]>([]);
|
||||
let editError = $state('');
|
||||
let editSaving = $state(false);
|
||||
let deletingId = $state<number | null>(null);
|
||||
let deleteError = $state('');
|
||||
|
||||
const entryFields = $derived(() => tournament.signup_config.entry_fields ?? []);
|
||||
const participantFields = $derived(() => tournament.signup_config.participant_fields ?? []);
|
||||
const signupConfig = $derived(() => tournament.signup_config);
|
||||
const minParticipants = $derived(() =>
|
||||
signupConfig().mode === 'team' ? Math.max(1, signupConfig().team_size.min) : 0
|
||||
);
|
||||
const maxParticipants = $derived(() =>
|
||||
signupConfig().mode === 'team' ? Math.max(1, signupConfig().team_size.max) : 0
|
||||
);
|
||||
|
||||
function formatDateTime(value: string | null) {
|
||||
if (!value) return null;
|
||||
|
|
@ -27,6 +121,243 @@
|
|||
const value = map[field.id];
|
||||
return value ?? '';
|
||||
}
|
||||
|
||||
function blankEntryMap() {
|
||||
const map: Record<string, string> = {};
|
||||
for (const field of entryFields()) {
|
||||
map[field.id] = '';
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function blankParticipantMap() {
|
||||
const map: Record<string, string> = {};
|
||||
for (const field of participantFields()) {
|
||||
map[field.id] = '';
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function fieldInputType(fieldType: string) {
|
||||
switch (fieldType) {
|
||||
case 'email':
|
||||
return 'email';
|
||||
case 'tel':
|
||||
return 'tel';
|
||||
default:
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
|
||||
function ensureParticipantBounds(list: Record<string, string>[]) {
|
||||
if (signupConfig().mode !== 'team') {
|
||||
return list;
|
||||
}
|
||||
let next = [...list];
|
||||
const min = minParticipants();
|
||||
const max = Math.max(min, maxParticipants());
|
||||
if (next.length < min) {
|
||||
for (let i = next.length; i < min; i += 1) {
|
||||
next = [...next, blankParticipantMap()];
|
||||
}
|
||||
}
|
||||
if (next.length > max) {
|
||||
next = next.slice(0, max);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function beginEdit(registration: TournamentRegistrationItem) {
|
||||
editingId = registration.id;
|
||||
editEntry = {
|
||||
...blankEntryMap(),
|
||||
...registration.entry
|
||||
};
|
||||
editParticipants = ensureParticipantBounds(
|
||||
(registration.participants ?? []).map((participant) => ({
|
||||
...blankParticipantMap(),
|
||||
...participant
|
||||
}))
|
||||
);
|
||||
editError = '';
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingId = null;
|
||||
editEntry = {};
|
||||
editParticipants = [];
|
||||
editError = '';
|
||||
editSaving = false;
|
||||
}
|
||||
|
||||
function updateEntryField(fieldId: string, value: string) {
|
||||
editEntry = { ...editEntry, [fieldId]: value };
|
||||
}
|
||||
|
||||
function updateParticipantField(index: number, fieldId: string, value: string) {
|
||||
editParticipants = editParticipants.map((participant, idx) =>
|
||||
idx === index ? { ...participant, [fieldId]: value } : participant
|
||||
);
|
||||
}
|
||||
|
||||
function addParticipant() {
|
||||
if (signupConfig().mode !== 'team') return;
|
||||
if (maxParticipants() > 0 && editParticipants.length >= maxParticipants()) return;
|
||||
editParticipants = [...editParticipants, blankParticipantMap()];
|
||||
}
|
||||
|
||||
function removeParticipant(index: number) {
|
||||
if (signupConfig().mode !== 'team') return;
|
||||
const min = minParticipants();
|
||||
if (editParticipants.length <= Math.max(1, min)) return;
|
||||
editParticipants = editParticipants.filter((_, idx) => idx !== index);
|
||||
}
|
||||
|
||||
function participantLabel(index: number) {
|
||||
if (signupConfig().mode === 'team') {
|
||||
return `Spelare ${index + 1}`;
|
||||
}
|
||||
return 'Spelare';
|
||||
}
|
||||
|
||||
async function saveEdit(registration: TournamentRegistrationItem) {
|
||||
if (!tournament.slug || editingId !== registration.id) return;
|
||||
editSaving = true;
|
||||
editError = '';
|
||||
const payload = {
|
||||
entry: { ...editEntry },
|
||||
participants: editParticipants.map((participant) => ({ ...participant }))
|
||||
};
|
||||
try {
|
||||
const result = await callEndpoint(
|
||||
'update',
|
||||
{
|
||||
registration_id: registration.id,
|
||||
entry: payload.entry,
|
||||
participants: payload.participants
|
||||
},
|
||||
'Kunde inte uppdatera anmälan.'
|
||||
);
|
||||
applyResult(result);
|
||||
loadError = result.warning ?? '';
|
||||
cancelEdit();
|
||||
} catch (err) {
|
||||
console.error('Failed to update registration', err);
|
||||
editError = err instanceof Error ? err.message : 'Ett oväntat fel inträffade.';
|
||||
} finally {
|
||||
editSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function exportRegistrations() {
|
||||
if (typeof window === 'undefined') return;
|
||||
const lines: string[] = [];
|
||||
const timestamp = formatDateTime(new Date().toISOString()) ?? new Date().toISOString();
|
||||
lines.push(`${tournament.title} – ${registrations.length} anmälningar`);
|
||||
lines.push(`Genererad ${timestamp}`);
|
||||
lines.push('');
|
||||
|
||||
if (registrations.length === 0) {
|
||||
lines.push('Inga anmälningar.');
|
||||
} else {
|
||||
for (const registration of registrations) {
|
||||
lines.push(`Anmälan #${registration.id}`);
|
||||
const created = formatDateTime(registration.created_at) ?? registration.created_at;
|
||||
lines.push(`Skapad: ${created}`);
|
||||
|
||||
if (entryFields().length > 0) {
|
||||
lines.push('Lag / deltagare:');
|
||||
for (const field of entryFields()) {
|
||||
lines.push(` ${field.label}: ${fieldValue(registration.entry, field) || '—'}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (participantFields().length === 0) {
|
||||
if (registration.participants.length > 0) {
|
||||
lines.push(`Spelare: ${registration.participants.length}`);
|
||||
}
|
||||
} else if (registration.participants.length === 0) {
|
||||
lines.push('Spelare: inga angivna');
|
||||
} else {
|
||||
lines.push('Spelare:');
|
||||
registration.participants.forEach((participant, index) => {
|
||||
lines.push(` Spelare ${index + 1}`);
|
||||
for (const field of participantFields()) {
|
||||
lines.push(` ${field.label}: ${fieldValue(participant, field) || '—'}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${tournament.slug?.trim() || 'turnering'}-registreringar.txt`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function removeRegistration(registration: TournamentRegistrationItem) {
|
||||
if (!tournament.slug) return;
|
||||
if (!window.confirm('Ta bort denna anmälan? Det går inte att ångra.')) return;
|
||||
deletingId = registration.id;
|
||||
deleteError = '';
|
||||
try {
|
||||
const result = await callEndpoint(
|
||||
'delete',
|
||||
{ registration_id: registration.id },
|
||||
'Kunde inte ta bort anmälan.'
|
||||
);
|
||||
applyResult(result);
|
||||
loadError = result.warning ?? '';
|
||||
if (editingId === registration.id) {
|
||||
cancelEdit();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete registration', err);
|
||||
deleteError = err instanceof Error ? err.message : 'Ett oväntat fel inträffade.';
|
||||
} finally {
|
||||
deletingId = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRegistrations() {
|
||||
if (!tournament.slug) return;
|
||||
refreshing = true;
|
||||
loadError = '';
|
||||
try {
|
||||
const result = await callEndpoint('refresh', undefined, 'Kunde inte hämta anmälningarna.');
|
||||
applyResult(result);
|
||||
loadError = result.warning ?? '';
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh registrations', err);
|
||||
loadError = err instanceof Error ? err.message : 'Ett oväntat fel inträffade.';
|
||||
} finally {
|
||||
refreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const stop = listenToTournamentEvents((updated) => {
|
||||
if (updated.id === tournament.id && editingId === null) {
|
||||
void refreshRegistrations();
|
||||
}
|
||||
}, (deletedId) => {
|
||||
if (deletedId === tournament.id) {
|
||||
registrations = [];
|
||||
loadError = 'Turneringen har tagits bort.';
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -73,21 +404,40 @@
|
|||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||
<p class="text-xs uppercase tracking-wide text-slate-500">Format</p>
|
||||
<p class="mt-1 text-sm text-slate-800">
|
||||
{tournament.signup_config.mode === 'team'
|
||||
? `Lag (${tournament.signup_config.team_size.min}–${tournament.signup_config.team_size.max} spelare)`
|
||||
{signupConfig().mode === 'team'
|
||||
? `Lag (${signupConfig().team_size.min}–${signupConfig().team_size.max} spelare)`
|
||||
: 'Individuell'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
|
||||
<header class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Registreringar</h2>
|
||||
{#if registrations.length > 0}
|
||||
<p class="text-sm text-slate-500">Senaste: {formatDateTime(registrations[0].created_at) ?? registrations[0].created_at}</p>
|
||||
{/if}
|
||||
</header>
|
||||
<section class="rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
|
||||
<header class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Registreringar</h2>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{#if registrations.length > 0}
|
||||
<p class="text-sm text-slate-500">Senaste: {formatDateTime(registrations[0].created_at) ?? registrations[0].created_at}</p>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
onclick={exportRegistrations}
|
||||
class="rounded-full border border-slate-300 px-4 py-2 text-xs font-semibold uppercase tracking-wide text-slate-600 transition hover:bg-slate-100"
|
||||
>
|
||||
Exportera .txt
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if loadError}
|
||||
<p class="mt-2 rounded-md border border-red-200 bg-red-500/10 px-4 py-2 text-sm text-red-600">{loadError}</p>
|
||||
{/if}
|
||||
{#if deleteError}
|
||||
<p class="mt-2 rounded-md border border-red-200 bg-red-500/10 px-4 py-2 text-sm text-red-600">{deleteError}</p>
|
||||
{/if}
|
||||
{#if refreshing && !loadError}
|
||||
<p class="mt-2 text-xs text-slate-500">Uppdaterar…</p>
|
||||
{/if}
|
||||
|
||||
{#if registrations.length === 0}
|
||||
<p class="mt-4 rounded-md border border-dashed border-slate-300 px-4 py-6 text-center text-sm text-slate-500">
|
||||
|
|
@ -97,52 +447,188 @@
|
|||
<div class="mt-6 space-y-5">
|
||||
{#each registrations as registration}
|
||||
<article class="space-y-4 rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||
<header class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h3 class="text-base font-semibold text-slate-900">Anmälan #{registration.id}</h3>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-500">
|
||||
Skapad {formatDateTime(registration.created_at) ?? registration.created_at}
|
||||
</p>
|
||||
<header class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-slate-900">Anmälan #{registration.id}</h3>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-500">
|
||||
Skapad {formatDateTime(registration.created_at) ?? registration.created_at}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{#if editingId === registration.id}
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelEdit}
|
||||
class="rounded-full border border-slate-300 px-3 py-1 text-xs font-semibold text-slate-600 transition hover:bg-slate-100"
|
||||
>
|
||||
Avbryt
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => beginEdit(registration)}
|
||||
class="rounded-full border border-indigo-300 px-3 py-1 text-xs font-semibold text-indigo-600 transition hover:border-indigo-400 hover:bg-indigo-50"
|
||||
>
|
||||
Redigera
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => removeRegistration(registration)}
|
||||
disabled={deletingId === registration.id || editSaving}
|
||||
class="rounded-full border border-red-300 px-3 py-1 text-xs font-semibold text-red-600 transition hover:border-red-400 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{deletingId === registration.id ? 'Tar bort…' : 'Ta bort'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if entryFields.length > 0}
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
{#each entryFields as field}
|
||||
<div class="rounded-md border border-slate-200 bg-white p-3">
|
||||
<p class="text-xs uppercase tracking-wide text-slate-500">{field.label}</p>
|
||||
<p class="mt-1 text-sm text-slate-800">{fieldValue(registration.entry, field) || '—'}</p>
|
||||
{#if editingId === registration.id}
|
||||
<form class="space-y-4" onsubmit={(event) => {
|
||||
event.preventDefault();
|
||||
saveEdit(registration);
|
||||
}}>
|
||||
{#if entryFields().length > 0}
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Lag / deltagare</h4>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
{#each entryFields() as field}
|
||||
<label class="flex flex-col gap-1 text-sm font-medium text-slate-700">
|
||||
<span>{field.label}</span>
|
||||
<input
|
||||
type={fieldInputType(field.field_type)}
|
||||
value={editEntry[field.id] ?? ''}
|
||||
oninput={(event) => updateEntryField(field.id, (event.currentTarget as HTMLInputElement).value)}
|
||||
placeholder={field.placeholder ?? ''}
|
||||
disabled={editSaving}
|
||||
class="rounded-md border border-slate-300 px-3 py-2 text-sm text-slate-900 focus:border-indigo-500 focus:outline-none focus:ring focus:ring-indigo-500/40"
|
||||
/>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<section class="space-y-2">
|
||||
<h4 class="text-sm font-semibold text-slate-800">Spelare</h4>
|
||||
{#if participantFields.length === 0}
|
||||
{#if registration.participants.length === 0}
|
||||
<p class="text-xs text-slate-500">Inga spelare angivna.</p>
|
||||
{:else}
|
||||
<p class="text-xs text-slate-500">Antal spelare: {registration.participants.length}</p>
|
||||
{/if}
|
||||
{:else if registration.participants.length === 0}
|
||||
<p class="text-xs text-slate-500">Inga spelare angivna.</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each registration.participants as participant, index}
|
||||
|
||||
<section class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Spelare</h4>
|
||||
{#if signupConfig().mode === 'team'}
|
||||
<button
|
||||
type="button"
|
||||
onclick={addParticipant}
|
||||
disabled={editSaving || (maxParticipants() > 0 && editParticipants.length >= maxParticipants())}
|
||||
class="rounded-full border border-indigo-300 px-3 py-1 text-xs font-semibold text-indigo-600 transition hover:border-indigo-400 hover:bg-indigo-50 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Lägg till spelare
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if participantFields().length === 0}
|
||||
<p class="text-xs text-slate-500">Inga spelarspecifika fält att redigera.</p>
|
||||
{:else if editParticipants.length === 0}
|
||||
<p class="text-xs text-slate-500">Inga spelare angivna.</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each editParticipants as participant, index}
|
||||
<div class="space-y-3 rounded-md border border-slate-200 bg-white p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">{participantLabel(index)}</p>
|
||||
{#if signupConfig().mode === 'team'}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => removeParticipant(index)}
|
||||
disabled={editSaving || editParticipants.length <= Math.max(1, minParticipants())}
|
||||
class="rounded-full border border-red-200 px-3 py-1 text-xs font-semibold text-red-600 transition hover:border-red-400 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Ta bort
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
{#each participantFields() as field}
|
||||
<label class="flex flex-col gap-1 text-sm font-medium text-slate-700">
|
||||
<span>{field.label}</span>
|
||||
<input
|
||||
type={fieldInputType(field.field_type)}
|
||||
value={participant[field.id] ?? ''}
|
||||
oninput={(event) => updateParticipantField(index, field.id, (event.currentTarget as HTMLInputElement).value)}
|
||||
placeholder={field.placeholder ?? ''}
|
||||
disabled={editSaving}
|
||||
class="rounded-md border border-slate-300 px-3 py-2 text-sm text-slate-900 focus:border-indigo-500 focus:outline-none focus:ring focus:ring-indigo-500/40"
|
||||
/>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if editError}
|
||||
<p class="rounded-md border border-red-200 bg-red-500/10 px-3 py-2 text-sm text-red-600">{editError}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={editSaving}
|
||||
class="inline-flex items-center justify-center rounded-full bg-indigo-500 px-5 py-2 text-sm font-semibold text-white transition hover:bg-indigo-600 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{editSaving ? 'Sparar…' : 'Spara ändringar'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelEdit}
|
||||
disabled={editSaving}
|
||||
class="inline-flex items-center justify-center rounded-full border border-slate-300 px-5 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Avbryt
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
{#if entryFields().length > 0}
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
{#each entryFields() as field}
|
||||
<div class="rounded-md border border-slate-200 bg-white p-3">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Spelare {index + 1}</p>
|
||||
<ul class="mt-2 space-y-1 text-sm text-slate-800">
|
||||
{#each participantFields as field}
|
||||
<li>
|
||||
<span class="font-medium text-slate-600">{field.label}:</span>
|
||||
<span class="ml-1">{fieldValue(participant, field) || '—'}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-500">{field.label}</p>
|
||||
<p class="mt-1 text-sm text-slate-800">{fieldValue(registration.entry, field) || '—'}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="space-y-2">
|
||||
<h4 class="text-sm font-semibold text-slate-800">Spelare</h4>
|
||||
{#if participantFields().length === 0}
|
||||
{#if registration.participants.length === 0}
|
||||
<p class="text-xs text-slate-500">Inga spelare angivna.</p>
|
||||
{:else}
|
||||
<p class="text-xs text-slate-500">Antal spelare: {registration.participants.length}</p>
|
||||
{/if}
|
||||
{:else if registration.participants.length === 0}
|
||||
<p class="text-xs text-slate-500">Inga spelare angivna.</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each registration.participants as participant, index}
|
||||
<div class="rounded-md border border-slate-200 bg-white p-3">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Spelare {index + 1}</p>
|
||||
<ul class="mt-2 space-y-1 text-sm text-slate-800">
|
||||
{#each participantFields() as field}
|
||||
<li>
|
||||
<span class="font-medium text-slate-600">{field.label}:</span>
|
||||
<span class="ml-1">{fieldValue(participant, field) || '—'}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { ApiRequestError, fetchRegistrationList, parseApiMessage } from '../helpers.server';
|
||||
import type { TournamentInfo } from '$lib/types';
|
||||
|
||||
const deleteErrorFallback = 'Kunde inte ta bort anmälan.';
|
||||
|
||||
export const POST: RequestHandler = async ({ fetch, params, request }) => {
|
||||
try {
|
||||
const snapshot = await fetchRegistrationList(fetch, params.slug);
|
||||
const body = await request.json().catch(() => null);
|
||||
const registrationId =
|
||||
body && typeof body.registration_id === 'number'
|
||||
? body.registration_id
|
||||
: Number.parseInt(body?.registration_id ?? '', 10);
|
||||
|
||||
if (!Number.isFinite(registrationId)) {
|
||||
return json({ message: 'Ogiltigt anmälnings-ID.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const apiResponse = await fetch(
|
||||
`/api/tournament/slug/${params.slug}/registrations/${registrationId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
const text = await apiResponse.text();
|
||||
if (!apiResponse.ok) {
|
||||
const message = parseApiMessage(text, deleteErrorFallback);
|
||||
try {
|
||||
const data = await fetchRegistrationList(fetch, params.slug);
|
||||
const removed = !data.registrations.some(
|
||||
(registration) => registration.id === registrationId
|
||||
);
|
||||
if (removed) {
|
||||
return json({ ...data, warning: message }, { status: 200 });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ApiRequestError) {
|
||||
const fallbackRegistration = snapshot.registrations.find(
|
||||
(registration) => registration.id === registrationId
|
||||
);
|
||||
const filteredRegistrations = snapshot.registrations.filter(
|
||||
(registration) => registration.id !== registrationId
|
||||
);
|
||||
const participantAdjustment = fallbackRegistration?.participants?.length ?? 0;
|
||||
const fallbackTournament: TournamentInfo = {
|
||||
...snapshot.tournament,
|
||||
total_registrations: Math.max(snapshot.tournament.total_registrations - 1, 0),
|
||||
total_participants: Math.max(
|
||||
snapshot.tournament.total_participants - participantAdjustment,
|
||||
0
|
||||
),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
return json(
|
||||
{
|
||||
warning: message,
|
||||
tournament: fallbackTournament,
|
||||
registrations: filteredRegistrations
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return json({ message }, { status: apiResponse.status });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetchRegistrationList(fetch, params.slug);
|
||||
return json(data);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiRequestError) {
|
||||
const message = parseApiMessage(err.body, deleteErrorFallback);
|
||||
const fallbackRegistration = snapshot.registrations.find(
|
||||
(registration) => registration.id === registrationId
|
||||
);
|
||||
const filteredRegistrations = snapshot.registrations.filter(
|
||||
(registration) => registration.id !== registrationId
|
||||
);
|
||||
const participantAdjustment = fallbackRegistration?.participants?.length ?? 0;
|
||||
const fallbackTournament: TournamentInfo = {
|
||||
...snapshot.tournament,
|
||||
total_registrations: Math.max(snapshot.tournament.total_registrations - 1, 0),
|
||||
total_participants: Math.max(
|
||||
snapshot.tournament.total_participants - participantAdjustment,
|
||||
0
|
||||
),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
return json(
|
||||
{
|
||||
warning: message,
|
||||
tournament: fallbackTournament,
|
||||
registrations: filteredRegistrations
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ApiRequestError) {
|
||||
const message = parseApiMessage(err.body, deleteErrorFallback);
|
||||
return json({ message }, { status: err.status });
|
||||
}
|
||||
console.error('Unexpected error deleting registration', err);
|
||||
return json({ message: 'Ett oväntat fel inträffade.' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import type { TournamentRegistrationList, TournamentRegistrationItem, TournamentInfo } from '$lib/types';
|
||||
|
||||
export class ApiRequestError extends Error {
|
||||
status: number;
|
||||
body: string;
|
||||
|
||||
constructor(status: number, body: string) {
|
||||
super(`API request failed with status ${status}`);
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
export function parseApiMessage(body: string, fallback: string): string {
|
||||
if (!body) return fallback;
|
||||
try {
|
||||
const parsed = JSON.parse(body) as { message?: string } | undefined;
|
||||
if (parsed?.message && parsed.message.trim()) {
|
||||
return parsed.message;
|
||||
}
|
||||
} catch {
|
||||
// ignore JSON parse errors
|
||||
}
|
||||
const trimmed = body.trim();
|
||||
return trimmed || fallback;
|
||||
}
|
||||
|
||||
export async function fetchRegistrationList(
|
||||
fetchFn: FetchFn,
|
||||
slug: string
|
||||
): Promise<{ tournament: TournamentInfo; registrations: TournamentRegistrationItem[] }> {
|
||||
const response = await fetchFn(`/api/tournament/slug/${slug}/registrations`, {
|
||||
method: 'GET'
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new ApiRequestError(response.status, text);
|
||||
}
|
||||
const data = JSON.parse(text) as TournamentRegistrationList;
|
||||
return {
|
||||
tournament: data.tournament,
|
||||
registrations: data.registrations ?? []
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeRecord(value: unknown): Record<string, string> {
|
||||
if (!value || typeof value !== 'object') return {};
|
||||
const record: Record<string, string> = {};
|
||||
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (typeof entry === 'string') {
|
||||
record[key] = entry;
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
export function normalizeParticipants(value: unknown): Record<string, string>[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map((item) => normalizeRecord(item));
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { ApiRequestError, fetchRegistrationList, parseApiMessage } from '../helpers.server';
|
||||
|
||||
const loadErrorFallback = 'Kunde inte hämta anmälningar.';
|
||||
|
||||
export const POST: RequestHandler = async ({ fetch, params }) => {
|
||||
try {
|
||||
const data = await fetchRegistrationList(fetch, params.slug);
|
||||
return json(data);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiRequestError) {
|
||||
const message = parseApiMessage(err.body, loadErrorFallback);
|
||||
return json({ message }, { status: err.status });
|
||||
}
|
||||
console.error('Unexpected error refreshing registrations', err);
|
||||
return json({ message: 'Ett oväntat fel inträffade.' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import {
|
||||
ApiRequestError,
|
||||
fetchRegistrationList,
|
||||
parseApiMessage,
|
||||
normalizeRecord,
|
||||
normalizeParticipants
|
||||
} from '../helpers.server';
|
||||
|
||||
const updateErrorFallback = 'Kunde inte uppdatera anmälan.';
|
||||
|
||||
export const POST: RequestHandler = async ({ fetch, params, request }) => {
|
||||
try {
|
||||
const raw = await request.json().catch(() => null);
|
||||
const registrationId =
|
||||
raw && typeof raw.registration_id === 'number'
|
||||
? raw.registration_id
|
||||
: Number.parseInt(raw?.registration_id ?? '', 10);
|
||||
|
||||
if (!Number.isFinite(registrationId)) {
|
||||
return json({ message: 'Ogiltigt anmälnings-ID.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const entry = normalizeRecord(raw?.entry);
|
||||
const participants = normalizeParticipants(raw?.participants);
|
||||
|
||||
const payload = { entry, participants };
|
||||
|
||||
const apiResponse = await fetch(
|
||||
`/api/tournament/slug/${params.slug}/registrations/${registrationId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
const text = await apiResponse.text();
|
||||
if (!apiResponse.ok) {
|
||||
const message = parseApiMessage(text, updateErrorFallback);
|
||||
return json({ message }, { status: apiResponse.status });
|
||||
}
|
||||
|
||||
const data = await fetchRegistrationList(fetch, params.slug);
|
||||
return json(data);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiRequestError) {
|
||||
const message = parseApiMessage(err.body, updateErrorFallback);
|
||||
return json({ message }, { status: err.status });
|
||||
}
|
||||
console.error('Unexpected error updating registration', err);
|
||||
return json({ message: 'Ett oväntat fel inträffade.' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
|
@ -1,26 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { listenToTournamentEvents } from '$lib/client/tournament-events';
|
||||
import type { TournamentInfo } from '$lib/types';
|
||||
|
||||
const props = $props<{ data: { tournaments: TournamentInfo[] } }>();
|
||||
const tournaments = props.data.tournaments ?? [];
|
||||
let tournamentList = $state(props.data.tournaments ?? []);
|
||||
|
||||
function pickFeatured(list: TournamentInfo[]) {
|
||||
if (list.length === 0) return null;
|
||||
const now = Date.now();
|
||||
const withDate = list
|
||||
.filter((item) => item.start_at)
|
||||
.map((item) => ({ item, time: new Date(item.start_at as string).getTime() }))
|
||||
.filter(({ time }) => !Number.isNaN(time));
|
||||
if (withDate.length > 0) {
|
||||
const upcoming = withDate
|
||||
.filter(({ time }) => time >= now)
|
||||
.sort((a, b) => a.time - b.time);
|
||||
if (upcoming.length > 0) {
|
||||
return upcoming[0].item;
|
||||
function sortTournaments(list: TournamentInfo[]): TournamentInfo[] {
|
||||
return [...list].sort((a, b) => {
|
||||
const timeA = a.start_at ? new Date(a.start_at).getTime() : Number.POSITIVE_INFINITY;
|
||||
const timeB = b.start_at ? new Date(b.start_at).getTime() : Number.POSITIVE_INFINITY;
|
||||
if (!Number.isNaN(timeA) && !Number.isNaN(timeB) && timeA !== timeB) {
|
||||
return timeA - timeB;
|
||||
}
|
||||
return withDate.sort((a, b) => a.time - b.time)[0].item;
|
||||
return a.title.localeCompare(b.title, 'sv');
|
||||
});
|
||||
}
|
||||
|
||||
const tournaments = $derived(() => sortTournaments(tournamentList));
|
||||
|
||||
function upsertTournament(tournament: TournamentInfo) {
|
||||
const index = tournamentList.findIndex((item) => item.id === tournament.id);
|
||||
if (index >= 0) {
|
||||
tournamentList = tournamentList.map((item, idx) => (idx === index ? tournament : item));
|
||||
return;
|
||||
}
|
||||
return list[0];
|
||||
tournamentList = [...tournamentList, tournament];
|
||||
}
|
||||
|
||||
function removeTournament(id: number) {
|
||||
if (!tournamentList.some((item) => item.id === id)) return;
|
||||
tournamentList = tournamentList.filter((item) => item.id !== id);
|
||||
}
|
||||
|
||||
function formatDate(value: string | null) {
|
||||
|
|
@ -37,105 +47,108 @@
|
|||
});
|
||||
}
|
||||
|
||||
const featuredTournament = pickFeatured(tournaments);
|
||||
const otherTournaments = tournaments.filter(
|
||||
(item: TournamentInfo) => item.id !== (featuredTournament?.id ?? -1)
|
||||
);
|
||||
function registrationSummary(tournament: TournamentInfo) {
|
||||
const teams = tournament.total_registrations ?? 0;
|
||||
const participants = tournament.total_participants ?? 0;
|
||||
if (tournament.signup_config.mode === 'team') {
|
||||
if (teams === 0) {
|
||||
return 'Inga lag anmälda ännu';
|
||||
}
|
||||
return `${teams} ${teams === 1 ? 'lag' : 'lag'} · ${participants} ${participants === 1 ? 'spelare' : 'spelare'}`;
|
||||
}
|
||||
const count = Math.max(participants, teams);
|
||||
if (count === 0) {
|
||||
return 'Inga spelare anmälda ännu';
|
||||
}
|
||||
return `${count} ${count === 1 ? 'spelare' : 'spelare'}`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const stop = listenToTournamentEvents(upsertTournament, removeTournament);
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>LAN Tournament</title>
|
||||
<title>Turneringar – VBytes LAN</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-slate-950 text-slate-100">
|
||||
<div class="mx-auto flex min-h-screen max-w-5xl flex-col items-center justify-center gap-10 px-4 text-center">
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm uppercase tracking-[0.4em] text-indigo-300">VBytes LAN</p>
|
||||
<p class="text-xs uppercase tracking-[0.4em] text-indigo-300">{featuredTournament?.game ?? 'Turnering'}</p>
|
||||
<h1 class="text-4xl font-bold sm:text-5xl">{featuredTournament?.title ?? 'Turnering & Community'}</h1>
|
||||
{#if featuredTournament?.tagline}
|
||||
<p class="mx-auto max-w-2xl text-lg text-slate-300">{featuredTournament.tagline}</p>
|
||||
{:else}
|
||||
<p class="mx-auto max-w-2xl text-lg text-slate-300">
|
||||
Samla laget, följ brackets i realtid och håll koll på allt som händer under turneringen.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mx-auto max-w-6xl space-y-12 px-4 py-16 sm:py-20">
|
||||
<header class="space-y-3 text-center">
|
||||
<p class="text-xs uppercase tracking-[0.4em] text-indigo-300">VBytes LAN</p>
|
||||
<h1 class="text-4xl font-bold sm:text-5xl">Turneringar</h1>
|
||||
<p class="mx-auto max-w-2xl text-base text-slate-300">
|
||||
Samla laget, följ brackets i realtid och håll koll på allt som händer under turneringarna.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{#if featuredTournament}
|
||||
<div class="w-full max-w-3xl space-y-4 rounded-2xl bg-slate-900/70 p-6 text-left shadow-lg">
|
||||
{#if featuredTournament.start_at}
|
||||
<p class="text-sm font-semibold text-indigo-200">Start: {formatDate(featuredTournament.start_at) ?? featuredTournament.start_at}</p>
|
||||
{/if}
|
||||
{#if featuredTournament.location}
|
||||
<p class="text-sm text-slate-200">Plats: {featuredTournament.location}</p>
|
||||
{/if}
|
||||
{#if featuredTournament.description}
|
||||
<p class="whitespace-pre-line text-sm text-slate-200">{featuredTournament.description}</p>
|
||||
{/if}
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{#if featuredTournament.slug}
|
||||
<a
|
||||
href={`/tournament/${featuredTournament.slug}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="rounded-full bg-indigo-500 px-4 py-2 text-xs font-semibold uppercase tracking-wide text-white transition hover:bg-indigo-600"
|
||||
>
|
||||
Anmäl laget
|
||||
</a>
|
||||
{/if}
|
||||
{#if featuredTournament.contact}
|
||||
<span class="rounded-full border border-slate-600 px-4 py-2 text-xs font-semibold uppercase tracking-wide text-slate-200">
|
||||
Kontakt: {featuredTournament.contact}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if otherTournaments.length > 0}
|
||||
<div class="w-full max-w-4xl space-y-3 text-left">
|
||||
<h2 class="text-base font-semibold text-slate-300">Fler event</h2>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
{#each otherTournaments as tournament}
|
||||
<div class="space-y-2 rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
||||
<h3 class="text-lg font-semibold text-slate-100">{tournament.title}</h3>
|
||||
{#if tournaments().length > 0}
|
||||
<div class="flex flex-wrap justify-center gap-8">
|
||||
{#each tournaments() as tournament}
|
||||
<article class="group w-full max-w-md flex flex-col rounded-3xl border border-slate-800 bg-gradient-to-br from-slate-900/80 via-slate-900/70 to-slate-900/50 p-8 shadow-xl transition duration-200 hover:-translate-y-1 hover:border-indigo-400/70 hover:shadow-indigo-500/25">
|
||||
<div class="flex items-center justify-between gap-3 text-xs uppercase tracking-wide">
|
||||
<span class="font-semibold text-indigo-200">{tournament.game}</span>
|
||||
<span class="rounded-full border border-slate-700 px-3 py-1 text-[0.7rem] font-semibold text-slate-300">
|
||||
{registrationSummary(tournament)}
|
||||
</span>
|
||||
</div>
|
||||
<h2 class="mt-4 text-3xl font-semibold text-slate-50">{tournament.title}</h2>
|
||||
{#if tournament.tagline}
|
||||
<p class="mt-3 text-base text-slate-300">{tournament.tagline}</p>
|
||||
{:else if tournament.description}
|
||||
<p class="mt-3 text-base text-slate-400">{tournament.description}</p>
|
||||
{/if}
|
||||
<dl class="mt-6 space-y-3 text-sm text-slate-300">
|
||||
{#if tournament.start_at}
|
||||
<p class="text-xs uppercase tracking-wide text-indigo-200">{formatDate(tournament.start_at) ?? tournament.start_at}</p>
|
||||
<div class="flex items-center gap-3 text-[0.95rem]">
|
||||
<span class="text-indigo-200">Start:</span>
|
||||
<span>{formatDate(tournament.start_at) ?? tournament.start_at}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if tournament.tagline}
|
||||
<p class="text-sm text-slate-300">{tournament.tagline}</p>
|
||||
{:else if tournament.description}
|
||||
<p class="text-sm text-slate-400">{tournament.description}</p>
|
||||
{#if tournament.location}
|
||||
<div class="flex items-center gap-3 text-[0.95rem]">
|
||||
<span class="text-indigo-200">Plats:</span>
|
||||
<span>{tournament.location}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#if tournament.contact}
|
||||
<div class="flex items-center gap-3 text-[0.95rem]">
|
||||
<span class="text-indigo-200">Kontakt:</span>
|
||||
<span>{tournament.contact}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</dl>
|
||||
<div class="mt-auto pt-8">
|
||||
{#if tournament.slug}
|
||||
<a
|
||||
href={`/tournament/${tournament.slug}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="rounded-full border border-indigo-400 px-3 py-1 text-xs font-semibold text-indigo-200"
|
||||
class="inline-flex items-center justify-center rounded-full bg-indigo-500 px-5 py-2 text-sm font-semibold uppercase tracking-wide text-white transition hover:bg-indigo-600"
|
||||
>
|
||||
Anmälan
|
||||
Visa turnering
|
||||
</a>
|
||||
{:else}
|
||||
<span class="text-xs text-slate-500">Ingen publik sida tillgänglig.</span>
|
||||
{/if}
|
||||
{#if tournament.contact}
|
||||
<span class="rounded-full border border-slate-600 px-3 py-1 text-xs font-semibold text-slate-300">
|
||||
Kontakt: {tournament.contact}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="rounded-2xl border border-dashed border-slate-700 bg-slate-900/40 px-6 py-12 text-center text-sm text-slate-400">
|
||||
Inga turneringar är publicerade ännu. Kom tillbaka senare!
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
href="/admin"
|
||||
class="rounded-full bg-indigo-500 px-6 py-3 text-sm font-semibold uppercase tracking-wide text-white transition hover:bg-indigo-600"
|
||||
>
|
||||
Till admin
|
||||
</a>
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="/admin"
|
||||
class="inline-flex items-center justify-center rounded-full bg-indigo-500 px-6 py-3 text-sm font-semibold uppercase tracking-wide text-white transition hover:bg-indigo-600"
|
||||
>
|
||||
Till admin
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -467,11 +467,11 @@
|
|||
<div class="space-y-3 rounded-md border border-slate-800 bg-slate-900/60 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-semibold text-slate-200">{participantDisplayName(index)}</span>
|
||||
{#if signupConfig.mode === 'team'}
|
||||
{#if signupConfig.mode === 'team' && canRemoveParticipant()}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => removeParticipant(index)}
|
||||
disabled={!canRemoveParticipant() || signup.submitting}
|
||||
disabled={signup.submitting}
|
||||
class="rounded-full border border-red-300 px-3 py-1 text-xs font-semibold text-red-200 transition hover:border-red-400 hover:bg-red-500/10 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Ta bort
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { proxyRequest } from '$lib/server/backend';
|
||||
|
||||
|
|
@ -15,6 +14,12 @@ export const GET: RequestHandler = async (event) => {
|
|||
if (contentType) headers.set('content-type', contentType);
|
||||
for (const cookie of setCookies) headers.append('set-cookie', cookie);
|
||||
|
||||
if (response.status >= 400) {
|
||||
console.error(
|
||||
`GET /tournament/slug/${event.params.slug}/registrations failed with ${response.status}: ${text}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Kunde inte hämta anmälningar.';
|
||||
try {
|
||||
|
|
@ -23,7 +28,11 @@ export const GET: RequestHandler = async (event) => {
|
|||
} catch {
|
||||
if (text) message = text;
|
||||
}
|
||||
throw error(response.status, message);
|
||||
headers.set('content-type', 'application/json');
|
||||
return new Response(JSON.stringify({ message }), {
|
||||
status: response.status,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(text, { status: response.status, headers });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { proxyRequest } from '$lib/server/backend';
|
||||
|
||||
|
|
@ -21,6 +20,11 @@ export const GET: RequestHandler = async (event) => {
|
|||
const { response, setCookies } = await proxyRequest(event, path, { method: 'GET' });
|
||||
const text = await response.text();
|
||||
const headers = buildHeaders(response, setCookies);
|
||||
if (response.status >= 400) {
|
||||
console.error(
|
||||
`GET /tournament/slug/${event.params.slug}/registrations/${event.params.registration} failed with ${response.status}: ${text}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Kunde inte hämta anmälan.';
|
||||
|
|
@ -30,8 +34,78 @@ export const GET: RequestHandler = async (event) => {
|
|||
} catch {
|
||||
if (text) message = text;
|
||||
}
|
||||
throw error(response.status, message);
|
||||
const errorHeaders = new Headers(headers);
|
||||
errorHeaders.set('content-type', 'application/json');
|
||||
return new Response(JSON.stringify({ message }), {
|
||||
status: response.status,
|
||||
headers: errorHeaders
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(text, { status: response.status, headers });
|
||||
};
|
||||
|
||||
export const PUT: RequestHandler = async (event) => {
|
||||
const body = await event.request.text();
|
||||
const path = `/tournament/slug/${event.params.slug}/registrations/${event.params.registration}`;
|
||||
const { response, setCookies } = await proxyRequest(event, path, {
|
||||
method: 'PUT',
|
||||
body
|
||||
});
|
||||
const text = await response.text();
|
||||
const headers = buildHeaders(response, setCookies);
|
||||
if (response.status >= 400) {
|
||||
console.error(
|
||||
`PUT /tournament/slug/${event.params.slug}/registrations/${event.params.registration} failed with ${response.status}: ${text}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Kunde inte uppdatera anmälan.';
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
message = parsed.message ?? message;
|
||||
} catch {
|
||||
if (text) message = text;
|
||||
}
|
||||
const errorHeaders = new Headers(headers);
|
||||
errorHeaders.set('content-type', 'application/json');
|
||||
return new Response(JSON.stringify({ message }), {
|
||||
status: response.status,
|
||||
headers: errorHeaders
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(text, { status: response.status, headers });
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async (event) => {
|
||||
const path = `/tournament/slug/${event.params.slug}/registrations/${event.params.registration}`;
|
||||
const { response, setCookies } = await proxyRequest(event, path, { method: 'DELETE' });
|
||||
const text = await response.text();
|
||||
const headers = buildHeaders(response, setCookies);
|
||||
|
||||
if (response.status >= 400) {
|
||||
console.error(
|
||||
`DELETE /tournament/slug/${event.params.slug}/registrations/${event.params.registration} failed with ${response.status}: ${text}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Kunde inte ta bort anmälan.';
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
message = parsed.message ?? message;
|
||||
} catch {
|
||||
if (text) message = text;
|
||||
}
|
||||
const errorHeaders = new Headers(headers);
|
||||
errorHeaders.set('content-type', 'application/json');
|
||||
return new Response(JSON.stringify({ message }), {
|
||||
status: response.status,
|
||||
headers: errorHeaders
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(text || '', { status: response.status, headers });
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue