diff --git a/src/main.rs b/src/main.rs index 351f91c..1707c86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod error; mod gitlab; mod openproject; mod planning; +mod utils; use crate::cli::Commands::{Planning, Test}; use crate::cli::Planning::I2work; diff --git a/src/openproject/mod.rs b/src/openproject/mod.rs index 0916fe7..c777d73 100644 --- a/src/openproject/mod.rs +++ b/src/openproject/mod.rs @@ -3,3 +3,4 @@ mod hal; pub(crate) mod root; pub(crate) mod user; pub(crate) mod work; +pub(crate) mod project; diff --git a/src/openproject/project.rs b/src/openproject/project.rs new file mode 100644 index 0000000..2cfecb0 --- /dev/null +++ b/src/openproject/project.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize)] +pub struct ProjectPartial { + href: String, + pub(crate) title: String, +} \ No newline at end of file diff --git a/src/openproject/user.rs b/src/openproject/user.rs index 3e29a3f..a796ed5 100644 --- a/src/openproject/user.rs +++ b/src/openproject/user.rs @@ -8,6 +8,27 @@ pub struct UserLink { #[serde(rename = "self")] pub d_self: Link, } + +#[derive(Deserialize, Debug, Clone)] +pub struct UserPartial { + pub href: Option, + pub title: Option, +} + +impl UserPartial { + pub fn is_null(&self) -> bool { + self.href.is_none() + } +} + +impl PartialEq for UserPartial { + fn eq(&self, other: &Self) -> bool { + self.href == other.href + } +} + + + #[derive(Deserialize, Debug, Clone)] pub struct User { #[serde(rename = "_type")] @@ -18,6 +39,16 @@ pub struct User { pub d_links: UserLink, } +impl PartialEq for User { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for User { + +} + pub trait GetMe { async fn me(&self) -> Result; } diff --git a/src/openproject/work.rs b/src/openproject/work.rs index 2f2bc28..f1c3c92 100644 --- a/src/openproject/work.rs +++ b/src/openproject/work.rs @@ -2,6 +2,15 @@ use crate::openproject::client::{handle_response_status, Client, OpenProjectErro use crate::openproject::hal::Link; use iso8601::Duration; use serde::{Deserialize, Serialize}; +use crate::openproject::project::ProjectPartial; +use crate::openproject::user::{User, UserLink, UserPartial}; + + +#[derive(Deserialize, Debug, Clone)] +pub struct BudgetPartial { + pub(crate) href: Option, + pub(crate) title: Option, +} #[derive(Serialize, Debug)] pub struct WorkPackageWriterAssignee { @@ -22,6 +31,13 @@ pub struct DescriptionWriter { pub(crate) raw: String, } +#[derive(Deserialize, Debug, Clone)] +pub struct WorkPackageLinks { + pub(crate) assignee: Option, + pub(crate) project: ProjectPartial, + pub(crate) budget: BudgetPartial, +} + #[derive(Deserialize, Debug, Clone)] pub struct WorkPackage { pub id: u64, @@ -30,6 +46,8 @@ pub struct WorkPackage { pub estimated_time: Option, #[serde(alias = "spentTime")] pub spent_time: Option, + #[serde(alias = "_links")] + pub links: WorkPackageLinks, } #[derive(Deserialize, Debug)] diff --git a/src/planning/planning_load.rs b/src/planning/planning_load.rs index 3cdb5bb..d369f61 100644 --- a/src/planning/planning_load.rs +++ b/src/planning/planning_load.rs @@ -1,15 +1,138 @@ +use std::hash::Hash; + +use tabled::settings::Style; + use crate::config::Config; use crate::error::GeneralError; use crate::openproject::client::Client; -use crate::openproject::work::WorkPackageCollectionClient; +use crate::openproject::user::UserPartial; +use crate::openproject::work::{WorkPackage, WorkPackageCollectionClient}; + +enum CellContent { + S(String), + N(f64), +} + +impl From for String { + fn from(value: CellContent) -> Self { + match value { + CellContent::S(s) => s, + CellContent::N(n) => n.to_string() + } + } +} + +struct UserList { + users: Vec, +} + +impl UserList { + fn from_work_packages(work_packages: &Vec) -> Self { + let mut users = vec![]; + for w in work_packages { + if w.links.assignee.is_none() { + continue; + } + + let u: &UserPartial = w.links.assignee.as_ref().unwrap(); + + if users.contains(u) { + continue; + } + + users.push(u.clone()); + } + + users.sort_by(|a, b| a.href.partial_cmp(&b.href).unwrap()); + + UserList { users } + } +} + +mod table_load { + use tabled::builder::Builder; + + use crate::openproject::work::WorkPackage; + use crate::planning::planning_load::{CellContent, UserList}; + use crate::utils::duration_to_seconds_f64; + + pub struct Table { + work_packages: Vec, + user_list: UserList, + } + + impl<'a> Table { + pub fn new(mut work_packages: Vec) -> Self { + let user_list = UserList::from_work_packages(&work_packages); + work_packages.sort_by(|a, b| { + + if a.links.project.title != b.links.project.title { + return a.links.project.title.partial_cmp(&b.links.project.title).unwrap(); + } + + if a.links.budget.href.as_ref().unwrap_or(&"".to_string()) != b.links.budget.href.as_ref().unwrap_or(&"".to_string()) { + return a.links.budget.href.as_ref().unwrap_or(&"".to_string()).partial_cmp( + b.links.budget.href.as_ref().unwrap_or(&"".to_string()) + ).unwrap(); + } + + return a.id.partial_cmp(&b.id).unwrap(); + }); + + Table {work_packages, user_list} + } + + pub fn to_rows(&self) -> tabled::Table { + let mut builder = Builder::default(); + let mut header = vec![ + "Projet".to_string(), + "Buddget".to_string(), + "Tâche".to_string() + ]; + for u in &self.user_list.users { + header.push(u.title.as_ref().unwrap_or(&"".to_string()).clone()); + } + header.push("Non assigné".to_string()); + builder.push_record(header); + + for work_package in &self.work_packages { + let mut row = Vec::with_capacity(3 + self.user_list.users.len() + 1); + row.push(CellContent::S(work_package.links.project.title.clone())); + row.push(CellContent::S(work_package.links.budget.title.as_ref().unwrap_or(&"".to_string()).clone())); + row.push(CellContent::S(format!("{} ({})", work_package.subject, work_package.id))); + for u in &self.user_list.users { + if work_package.links.assignee.is_some() && work_package.links.assignee.as_ref().unwrap().eq(u) { + row.push({ + match work_package.estimated_time { + Some(duration) => CellContent::N(duration_to_seconds_f64(&duration)/3_600_f64), + None => CellContent::S("".to_string()) + } + }); + } else { + row.push(CellContent::S("".to_string())); + } + if work_package.links.assignee.is_none() { + row.push(CellContent::S("".to_string())); + } + } + builder.push_record(row); + } + + builder.build() + } + } +} pub async fn planning_load(config: Config) -> Result<(), GeneralError> { let open_project_client = Client::from_config(&config.openproject); let work_packages = open_project_client.work_package().await?; - for w in work_packages { - println!("{:?}", w); - } + + let table_data = table_load::Table::new(work_packages); + let mut table = table_data.to_rows(); + table.with(Style::markdown()); + + println!("{}", table); Ok(()) } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..e94088e --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,29 @@ +use iso8601::Duration; +use iso8601::Duration::{Weeks, YMDHMS}; + +pub fn duration_to_seconds_f64(duration: &Duration) -> f64 { + if duration.is_zero() { + return 0f64; + } + + match duration { + Weeks(w) => f64::from(*w) * 24f64 * 7f64 * 3600f64, + YMDHMS{year, month, day, hour, minute, second, millisecond} => + f64::from(*day) * 86400f64 + + f64::from(*hour) * 3600f64 + + f64::from(*minute) * 60f64 + + f64::from(*second) + } +} + +pub fn empty_duration() -> Duration { + YMDHMS { + year: 0, + month: 0, + day: 0, + hour: 0, + minute: 0, + second: 0, + millisecond: 0 + } +} \ No newline at end of file