242 lines
7.6 KiB
Rust
242 lines
7.6 KiB
Rust
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<sled::Db>) -> Result<RawHtml<Template>, StatusJson> {
|
|
#[derive(Debug, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq)]
|
|
struct Node {
|
|
category: category::V,
|
|
children: BTreeMap<category::K, Node>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct TemplateContext {
|
|
categories: BTreeMap<category::K, Node>,
|
|
}
|
|
|
|
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<category::K, Node> = 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<category::K, category::V>,
|
|
) {
|
|
// 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/<category_uuid>/start_session")]
|
|
pub fn start_session(
|
|
_auth: Authorized,
|
|
category_uuid: Uuid,
|
|
event_notifier: &State<EventNotifier>,
|
|
db: &State<sled::Db>,
|
|
) -> Result<Redirect, StatusJson> {
|
|
api::session::toggle_category_session(category_uuid, true, event_notifier, db)?;
|
|
Ok(Redirect::to(uri!(index)))
|
|
}
|
|
|
|
#[post("/category/<category_uuid>/end_session")]
|
|
pub fn end_session(
|
|
_auth: Authorized,
|
|
category_uuid: Uuid,
|
|
event_notifier: &State<EventNotifier>,
|
|
db: &State<sled::Db>,
|
|
) -> Result<Redirect, StatusJson> {
|
|
api::session::toggle_category_session(category_uuid, false, event_notifier, db)?;
|
|
Ok(Redirect::to(uri!(index)))
|
|
}
|
|
|
|
#[post("/category/<category_uuid>/bump_session/minutes/<minutes>")]
|
|
pub fn bump_session(
|
|
_auth: Authorized,
|
|
category_uuid: Uuid,
|
|
minutes: i64,
|
|
db: &State<sled::Db>,
|
|
) -> Result<Redirect, StatusJson> {
|
|
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/<session_uuid>/edit")]
|
|
pub fn session_edit(
|
|
_auth: Authorized,
|
|
session_uuid: Uuid,
|
|
db: &State<sled::Db>,
|
|
) -> Result<RawHtml<Template>, 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<sled::Db>) -> Result<RawHtml<Template>, 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<HistoryEntryContext>,
|
|
}
|
|
|
|
let categories_tree = db.open_tree(category::NAME)?;
|
|
let sessions_tree = db.open_tree(session::NAME)?;
|
|
|
|
let categories: HashMap<category::K, category::V> = categories_tree
|
|
.iter()
|
|
.map(|result| {
|
|
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
|
|
})
|
|
.collect::<Result<Result<_, _>, _>>()??;
|
|
|
|
let sessions: HashMap<session::K, session::V> = sessions_tree
|
|
.iter()
|
|
.map(|result| {
|
|
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
|
|
})
|
|
.collect::<Result<Result<_, _>, _>>()??;
|
|
|
|
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)))
|
|
}
|