diff --git a/Cargo.lock b/Cargo.lock index 46462dbe..94022243 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -667,10 +667,12 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" dependencies = [ + "js-sys", "num-integer", "num-traits", "serde", "time 0.1.43", + "wasm-bindgen", ] [[package]] diff --git a/jirs-client/Cargo.toml b/jirs-client/Cargo.toml index 169ea567..1930bff2 100644 --- a/jirs-client/Cargo.toml +++ b/jirs-client/Cargo.toml @@ -23,7 +23,7 @@ seed = { version = "0.7.0" } serde = "*" serde_json = "*" bincode = "1.2.1" -chrono = { version = "0.4", features = [ "serde" ] } +chrono = { version = "0.4", features = [ "serde", "wasmbind" ] } uuid = { version = "0.8.1", features = [ "serde" ] } wasm-bindgen = "0.2.60" futures = "^0.1.26" diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 4d5e9c35..fb991ea7 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -19,6 +19,7 @@ mod model; mod profile; mod project; mod project_settings; +mod reports; mod shared; mod sign_in; mod sign_up; @@ -193,6 +194,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { Page::Invite => invite::update(msg, model, orders), Page::Users => users::update(msg, model, orders), Page::Profile => profile::update(msg, model, orders), + Page::Reports => reports::update(msg, model, orders), } if cfg!(debug_assertions) { // debug!(model); @@ -209,6 +211,7 @@ fn view(model: &model::Model) -> Node { Page::Invite => invite::view(model), Page::Users => users::view(model), Page::Profile => profile::view(model), + Page::Reports => reports::view(model), } } @@ -237,6 +240,7 @@ fn resolve_page(url: Url) -> Option { "register" => Page::SignUp, "invite" => Page::Invite, "users" => Page::Users, + "reports" => Page::Reports, _ => Page::Project, }; Some(page) diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 10226cb6..d908ab27 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -215,6 +215,7 @@ pub enum Page { Invite, Users, Profile, + Reports, } impl Page { @@ -229,6 +230,7 @@ impl Page { Page::Invite => "/invite".to_string(), Page::Users => "/users".to_string(), Page::Profile => "/profile".to_string(), + Page::Reports => "/reports".to_string(), } } } @@ -428,6 +430,9 @@ impl ProfilePage { } } +#[derive(Debug)] +pub struct ReportsPage {} + #[derive(Debug)] pub enum PageContent { SignIn(Box), @@ -437,6 +442,7 @@ pub enum PageContent { Invite(Box), Users(Box), Profile(Box), + Reports(Box), } #[derive(Debug)] diff --git a/jirs-client/src/profile/view.rs b/jirs-client/src/profile/view.rs index 6b19ec43..b77b2368 100644 --- a/jirs-client/src/profile/view.rs +++ b/jirs-client/src/profile/view.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use seed::{prelude::*, *}; use jirs_data::*; @@ -11,7 +13,6 @@ use crate::shared::styled_input::StyledInput; use crate::shared::styled_select::StyledSelect; use crate::shared::{inner_layout, ToChild, ToNode}; use crate::{FieldId, Msg, PageChanged, ProfilePageChange}; -use std::collections::HashMap; pub fn view(model: &Model) -> Node { let page = match &model.page_content { @@ -75,7 +76,7 @@ pub fn view(model: &Model) -> Node { .add_field(submit_field) .build() .into_node(); - inner_layout(model, "profile", vec![content], crate::modal::view(model)) + inner_layout(model, "profile", vec![content]) } fn build_current_project(model: &Model, page: &Box) -> Node { diff --git a/jirs-client/src/project/view.rs b/jirs-client/src/project/view.rs index cce07afe..2d2dbd65 100644 --- a/jirs-client/src/project/view.rs +++ b/jirs-client/src/project/view.rs @@ -19,12 +19,7 @@ pub fn view(model: &Model) -> Node { project_board_lists(model), ]; - inner_layout( - model, - "projectPage", - project_section, - crate::modal::view(model), - ) + inner_layout(model, "projectPage", project_section) } fn breadcrumbs(model: &Model) -> Node { diff --git a/jirs-client/src/project_settings/view.rs b/jirs-client/src/project_settings/view.rs index 1ef8aca0..3b14728d 100644 --- a/jirs-client/src/project_settings/view.rs +++ b/jirs-client/src/project_settings/view.rs @@ -1,6 +1,7 @@ -use seed::{prelude::*, *}; use std::collections::HashMap; +use seed::{prelude::*, *}; + use jirs_data::{IssueStatus, ProjectCategory, TimeTracking, ToVec}; use crate::model::{DeleteIssueStatusModal, ModalType, Model, PageContent, ProjectSettingsPage}; @@ -88,12 +89,7 @@ pub fn view(model: &model::Model) -> Node { let project_section = vec![div![class!["formContainer"], form]]; - inner_layout( - model, - "projectSettings", - project_section, - crate::modal::view(model), - ) + inner_layout(model, "projectSettings", project_section) } /// Build project name input with styled field wrapper diff --git a/jirs-client/src/reports/update.rs b/jirs-client/src/reports/update.rs index e69de29b..4af1f23c 100644 --- a/jirs-client/src/reports/update.rs +++ b/jirs-client/src/reports/update.rs @@ -0,0 +1,49 @@ +use seed::prelude::*; + +use jirs_data::WsMsg; + +use crate::model::{Model, Page, PageContent, ReportsPage}; +use crate::ws::enqueue_ws_msg; +use crate::{Msg, WebSocketChanged}; + +pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { + match msg { + Msg::ChangePage(Page::Reports) => { + build_page_content(model); + } + _ => (), + } + + let _page = match &mut model.page_content { + PageContent::Reports(page) => page, + _ => return, + }; + + if model.user.is_none() { + return; + } + match msg { + Msg::UserChanged(Some(..)) + | Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..))) + | Msg::ChangePage(Page::Reports) => { + init_load(model, orders); + } + _ => {} + } +} + +fn build_page_content(model: &mut Model) { + model.page_content = PageContent::Reports(Box::new(ReportsPage {})) +} + +fn init_load(model: &mut Model, orders: &mut impl Orders) { + if model.user.is_none() { + return; + } + + enqueue_ws_msg( + vec![WsMsg::ProjectIssuesRequest, WsMsg::IssueStatusesRequest], + model.ws.as_ref(), + orders, + ); +} diff --git a/jirs-client/src/reports/view.rs b/jirs-client/src/reports/view.rs index e69de29b..86d0c458 100644 --- a/jirs-client/src/reports/view.rs +++ b/jirs-client/src/reports/view.rs @@ -0,0 +1,95 @@ +use std::collections::HashMap; + +use chrono::Datelike; +use seed::{prelude::*, *}; + +use jirs_data::Issue; + +use crate::model::Model; +use crate::shared::inner_layout; +use crate::Msg; + +const SVG_MARGIN_X: u32 = 5; +const SVG_DRAWABLE_HEIGHT: u32 = 300; +const SVG_HEIGHT: u32 = SVG_DRAWABLE_HEIGHT + 30; +const SVG_WIDTH: u32 = 1060; +const SVG_BAR_WIDTH: u32 = 25; +const SVG_BAR_MARGIN: u32 = 10; + +pub fn view(model: &Model) -> Node { + let body = section![ + h1![class!["header"], "Reports"], + div![this_month_graph(model)], + ]; + + inner_layout(model, "reports", vec![body]) +} + +fn this_month_graph(model: &Model) -> Node { + let first_day = chrono::Utc::today().with_day(1).unwrap().naive_local(); + let last_day = (first_day + chrono::Duration::days(32)) + .with_day(1) + .unwrap() + - chrono::Duration::days(1); + + let this_month_updated: Vec<&Issue> = model + .issues + .iter() + .filter(|issue| issue.updated_at.date() >= first_day) + .collect(); + + let mut issues: HashMap> = HashMap::new(); + + let list: Vec> = this_month_updated + .into_iter() + .map(|issue| { + let date = issue.updated_at.date(); + issues.entry(date.day0()).or_default().push(issue); + let day = issue.updated_at.date().format("%Y-%m-%d").to_string(); + li![span![issue.title.as_str()], span![day.as_str()]] + }) + .collect(); + + let mut columns: Vec> = vec![]; + let x_origin: Node = seed::rect![attrs![ + At::X => SVG_MARGIN_X, + At::Y => SVG_HEIGHT - SVG_MARGIN_X - 20, + At::Width => SVG_WIDTH - (SVG_MARGIN_X * 2), + At::Height => 2, + At::Style => "fill: var(--textDark);" + ]]; + + for day in (first_day.day0())..(last_day.day0()) { + let x = (SVG_BAR_WIDTH * day) + (SVG_BAR_MARGIN * day) + (SVG_MARGIN_X * 2); + let num_issues = issues.get(&day).map(|v| v.len()).unwrap_or_default() as u32; + let height = num_issues * SVG_BAR_WIDTH; + + let day = first_day.with_day0(day).unwrap(); + + columns.push(seed::rect![attrs![ + At::X => x, + At::Y => SVG_DRAWABLE_HEIGHT - height, // reverse draw origin + At::Width => "10", + At::Height => height, + At::Style => "fill: rgb(255,0,0);", + At::Title => format!("Number of issues: {}", num_issues), + ]]); + columns.push(seed::text![ + attrs![ + At::X => x, + At::Y => SVG_HEIGHT, + At::Style => "fill: var(--textDark); font-family: var(--font-regular); font-size: 10px;", + ], + day.format("%d/%m").to_string(), + ]); + } + + div![ + svg![ + attrs![At::Height => SVG_HEIGHT, At::Width => SVG_WIDTH], + x_origin, + columns, + ], + ul![list], + ] +} diff --git a/jirs-client/src/shared/aside.rs b/jirs-client/src/shared/aside.rs index 2e11eac6..9919eefd 100644 --- a/jirs-client/src/shared/aside.rs +++ b/jirs-client/src/shared/aside.rs @@ -63,7 +63,7 @@ pub fn render(model: &Model) -> Node { sidebar_link_item(model, "Releases", Icon::Shipping, None), sidebar_link_item(model, "Issue and Filters", Icon::Issues, None), sidebar_link_item(model, "Pages", Icon::Page, None), - sidebar_link_item(model, "Reports", Icon::Reports, None), + sidebar_link_item(model, "Reports", Icon::Reports, Some(Page::Reports)), sidebar_link_item(model, "Components", Icon::Component, None), ]); diff --git a/jirs-client/src/shared/mod.rs b/jirs-client/src/shared/mod.rs index 96ce3aee..491d596b 100644 --- a/jirs-client/src/shared/mod.rs +++ b/jirs-client/src/shared/mod.rs @@ -61,12 +61,8 @@ pub fn divider() -> Node { div![class!["divider"], ""] } -pub fn inner_layout( - model: &Model, - page_name: &str, - children: Vec>, - modal_node: Node, -) -> Node { +pub fn inner_layout(model: &Model, page_name: &str, children: Vec>) -> Node { + let modal_node = crate::modal::view(model); article![ modal_node, class!["inner-layout", "innerPage"], diff --git a/jirs-client/src/users/view.rs b/jirs-client/src/users/view.rs index 43a9133b..9b131b96 100644 --- a/jirs-client/src/users/view.rs +++ b/jirs-client/src/users/view.rs @@ -171,6 +171,5 @@ pub fn view(model: &Model) -> Node { model, "users", vec![form, users_section, invitations_section], - empty![], ) }