diff --git a/src/bin/main.rs b/src/bin/main.rs index 2d22699..0092da2 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -78,6 +78,7 @@ fn load_router( // https://github.com/tokio-rs/axum/blob/main/examples/graceful-shutdown/src/main.rs async fn shutdown_signal() { let ctrl_c = async { + debug!("Got terminate signal"); signal::ctrl_c() .await .expect("failed to install Ctrl+C handler"); @@ -85,6 +86,7 @@ async fn shutdown_signal() { #[cfg(unix)] let terminate = async { + debug!("Got terminate signal"); signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() diff --git a/src/endpoints/facility.rs b/src/endpoints/facility.rs index 102d739..8055cc1 100644 --- a/src/endpoints/facility.rs +++ b/src/endpoints/facility.rs @@ -1,8 +1,9 @@ use crate::shared::{ - sql::{self, Certification, Controller}, + sql::{self, Activity, Certification, Controller}, AppError, AppState, Config, UserInfo, SESSION_USER_INFO_KEY, }; use axum::{extract::State, response::Html, routing::get, Router}; +use chrono::{Months, Utc}; use itertools::Itertools; use log::warn; use minijinja::{context, Environment}; @@ -233,6 +234,97 @@ async fn page_staff( Ok(Html(rendered)) } +/// View all controller's recent (summarized) controlling activity. +async fn page_activity( + State(state): State>, + session: Session, +) -> Result, AppError> { + #[derive(Debug, Serialize)] + struct ControllerActivity { + name: String, + ois: String, + month_0: u32, + month_1: u32, + month_2: u32, + month_3: u32, + month_4: u32, + } + + let controllers: Vec = sqlx::query_as(sql::GET_ALL_CONTROLLERS) + .fetch_all(&state.db) + .await?; + let activity: Vec = sqlx::query_as(sql::GET_ALL_ACTIVITY) + .fetch_all(&state.db) + .await?; + + let now = Utc::now(); + let months: [String; 5] = [ + now.format("%Y-%m").to_string(), + now.checked_sub_months(Months::new(1)) + .unwrap() + .format("%Y-%m") + .to_string(), + now.checked_sub_months(Months::new(2)) + .unwrap() + .format("%Y-%m") + .to_string(), + now.checked_sub_months(Months::new(3)) + .unwrap() + .format("%Y-%m") + .to_string(), + now.checked_sub_months(Months::new(4)) + .unwrap() + .format("%Y-%m") + .to_string(), + ]; + let activity_data: Vec = controllers + .iter() + .map(|controller| { + let this_controller: Vec<_> = activity + .iter() + .filter(|a| a.cid == controller.cid) + .collect(); + ControllerActivity { + name: format!("{} {}", controller.first_name, controller.last_name), + ois: match &controller.operating_initials { + Some(ois) => ois.to_owned(), + None => String::new(), + }, + month_0: this_controller + .iter() + .filter(|a| a.month == months[0]) + .map(|a| a.minutes) + .sum(), + month_1: this_controller + .iter() + .filter(|a| a.month == months[1]) + .map(|a| a.minutes) + .sum(), + month_2: this_controller + .iter() + .filter(|a| a.month == months[2]) + .map(|a| a.minutes) + .sum(), + month_3: this_controller + .iter() + .filter(|a| a.month == months[3]) + .map(|a| a.minutes) + .sum(), + month_4: this_controller + .iter() + .filter(|a| a.month == months[4]) + .map(|a| a.minutes) + .sum(), + } + }) + .collect(); + + let user_info: Option = session.get(SESSION_USER_INFO_KEY).await?; + let template = state.templates.get_template("activity")?; + let rendered = template.render(context! { user_info, activity_data })?; + Ok(Html(rendered)) +} + pub fn router(templates: &mut Environment) -> Router> { templates .add_template("roster", include_str!("../../templates/roster.jinja")) @@ -240,8 +332,17 @@ pub fn router(templates: &mut Environment) -> Router> { templates .add_template("staff", include_str!("../../templates/staff.jinja")) .unwrap(); + templates + .add_template("activity", include_str!("../../templates/activity.jinja")) + .unwrap(); + templates.add_filter("minutes_to_hm", |total_minutes: u32| { + let hours = total_minutes / 60; + let minutes = total_minutes % 60; + format!("{hours}h{minutes}m") + }); Router::new() .route("/facility/roster", get(page_roster)) .route("/facility/staff", get(page_staff)) + .route("/facility/activity", get(page_activity)) } diff --git a/src/shared/sql.rs b/src/shared/sql.rs index a885c3a..d35f99f 100644 --- a/src/shared/sql.rs +++ b/src/shared/sql.rs @@ -57,6 +57,14 @@ pub struct Certification { pub set_by: u32, } +#[derive(Debug, FromRow, Serialize)] +pub struct Activity { + pub id: u32, + pub cid: u32, + pub month: String, + pub minutes: u32, +} + /// Statements to create tables. Only ran when the DB file does not exist, /// so no migration or "IF NOT EXISTS" conditions need to be added. pub const CREATE_TABLES: &str = r#" @@ -156,3 +164,5 @@ VALUES "; pub const DELETE_FROM_ROSTER: &str = "DELETE FROM controller WHERE cid=$1"; + +pub const GET_ALL_ACTIVITY: &str = "SELECT * FROM activity"; diff --git a/templates/activity.jinja b/templates/activity.jinja new file mode 100644 index 0000000..d4a646e --- /dev/null +++ b/templates/activity.jinja @@ -0,0 +1,36 @@ +{% extends "_layout" %} + +{% block title %}Activity | {{ super() }}{% endblock %} + +{% block body %} + +

Activity

+ + + + + + + + + + + + + + {% for row in activity_data %} + + + + + + + + + {% endfor %} + +
WhoThis monthLast month2 months ago3 months ago4 months ago
+ {{ row.name }} {% if row.ois %}({{ row.ois }}){% endif %} + {{ row.month_0|minutes_to_hm }}{{ row.month_1|minutes_to_hm }}{{ row.month_2|minutes_to_hm }}{{ row.month_3|minutes_to_hm }}{{ row.month_4|minutes_to_hm }}
+ +{% endblock %}