changing data model according to new data

This commit is contained in:
Sebastian 2025-09-21 21:55:40 +02:00
parent 464af45107
commit 9de3c4a482
4 changed files with 271 additions and 72 deletions

View file

@ -0,0 +1,23 @@
BEGIN;
DROP TABLE IF EXISTS persons;
CREATE TABLE persons (
id SERIAL PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
grade INTEGER NOT NULL,
parent_name TEXT NOT NULL,
parent_phone_number TEXT NOT NULL,
checked_in BOOLEAN NOT NULL DEFAULT FALSE,
inside BOOLEAN NOT NULL DEFAULT FALSE,
visitor BOOLEAN NOT NULL DEFAULT FALSE,
sleeping_spot BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX idx_persons_first_name_trgm ON persons USING GIN (first_name gin_trgm_ops);
CREATE INDEX idx_persons_last_name_trgm ON persons USING GIN (last_name gin_trgm_ops);
CREATE INDEX idx_persons_parent_name_trgm ON persons USING GIN (parent_name gin_trgm_ops);
CREATE INDEX idx_persons_parent_phone_number ON persons (parent_phone_number);
COMMIT;

View file

@ -187,13 +187,25 @@ async fn search_persons(
let persons = if let Some(id) = id_value {
sqlx::query_as::<_, Person>(
r#"
SELECT id, name, age, phone_number, checked_in, inside
SELECT
id,
first_name,
last_name,
grade,
parent_name,
parent_phone_number,
checked_in,
inside,
visitor,
sleeping_spot
FROM persons
WHERE name ILIKE $1
OR phone_number ILIKE $1
WHERE first_name ILIKE $1
OR last_name ILIKE $1
OR parent_name ILIKE $1
OR parent_phone_number ILIKE $1
OR (first_name || ' ' || last_name) ILIKE $1
OR id = $2
ORDER BY name
LIMIT 100
ORDER BY last_name, first_name
"#,
)
.bind(&like_pattern)
@ -203,12 +215,24 @@ async fn search_persons(
} else {
sqlx::query_as::<_, Person>(
r#"
SELECT id, name, age, phone_number, checked_in, inside
SELECT
id,
first_name,
last_name,
grade,
parent_name,
parent_phone_number,
checked_in,
inside,
visitor,
sleeping_spot
FROM persons
WHERE name ILIKE $1
OR phone_number ILIKE $1
ORDER BY name
LIMIT 100
WHERE first_name ILIKE $1
OR last_name ILIKE $1
OR parent_name ILIKE $1
OR parent_phone_number ILIKE $1
OR (first_name || ' ' || last_name) ILIKE $1
ORDER BY last_name, first_name
"#,
)
.bind(&like_pattern)
@ -252,7 +276,7 @@ async fn list_checked_in(
let id_value = search_term.as_ref().and_then(|s| s.parse::<i32>().ok());
let mut qb = QueryBuilder::<sqlx::Postgres>::new(
"SELECT id, name, age, phone_number, checked_in, inside FROM persons",
"SELECT id, first_name, last_name, grade, parent_name, parent_phone_number, checked_in, inside, visitor, sleeping_spot FROM persons",
);
let mut first_condition = true;
@ -278,8 +302,12 @@ async fn list_checked_in(
if let Some(like) = like_pattern.as_ref() {
append_condition(&mut qb);
qb.push("(");
qb.push("name ILIKE ").push_bind(like);
qb.push(" OR phone_number ILIKE ").push_bind(like);
qb.push("first_name ILIKE ").push_bind(like);
qb.push(" OR last_name ILIKE ").push_bind(like);
qb.push(" OR parent_name ILIKE ").push_bind(like);
qb.push(" OR parent_phone_number ILIKE ").push_bind(like);
qb.push(" OR (first_name || ' ' || last_name) ILIKE ")
.push_bind(like);
if let Some(id) = id_value {
qb.push(" OR id = ").push_bind(id);
}
@ -289,12 +317,9 @@ async fn list_checked_in(
if let Some(id) = id_value {
qb.push(" ORDER BY CASE WHEN id = ")
.push_bind(id)
.push(" THEN 0 ELSE 1 END, id, name");
.push(" THEN 0 ELSE 1 END, id, last_name, first_name");
} else {
qb.push(" ORDER BY id, name");
}
if like_pattern.is_some() {
qb.push(" LIMIT 1000");
qb.push(" ORDER BY id, last_name, first_name");
}
let persons = qb.build_query_as::<Person>().fetch_all(&state.db).await?;
@ -345,53 +370,116 @@ async fn create_person(
state: &State<AppState>,
payload: Json<NewPersonRequest>,
) -> Result<Json<PersonActionResponse>, ApiError> {
let name = payload.name.trim();
if name.is_empty() {
return Err(ApiError::bad_request("Namn får inte vara tomt."));
let first_name = payload.first_name.trim();
if first_name.is_empty() {
return Err(ApiError::bad_request("Förnamn får inte vara tomt."));
}
if payload.age < 0 {
return Err(ApiError::bad_request("Ålder måste vara noll eller högre."));
let last_name = payload.last_name.trim();
if last_name.is_empty() {
return Err(ApiError::bad_request("Efternamn får inte vara tomt."));
}
let phone_number = payload.phone_number.trim();
if phone_number.is_empty() {
return Err(ApiError::bad_request("Telefonnummer krävs."));
if payload.grade < 0 {
return Err(ApiError::bad_request("Klass måste vara noll eller högre."));
}
let age = payload.age;
let parent_name = payload.parent_name.trim();
if parent_name.is_empty() {
return Err(ApiError::bad_request("Kontaktperson krävs."));
}
let parent_phone_number = payload.parent_phone_number.trim();
if parent_phone_number.is_empty() {
return Err(ApiError::bad_request(
"Kontaktpersonens telefonnummer krävs.",
));
}
let grade = payload.grade;
let checked_in = payload.checked_in.unwrap_or(false);
let inside = payload.inside.unwrap_or(false);
let visitor = payload.visitor.unwrap_or(false);
let sleeping_spot = payload.sleeping_spot.unwrap_or(false);
let person = match payload.id {
Some(id) => sqlx::query_as::<_, Person>(
r#"
INSERT INTO persons (id, name, age, phone_number, checked_in, inside)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, name, age, phone_number, checked_in, inside
INSERT INTO persons (
id,
first_name,
last_name,
grade,
parent_name,
parent_phone_number,
checked_in,
inside,
visitor,
sleeping_spot
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING
id,
first_name,
last_name,
grade,
parent_name,
parent_phone_number,
checked_in,
inside,
visitor,
sleeping_spot
"#,
)
.bind(id)
.bind(name)
.bind(age)
.bind(phone_number)
.bind(first_name)
.bind(last_name)
.bind(grade)
.bind(parent_name)
.bind(parent_phone_number)
.bind(checked_in)
.bind(inside)
.bind(visitor)
.bind(sleeping_spot)
.fetch_one(&state.db)
.await
.map_err(|err| map_db_error(err, "Kunde inte skapa person"))?,
None => sqlx::query_as::<_, Person>(
r#"
INSERT INTO persons (name, age, phone_number, checked_in, inside)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, name, age, phone_number, checked_in, inside
INSERT INTO persons (
first_name,
last_name,
grade,
parent_name,
parent_phone_number,
checked_in,
inside,
visitor,
sleeping_spot
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING
id,
first_name,
last_name,
grade,
parent_name,
parent_phone_number,
checked_in,
inside,
visitor,
sleeping_spot
"#,
)
.bind(name)
.bind(age)
.bind(phone_number)
.bind(first_name)
.bind(last_name)
.bind(grade)
.bind(parent_name)
.bind(parent_phone_number)
.bind(checked_in)
.bind(inside)
.bind(visitor)
.bind(sleeping_spot)
.fetch_one(&state.db)
.await
.map_err(|err| map_db_error(err, "Kunde inte skapa person"))?,
@ -415,7 +503,17 @@ async fn update_checked_in(
SET checked_in = $2,
inside = $2
WHERE id = $1
RETURNING id, name, age, phone_number, checked_in, inside
RETURNING
id,
first_name,
last_name,
grade,
parent_name,
parent_phone_number,
checked_in,
inside,
visitor,
sleeping_spot
"#,
)
.bind(id)
@ -446,7 +544,17 @@ async fn update_inside(
SET inside = $2
WHERE id = $1
AND checked_in = TRUE
RETURNING id, name, age, phone_number, checked_in, inside
RETURNING
id,
first_name,
last_name,
grade,
parent_name,
parent_phone_number,
checked_in,
inside,
visitor,
sleeping_spot
"#,
)
.bind(id)
@ -484,7 +592,7 @@ fn map_db_error(err: sqlx::Error, context: &str) -> ApiError {
if let Some(code) = db_err.code() {
if code == "23505" {
return ApiError::bad_request(
"Krock i databasen kontrollera id eller telefonnummer.",
"Krock i databasen kontrollera id eller kontaktuppgifter.",
);
}
}

View file

@ -12,11 +12,15 @@ pub struct User {
#[derive(Debug, FromRow)]
pub struct Person {
pub id: i32,
pub name: String,
pub age: i32,
pub phone_number: String,
pub first_name: String,
pub last_name: String,
pub grade: i32,
pub parent_name: String,
pub parent_phone_number: String,
pub checked_in: bool,
pub inside: bool,
pub visitor: bool,
pub sleeping_spot: bool,
}
#[derive(Debug, Deserialize)]
@ -36,25 +40,30 @@ pub struct LoginResponse {
#[serde(crate = "rocket::serde")]
pub struct PersonResponse {
pub id: i32,
pub name: String,
pub age: i32,
pub phone_number: String,
pub first_name: String,
pub last_name: String,
pub grade: i32,
pub parent_name: String,
pub parent_phone_number: String,
pub checked_in: bool,
pub inside: bool,
pub under_ten: bool,
pub visitor: bool,
pub sleeping_spot: bool,
}
impl From<Person> for PersonResponse {
fn from(person: Person) -> Self {
let under_ten = person.age < 10;
PersonResponse {
id: person.id,
name: person.name,
age: person.age,
phone_number: person.phone_number,
first_name: person.first_name,
last_name: person.last_name,
grade: person.grade,
parent_name: person.parent_name,
parent_phone_number: person.parent_phone_number,
checked_in: person.checked_in,
inside: person.inside,
under_ten,
visitor: person.visitor,
sleeping_spot: person.sleeping_spot,
}
}
}
@ -74,13 +83,19 @@ pub struct PersonActionResponse {
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct NewPersonRequest {
pub name: String,
pub age: i32,
pub phone_number: String,
pub first_name: String,
pub last_name: String,
pub grade: i32,
pub parent_name: String,
pub parent_phone_number: String,
#[serde(default)]
pub id: Option<i32>,
#[serde(default)]
pub checked_in: Option<bool>,
#[serde(default)]
pub inside: Option<bool>,
#[serde(default)]
pub visitor: Option<bool>,
#[serde(default)]
pub sleeping_spot: Option<bool>,
}

View file

@ -45,13 +45,30 @@ async fn ensure_person_seed(pool: &PgPool) -> anyhow::Result<()> {
for person in persons {
sqlx::query(
"INSERT INTO persons (name, age, phone_number, checked_in, inside) VALUES ($1, $2, $3, $4, $5)",
r#"
INSERT INTO persons (
first_name,
last_name,
grade,
parent_name,
parent_phone_number,
checked_in,
inside,
visitor,
sleeping_spot
)
.bind(&person.name)
.bind(person.age)
.bind(&person.phone_number)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
)
.bind(&person.first_name)
.bind(&person.last_name)
.bind(person.grade)
.bind(&person.parent_name)
.bind(&person.parent_phone_number)
.bind(person.checked_in)
.bind(person.inside)
.bind(person.visitor)
.bind(person.sleeping_spot)
.execute(pool)
.await
.context("Misslyckades att lägga till seed-person")?;
@ -61,20 +78,24 @@ async fn ensure_person_seed(pool: &PgPool) -> anyhow::Result<()> {
}
struct PersonSeed {
name: String,
age: i32,
phone_number: String,
first_name: String,
last_name: String,
grade: i32,
parent_name: String,
parent_phone_number: String,
checked_in: bool,
inside: bool,
visitor: bool,
sleeping_spot: bool,
}
fn generate_people() -> Vec<PersonSeed> {
let first_names = vec![
let first_names = [
"Alex", "Bianca", "Cecilia", "David", "Elias", "Fatima", "Gabriel", "Hanna", "Isak",
"Johanna", "Karin", "Liam", "Maja", "Nils", "Olivia",
];
let last_names = vec![
let last_names = [
"Andersson",
"Berg",
"Carlsson",
@ -87,20 +108,52 @@ fn generate_people() -> Vec<PersonSeed> {
"Johansson",
];
let guardian_first_names = [
"Anna",
"Bertil",
"Charlotte",
"Daniel",
"Emma",
"Fredrik",
"Greta",
"Henrik",
"Ingrid",
"Jakob",
"Klara",
"Lars",
"Maria",
"Niklas",
"Petra",
];
let guardian_last_names = [
"Lind", "Sandberg", "Forsberg", "Nyström", "Sjöberg", "Viklund", "Ågren", "Öberg",
"Boström", "Engman",
];
let mut people = Vec::with_capacity(first_names.len() * last_names.len());
let mut idx: usize = 0;
for first in &first_names {
for last in &last_names {
let name = format!("{} {}", first, last);
let age = 5 + ((idx * 11) % 60) as i32;
let phone_number = format!("070{:03}{:04}", idx % 1_000, (idx * 37) % 10_000);
let grade = 1 + ((idx * 3) % 9) as i32; // Grades between 1 and 9
let parent_first = guardian_first_names[idx % guardian_first_names.len()];
let parent_last = guardian_last_names[idx % guardian_last_names.len()];
let parent_name = format!("{} {}", parent_first, parent_last);
let parent_phone_number = format!("070{:03}{:04}", idx % 1_000, (idx * 37) % 10_000);
let visitor = idx % 7 == 0;
let sleeping_spot = !visitor && grade >= 3 && idx % 5 == 0;
people.push(PersonSeed {
name,
age,
phone_number,
first_name: (*first).to_string(),
last_name: (*last).to_string(),
grade,
parent_name,
parent_phone_number,
checked_in: false,
inside: false,
visitor,
sleeping_spot,
});
idx += 1;
}