Starts reports
This commit is contained in:
parent
9b6bfc76cb
commit
99e39caab7
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -667,10 +667,12 @@ version = "0.4.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
|
checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
"time 0.1.43",
|
"time 0.1.43",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -23,7 +23,7 @@ seed = { version = "0.7.0" }
|
|||||||
serde = "*"
|
serde = "*"
|
||||||
serde_json = "*"
|
serde_json = "*"
|
||||||
bincode = "1.2.1"
|
bincode = "1.2.1"
|
||||||
chrono = { version = "0.4", features = [ "serde" ] }
|
chrono = { version = "0.4", features = [ "serde", "wasmbind" ] }
|
||||||
uuid = { version = "0.8.1", features = [ "serde" ] }
|
uuid = { version = "0.8.1", features = [ "serde" ] }
|
||||||
wasm-bindgen = "0.2.60"
|
wasm-bindgen = "0.2.60"
|
||||||
futures = "^0.1.26"
|
futures = "^0.1.26"
|
||||||
|
@ -19,6 +19,7 @@ mod model;
|
|||||||
mod profile;
|
mod profile;
|
||||||
mod project;
|
mod project;
|
||||||
mod project_settings;
|
mod project_settings;
|
||||||
|
mod reports;
|
||||||
mod shared;
|
mod shared;
|
||||||
mod sign_in;
|
mod sign_in;
|
||||||
mod sign_up;
|
mod sign_up;
|
||||||
@ -193,6 +194,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Page::Invite => invite::update(msg, model, orders),
|
Page::Invite => invite::update(msg, model, orders),
|
||||||
Page::Users => users::update(msg, model, orders),
|
Page::Users => users::update(msg, model, orders),
|
||||||
Page::Profile => profile::update(msg, model, orders),
|
Page::Profile => profile::update(msg, model, orders),
|
||||||
|
Page::Reports => reports::update(msg, model, orders),
|
||||||
}
|
}
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
// debug!(model);
|
// debug!(model);
|
||||||
@ -209,6 +211,7 @@ fn view(model: &model::Model) -> Node<Msg> {
|
|||||||
Page::Invite => invite::view(model),
|
Page::Invite => invite::view(model),
|
||||||
Page::Users => users::view(model),
|
Page::Users => users::view(model),
|
||||||
Page::Profile => profile::view(model),
|
Page::Profile => profile::view(model),
|
||||||
|
Page::Reports => reports::view(model),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,6 +240,7 @@ fn resolve_page(url: Url) -> Option<Page> {
|
|||||||
"register" => Page::SignUp,
|
"register" => Page::SignUp,
|
||||||
"invite" => Page::Invite,
|
"invite" => Page::Invite,
|
||||||
"users" => Page::Users,
|
"users" => Page::Users,
|
||||||
|
"reports" => Page::Reports,
|
||||||
_ => Page::Project,
|
_ => Page::Project,
|
||||||
};
|
};
|
||||||
Some(page)
|
Some(page)
|
||||||
|
@ -215,6 +215,7 @@ pub enum Page {
|
|||||||
Invite,
|
Invite,
|
||||||
Users,
|
Users,
|
||||||
Profile,
|
Profile,
|
||||||
|
Reports,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
@ -229,6 +230,7 @@ impl Page {
|
|||||||
Page::Invite => "/invite".to_string(),
|
Page::Invite => "/invite".to_string(),
|
||||||
Page::Users => "/users".to_string(),
|
Page::Users => "/users".to_string(),
|
||||||
Page::Profile => "/profile".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)]
|
#[derive(Debug)]
|
||||||
pub enum PageContent {
|
pub enum PageContent {
|
||||||
SignIn(Box<SignInPage>),
|
SignIn(Box<SignInPage>),
|
||||||
@ -437,6 +442,7 @@ pub enum PageContent {
|
|||||||
Invite(Box<InvitePage>),
|
Invite(Box<InvitePage>),
|
||||||
Users(Box<UsersPage>),
|
Users(Box<UsersPage>),
|
||||||
Profile(Box<ProfilePage>),
|
Profile(Box<ProfilePage>),
|
||||||
|
Reports(Box<ReportsPage>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
use jirs_data::*;
|
use jirs_data::*;
|
||||||
@ -11,7 +13,6 @@ use crate::shared::styled_input::StyledInput;
|
|||||||
use crate::shared::styled_select::StyledSelect;
|
use crate::shared::styled_select::StyledSelect;
|
||||||
use crate::shared::{inner_layout, ToChild, ToNode};
|
use crate::shared::{inner_layout, ToChild, ToNode};
|
||||||
use crate::{FieldId, Msg, PageChanged, ProfilePageChange};
|
use crate::{FieldId, Msg, PageChanged, ProfilePageChange};
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub fn view(model: &Model) -> Node<Msg> {
|
pub fn view(model: &Model) -> Node<Msg> {
|
||||||
let page = match &model.page_content {
|
let page = match &model.page_content {
|
||||||
@ -75,7 +76,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
.add_field(submit_field)
|
.add_field(submit_field)
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.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<ProfilePage>) -> Node<Msg> {
|
fn build_current_project(model: &Model, page: &Box<ProfilePage>) -> Node<Msg> {
|
||||||
|
@ -19,12 +19,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
project_board_lists(model),
|
project_board_lists(model),
|
||||||
];
|
];
|
||||||
|
|
||||||
inner_layout(
|
inner_layout(model, "projectPage", project_section)
|
||||||
model,
|
|
||||||
"projectPage",
|
|
||||||
project_section,
|
|
||||||
crate::modal::view(model),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn breadcrumbs(model: &Model) -> Node<Msg> {
|
fn breadcrumbs(model: &Model) -> Node<Msg> {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use seed::{prelude::*, *};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
use jirs_data::{IssueStatus, ProjectCategory, TimeTracking, ToVec};
|
use jirs_data::{IssueStatus, ProjectCategory, TimeTracking, ToVec};
|
||||||
|
|
||||||
use crate::model::{DeleteIssueStatusModal, ModalType, Model, PageContent, ProjectSettingsPage};
|
use crate::model::{DeleteIssueStatusModal, ModalType, Model, PageContent, ProjectSettingsPage};
|
||||||
@ -88,12 +89,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
|||||||
|
|
||||||
let project_section = vec![div![class!["formContainer"], form]];
|
let project_section = vec![div![class!["formContainer"], form]];
|
||||||
|
|
||||||
inner_layout(
|
inner_layout(model, "projectSettings", project_section)
|
||||||
model,
|
|
||||||
"projectSettings",
|
|
||||||
project_section,
|
|
||||||
crate::modal::view(model),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build project name input with styled field wrapper
|
/// Build project name input with styled field wrapper
|
||||||
|
@ -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<Msg>) {
|
||||||
|
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<Msg>) {
|
||||||
|
if model.user.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueue_ws_msg(
|
||||||
|
vec![WsMsg::ProjectIssuesRequest, WsMsg::IssueStatusesRequest],
|
||||||
|
model.ws.as_ref(),
|
||||||
|
orders,
|
||||||
|
);
|
||||||
|
}
|
@ -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<Msg> {
|
||||||
|
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<Msg> {
|
||||||
|
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<u32, Vec<&Issue>> = HashMap::new();
|
||||||
|
|
||||||
|
let list: Vec<Node<Msg>> = 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<Node<Msg>> = vec![];
|
||||||
|
let x_origin: Node<Msg> = 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],
|
||||||
|
]
|
||||||
|
}
|
@ -63,7 +63,7 @@ pub fn render(model: &Model) -> Node<Msg> {
|
|||||||
sidebar_link_item(model, "Releases", Icon::Shipping, None),
|
sidebar_link_item(model, "Releases", Icon::Shipping, None),
|
||||||
sidebar_link_item(model, "Issue and Filters", Icon::Issues, None),
|
sidebar_link_item(model, "Issue and Filters", Icon::Issues, None),
|
||||||
sidebar_link_item(model, "Pages", Icon::Page, 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),
|
sidebar_link_item(model, "Components", Icon::Component, None),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -61,12 +61,8 @@ pub fn divider() -> Node<Msg> {
|
|||||||
div![class!["divider"], ""]
|
div![class!["divider"], ""]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inner_layout(
|
pub fn inner_layout(model: &Model, page_name: &str, children: Vec<Node<Msg>>) -> Node<Msg> {
|
||||||
model: &Model,
|
let modal_node = crate::modal::view(model);
|
||||||
page_name: &str,
|
|
||||||
children: Vec<Node<Msg>>,
|
|
||||||
modal_node: Node<Msg>,
|
|
||||||
) -> Node<Msg> {
|
|
||||||
article![
|
article![
|
||||||
modal_node,
|
modal_node,
|
||||||
class!["inner-layout", "innerPage"],
|
class!["inner-layout", "innerPage"],
|
||||||
|
@ -171,6 +171,5 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
model,
|
model,
|
||||||
"users",
|
"users",
|
||||||
vec![form, users_section, invitations_section],
|
vec![form, users_section, invitations_section],
|
||||||
empty![],
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user