use crate::admin::{station::Station, team::Team}; use axum::Router; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Row, SqlitePool}; use std::sync::Arc; mod web; #[derive(FromRow, Debug, Serialize, Deserialize)] pub(crate) struct Route { pub(crate) id: i64, pub(crate) name: String, } impl Route { pub(crate) async fn all(db: &SqlitePool) -> Vec { sqlx::query_as::<_, Self>("SELECT id, name FROM route;") .fetch_all(db) .await .unwrap() } pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { sqlx::query_as!(Self, "SELECT id, name FROM route WHERE id = ?", id) .fetch_one(db) .await .ok() } async fn create(db: &SqlitePool, name: &str) -> Result<(), String> { sqlx::query!("INSERT INTO route(name) VALUES (?)", name) .execute(db) .await .map_err(|e| e.to_string())?; Ok(()) } async fn update_name(&self, db: &SqlitePool, name: &str) { sqlx::query!("UPDATE route SET name = ? WHERE id = ?", name, self.id) .execute(db) .await .unwrap(); } async fn add_station(&self, db: &SqlitePool, station: &Station) -> Result<(), String> { sqlx::query!( r#" INSERT INTO route_station (route_id, station_id, pos) VALUES (?, ?, ( SELECT COALESCE(MAX(pos), 0) + 2 FROM route_station WHERE route_id = ? )) "#, self.id, station.id, self.id ) .execute(db) .await .map_err(|e| e.to_string())?; Ok(()) } async fn delete_station(&self, db: &SqlitePool, station: &Station) -> bool { let result = sqlx::query!( "DELETE FROM route_station WHERE route_id = ? AND station_id = ?", self.id, station.id ) .execute(db) .await .unwrap(); result.rows_affected() > 0 } async fn move_station_higher(&self, db: &SqlitePool, station: &Station) -> bool { let result = sqlx::query!( "UPDATE route_station SET pos = pos-3 WHERE route_id = ? AND station_id = ?", self.id, station.id ) .execute(db) .await .unwrap(); if result.rows_affected() == 0 { return false; } sqlx::query( &format!( " -- Step 1: Create a temporary table with new rank values CREATE TEMP TABLE IF NOT EXISTS temp_pos AS SELECT route_id, station_id, ROW_NUMBER() OVER (ORDER BY pos ASC) AS new_rank FROM route_station WHERE route_id = {}; -- Step 2: Update the original table UPDATE route_station SET pos = (SELECT 2*new_rank FROM temp_pos WHERE temp_pos.route_id = route_station.route_id AND temp_pos.station_id = route_station.station_id) WHERE route_id = {}; -- Step 3: Drop the temporary table (no longer needed) DROP TABLE temp_pos;", self.id, self.id, )) .execute(db) .await .unwrap(); true } async fn delete(&self, db: &SqlitePool) -> Result<(), String> { sqlx::query!("DELETE FROM route WHERE id = ?", self.id) .execute(db) .await .map_err(|e| e.to_string())?; Ok(()) } pub(crate) async fn stations(&self, db: &SqlitePool) -> Vec { sqlx::query_as::<_, Station>( " SELECT s.id, s.name, s.notes, s.amount_people, s.last_login, s.pw, s.lat, s.lng FROM station s JOIN route_station rs ON s.id = rs.station_id WHERE rs.route_id = ? ORDER BY rs.pos ", ) .bind(self.id) .fetch_all(db) .await .unwrap() } async fn stations_not_in_route(&self, db: &SqlitePool) -> Vec { sqlx::query_as::<_, Station>( " SELECT id, name, notes, amount_people, last_login, pw, lat, lng FROM station WHERE id NOT IN ( SELECT station_id FROM route_station WHERE route_id = ? ) ORDER BY name ", ) .bind(self.id) .fetch_all(db) .await .unwrap() } pub(crate) async fn teams(&self, db: &SqlitePool) -> Vec { Team::all_with_route(db, self).await } pub(crate) async fn get_next_first_station(&self, db: &SqlitePool) -> Option { let Ok(row) = sqlx::query(&format!( " SELECT s.id AS next_first_station_id, COUNT(g.id) AS team_count FROM station s JOIN route_station rs ON s.id = rs.station_id LEFT JOIN 'team' g ON s.id = g.first_station_id WHERE rs.route_id = {} GROUP BY s.id ORDER BY team_count ASC, s.id ASC LIMIT 1", self.id )) .fetch_one(db) .await else { return None; // No station for route exists }; let next_first_station_id = row.get::("next_first_station_id"); Some( Station::find_by_id(db, next_first_station_id) .await .expect("db constraints"), ) } } pub(super) fn routes() -> Router> { web::routes() }