First impl of planning load

This commit is contained in:
Julien Fastré 2024-05-29 23:12:52 +02:00
parent b49c08aed3
commit 2d103e2295
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
7 changed files with 214 additions and 4 deletions

View File

@ -10,6 +10,7 @@ mod error;
mod gitlab; mod gitlab;
mod openproject; mod openproject;
mod planning; mod planning;
mod utils;
use crate::cli::Commands::{Planning, Test}; use crate::cli::Commands::{Planning, Test};
use crate::cli::Planning::I2work; use crate::cli::Planning::I2work;

View File

@ -3,3 +3,4 @@ mod hal;
pub(crate) mod root; pub(crate) mod root;
pub(crate) mod user; pub(crate) mod user;
pub(crate) mod work; pub(crate) mod work;
pub(crate) mod project;

View File

@ -0,0 +1,7 @@
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
pub struct ProjectPartial {
href: String,
pub(crate) title: String,
}

View File

@ -8,6 +8,27 @@ pub struct UserLink {
#[serde(rename = "self")] #[serde(rename = "self")]
pub d_self: Link, pub d_self: Link,
} }
#[derive(Deserialize, Debug, Clone)]
pub struct UserPartial {
pub href: Option<String>,
pub title: Option<String>,
}
impl UserPartial {
pub fn is_null(&self) -> bool {
self.href.is_none()
}
}
impl PartialEq<Self> for UserPartial {
fn eq(&self, other: &Self) -> bool {
self.href == other.href
}
}
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct User { pub struct User {
#[serde(rename = "_type")] #[serde(rename = "_type")]
@ -18,6 +39,16 @@ pub struct User {
pub d_links: UserLink, pub d_links: UserLink,
} }
impl PartialEq<Self> for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for User {
}
pub trait GetMe { pub trait GetMe {
async fn me(&self) -> Result<User, OpenProjectError>; async fn me(&self) -> Result<User, OpenProjectError>;
} }

View File

@ -2,6 +2,15 @@ use crate::openproject::client::{handle_response_status, Client, OpenProjectErro
use crate::openproject::hal::Link; use crate::openproject::hal::Link;
use iso8601::Duration; use iso8601::Duration;
use serde::{Deserialize, Serialize}; 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<String>,
pub(crate) title: Option<String>,
}
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct WorkPackageWriterAssignee { pub struct WorkPackageWriterAssignee {
@ -22,6 +31,13 @@ pub struct DescriptionWriter {
pub(crate) raw: String, pub(crate) raw: String,
} }
#[derive(Deserialize, Debug, Clone)]
pub struct WorkPackageLinks {
pub(crate) assignee: Option<UserPartial>,
pub(crate) project: ProjectPartial,
pub(crate) budget: BudgetPartial,
}
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct WorkPackage { pub struct WorkPackage {
pub id: u64, pub id: u64,
@ -30,6 +46,8 @@ pub struct WorkPackage {
pub estimated_time: Option<Duration>, pub estimated_time: Option<Duration>,
#[serde(alias = "spentTime")] #[serde(alias = "spentTime")]
pub spent_time: Option<Duration>, pub spent_time: Option<Duration>,
#[serde(alias = "_links")]
pub links: WorkPackageLinks,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]

View File

@ -1,15 +1,138 @@
use std::hash::Hash;
use tabled::settings::Style;
use crate::config::Config; use crate::config::Config;
use crate::error::GeneralError; use crate::error::GeneralError;
use crate::openproject::client::Client; 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<CellContent> for String {
fn from(value: CellContent) -> Self {
match value {
CellContent::S(s) => s,
CellContent::N(n) => n.to_string()
}
}
}
struct UserList {
users: Vec<UserPartial>,
}
impl UserList {
fn from_work_packages(work_packages: &Vec<WorkPackage>) -> 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<WorkPackage>,
user_list: UserList,
}
impl<'a> Table {
pub fn new(mut work_packages: Vec<WorkPackage>) -> 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> { pub async fn planning_load(config: Config) -> Result<(), GeneralError> {
let open_project_client = Client::from_config(&config.openproject); let open_project_client = Client::from_config(&config.openproject);
let work_packages = open_project_client.work_package().await?; 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(()) Ok(())
} }

29
src/utils.rs Normal file
View File

@ -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
}
}