pub mod dailies; pub mod stats; pub mod weeks; use crate::auth::Authorized; use crate::database::latest::trees::{category, session}; use crate::routes::api; use crate::status_json::StatusJson; use crate::util::EventNotifier; use bincode::{deserialize, serialize}; use rocket::http::Status; use rocket::response::content::RawHtml; use rocket::response::Redirect; use rocket::serde::uuid::Uuid; use rocket::{get, post, uri, State}; use rocket_dyn_templates::Template; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::time::Duration; #[get("/")] pub fn index(_auth: Authorized, db: &State) -> Result, StatusJson> { #[derive(Debug, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq)] struct Node { category: category::V, children: BTreeMap, } #[derive(Debug, Serialize, Deserialize)] struct TemplateContext { categories: BTreeMap, } let categories_tree = db.open_tree(category::NAME)?; let mut categories = category::get_all(&categories_tree)?; // filter archived categories categories.retain(|_, category| !category.deleted); // collect the top-level categories (those without a parent) let mut top_level_nodes: BTreeMap = categories .iter() .filter(|(_, c)| c.parent.is_none()) .map(|(&id, category)| { let node = Node { category: category.clone(), children: Default::default(), }; (id, node) }) .collect(); // remove top-level categories from the list for id in top_level_nodes.keys() { categories.remove(id); } /// populate `node.children with entries from `remaining` fn populate_node( node_id: category::K, node: &mut Node, remaining: &mut HashMap, ) { // make a list of the nodes children let mut new_children = vec![]; for (&id, category) in remaining.iter() { if category.parent == Some(node_id) { new_children.push(id); } } // move the children from `remaining` to `node.children` for &id in &new_children { let child_node = Node { category: remaining.remove(&id).unwrap(), children: Default::default(), }; node.children.insert(id, child_node); } // recursively populate the childrens children for (child_id, child) in node.children.iter_mut() { populate_node(*child_id, child, remaining); } } for (id, node) in top_level_nodes.iter_mut() { populate_node(*id, node, &mut categories); } let context = TemplateContext { categories: top_level_nodes, }; Ok(RawHtml(Template::render("index", &context))) } #[post("/category//start_session")] pub fn start_session( _auth: Authorized, category_uuid: Uuid, event_notifier: &State, db: &State, ) -> Result { api::session::toggle_category_session(category_uuid, true, event_notifier, db)?; Ok(Redirect::to(uri!(index))) } #[post("/category//end_session")] pub fn end_session( _auth: Authorized, category_uuid: Uuid, event_notifier: &State, db: &State, ) -> Result { api::session::toggle_category_session(category_uuid, false, event_notifier, db)?; Ok(Redirect::to(uri!(index))) } #[post("/category//bump_session/minutes/")] pub fn bump_session( _auth: Authorized, category_uuid: Uuid, minutes: i64, db: &State, ) -> Result { use crate::database::latest::trees::category; let duration = chrono::Duration::minutes(minutes); let category_uuid_s = sled::IVec::from(serialize(&category_uuid)?); let categories_tree = db.open_tree(category::NAME)?; (&categories_tree).transaction(|tx_categories| { match tx_categories.get(&category_uuid_s)? { None => Ok(Err(Status::NotFound.into())), Some(data) => { let mut category: category::V = deserialize(&data).unwrap(); match category.started.as_mut() { Some(started) => { if let Some(new_started) = started.checked_sub_signed(duration) { *started = new_started; tx_categories .insert(&category_uuid_s, serialize(&category).unwrap())?; } else { return Ok(Err(StatusJson::new( Status::BadRequest, "Duration subtract resulted in overflow", ))); } Ok(Ok(Redirect::to(uri!(index)))) } None => Ok(Err(StatusJson::new( Status::BadRequest, "No active session", ))), } } } })? } #[get("/session//edit")] pub fn session_edit( _auth: Authorized, session_uuid: Uuid, db: &State, ) -> Result, StatusJson> { #[derive(Debug, Serialize, Deserialize)] struct SessionPageContext { session: session::V, session_id: uuid::Uuid, } let session_uuid_s = sled::IVec::from(serialize(&session_uuid)?); let sessions_tree = db.open_tree(session::NAME)?; match sessions_tree.get(session_uuid_s)? { None => Err(Status::NotFound.into()), Some(data) => { let context = SessionPageContext { session: deserialize(&data).unwrap(), session_id: session_uuid, }; Ok(RawHtml(Template::render("edit_session", &context))) } } } #[get("/history")] pub fn history(_auth: Authorized, db: &State) -> Result, StatusJson> { #[derive(Debug, Serialize, Deserialize)] struct HistoryEntryContext { category: category::V, session: session::V, session_id: uuid::Uuid, duration: Duration, } #[derive(Debug, Serialize, Deserialize)] struct TemplateContext { entries: Vec, } let categories_tree = db.open_tree(category::NAME)?; let sessions_tree = db.open_tree(session::NAME)?; let categories: HashMap = categories_tree .iter() .map(|result| { result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v)))) }) .collect::, _>>()??; let sessions: HashMap = sessions_tree .iter() .map(|result| { result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v)))) }) .collect::, _>>()??; let mut context = TemplateContext { entries: sessions .into_iter() .map(|(session_id, session)| HistoryEntryContext { duration: (session.ended - session.started).to_std().unwrap(), category: categories.get(&session.category).unwrap().clone(), session, session_id, }) .collect(), }; // Newest entries first context.entries.sort_by_key(|entry| entry.session.started); context.entries.reverse(); Ok(RawHtml(Template::render("history", &context))) }