From 42a4107162d52e9891f376926c7572b050eecc1d Mon Sep 17 00:00:00 2001 From: Pierre de Lacroix Date: Sun, 28 Jun 2026 14:46:13 +0200 Subject: [PATCH] allow adding custom shifts --- src/command.rs | 39 ++++++++- src/grist.rs | 215 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 226 insertions(+), 28 deletions(-) diff --git a/src/command.rs b/src/command.rs index 14b3289..a7a942c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,4 +1,5 @@ use crate::grist::{Priority, Shift}; +use chrono::{DateTime, FixedOffset, TimeZone}; use clap::{CommandFactory, Parser, Subcommand}; use matrix_sdk::{Client, Room}; @@ -25,15 +26,38 @@ enum Action { name: String, /// Date de début, sous ce format : J#-HH-MM (ex: J1-14-30) - #[arg(id = "DATE", short = 'd', long = "date")] - starting_time: String, + #[arg(id = "DATE", short = 't', long = "date", value_parser= parse_date)] + start_time: DateTime, /// Priorité #[arg(id = "PRIORITÉ", short = 'p', long = "priorité", value_enum)] priority: Priority, + + /// Nombre de personnes + #[arg(id = "PERSONNES", short = 'n', long = "personnes")] + persons: u64, + + /// Durée + #[arg(id = "DURÉE", short = 'd', long = "durée")] + duration: u64, + + /// Guide + #[arg(id = "GUIDE", short = 'g', long = "guide")] + guide: String, }, } +fn parse_date(input: &str) -> Res> { + let day_index = &input[1..2].parse::()?; + let hour = &input[3..5].parse::()?; + let minute = &input[6..8].parse::()?; + FixedOffset::east_opt(2 * 3600) + .ok_or(anyhow!("failed to local time 1"))? + .with_ymd_and_hms(2026, 07, 01 + day_index, *hour, *minute, 0) + .single() + .ok_or(anyhow!("failed to local time 2")) +} + pub async fn send_room_msg(_client: Client, room: Room, content: String) -> Res { use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; let content = RoomMessageEventContent::text_plain(content); @@ -84,10 +108,19 @@ pub async fn handle(command: impl AsRef, room: Room, client: Client) -> Res } Action::Add { name, - starting_time, + start_time, priority, + persons, + duration, + guide, } => { debug!("user asked for add"); + let shift = + Shift::create_custom(name, start_time, priority, persons, duration, guide); + debug!("{:?}", shift); + shift.save_new_custom().await?; + send_room_msg_html(client, room, format!("Nouveau shift ajouté : {}", shift)) + .await?; } } } diff --git a/src/grist.rs b/src/grist.rs index 7ba81bb..24b3416 100644 --- a/src/grist.rs +++ b/src/grist.rs @@ -1,11 +1,13 @@ prelude! {} -use chrono::DateTime; +use chrono::{DateTime, FixedOffset}; use clap::ValueEnum; use clap::builder::PossibleValue; use grist_client::apis::configuration::Configuration; -use grist_client::apis::records_api::list_records; -use grist_client::models::{RecordsList, RecordsListRecordsInner}; +use grist_client::apis::records_api::{add_records, list_records}; +use grist_client::models::{ + RecordsList, RecordsListRecordsInner, RecordsWithoutId, RecordsWithoutIdRecordsInner, +}; pub const API_URL: &str = "https://grist.interhacker.space/api"; pub const SHIFTS_DOC: &str = "iwSaC82TsQiuD3MYSG9RXX"; @@ -21,20 +23,36 @@ pub enum Priority { Urgent, } +impl Priority { + pub fn to_grist(&self) -> String { + match self { + Self::Prioritaire => "1 - Prioritaire".to_string(), + Self::Secondaire => "2 - Secondaire".to_string(), + Self::Bonus => "3 - Bonus".to_string(), + Self::Urgent => "0 - URGENT".to_string(), + } + } +} + impl Display for Priority { fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes { match self { Self::Prioritaire => write!(f, "Prioritaire"), Self::Secondaire => write!(f, "Secondaire"), Self::Bonus => write!(f, "Bonus"), - Self::Urgent => write!(f, "Prioritaire"), + Self::Urgent => write!(f, "Urgent"), } } } impl ValueEnum for Priority { fn value_variants<'a>() -> &'a [Self] { - &[Self::Prioritaire, Self::Secondaire, Self::Bonus, Self::Urgent] + &[ + Self::Prioritaire, + Self::Secondaire, + Self::Bonus, + Self::Urgent, + ] } fn to_possible_value(&self) -> Option { @@ -80,43 +98,168 @@ impl Display for ShiftType { fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes { write!( f, - "{} ({}): {} x/j, {} personnes, {} h", + "{} ({}): {} x/j, {} personnes, {} h", self.name, self.priority, self.frequency, self.persons, self.duration ) } } +#[derive(Clone, Debug)] +pub struct CustomShiftValues { + pub name: String, + pub priority: Priority, + pub persons: u64, + pub duration: u64, + pub guide: String, +} +impl CustomShiftValues { + pub fn new( + name: impl Into, + priority: Priority, + persons: u64, + duration: u64, + guide: impl Into, + ) -> Self { + Self { + name: name.into(), + priority: priority, + persons: persons, + duration: duration, + guide: guide.into(), + } + } +} + +#[derive(Clone, Debug)] +pub enum CustomOrDefined { + Defined(ShiftType), + Custom(CustomShiftValues), +} + // {"Autres_infos": Null, "Heure_de_debut": Number(1782144000), "Inscriptions": Array [String("L"), Number(1)], "Type_de_shift": Number(23)} #[derive(Clone, Debug)] pub struct Shift { - pub shift_type: ShiftType, - pub start_time: u64, + pub shift_type: CustomOrDefined, + pub start_time: DateTime, pub inscriptions: Vec, } impl Shift { - pub fn new(shift_type: ShiftType, start_time: u64, inscriptions: Vec) -> Self { + pub fn new(shift_type: CustomOrDefined, start_time: u64, inscriptions: Vec) -> Self { Self { shift_type: shift_type, - start_time: start_time.into(), + start_time: DateTime::from_timestamp(start_time as i64, 0) + .expect("failed to parse timestamp") + .with_timezone( + &FixedOffset::east_opt(2 * 3600).expect("failed to create timezone"), + ), inscriptions: inscriptions, } } + + pub fn create_custom( + name: String, + start_time: DateTime, + priority: Priority, + persons: u64, + duration: u64, + guide: impl Into, + ) -> Self { + Self { + shift_type: CustomOrDefined::Custom(CustomShiftValues::new( + name, priority, persons, duration, guide, + )), + start_time: start_time, + inscriptions: vec![], + } + } + + pub fn name(&self) -> String { + match &self.shift_type { + CustomOrDefined::Defined(shift_type) => shift_type.name.clone(), + CustomOrDefined::Custom(values) => values.name.clone(), + } + } + + pub fn priority(&self) -> Priority { + match &self.shift_type { + CustomOrDefined::Defined(shift_type) => shift_type.priority.clone(), + CustomOrDefined::Custom(values) => values.priority.clone(), + } + } + + pub fn duration(&self) -> u64 { + match &self.shift_type { + CustomOrDefined::Defined(shift_type) => shift_type.duration.clone(), + CustomOrDefined::Custom(values) => values.duration.clone(), + } + } + + pub fn persons(&self) -> u64 { + match &self.shift_type { + CustomOrDefined::Defined(shift_type) => shift_type.persons.clone(), + CustomOrDefined::Custom(values) => values.persons.clone(), + } + } + + pub async fn save_new_custom(&self) -> Res<()> { + let conf = conf::get(); + let api_key = conf.grist_api_key(); + let grist_conf = Configuration::new(API_URL.into(), Some(api_key.into())); + let mut fields = serde_json::Map::new(); + fields.insert("Type_de_shift".to_string(), serde_json::to_value(27)?); // 17 is custom type id + fields.insert( + "Heure_de_debut".to_string(), + serde_json::to_value(self.start_time)?, + ); + if let CustomOrDefined::Custom(values) = &self.shift_type { + fields.insert( + "Nom_Custom".to_string(), + serde_json::to_value(values.name.as_str())?, + ); + fields.insert( + "Priorite_Custom".to_string(), + serde_json::to_value(values.priority.to_grist())?, + ); + fields.insert( + "Personnes_Custom".to_string(), + serde_json::to_value(values.persons)?, + ); + fields.insert( + "Duree_Custom".to_string(), + serde_json::to_value(values.duration)?, + ); + fields.insert( + "Guide_Custom".to_string(), + serde_json::to_value(values.guide.as_str())?, + ); + } + let record = RecordsWithoutIdRecordsInner { + fields: fields.into(), + }; + let records = RecordsWithoutId { + records: vec![record], + }; + + add_records(&grist_conf, SHIFTS_DOC, SHIFTS_TABLE, records, None) + .await + .context("Failed to add shift record")?; + + Ok(()) + } } impl Display for Shift { fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes { - let start_time = DateTime::from_timestamp(self.start_time as i64, 0) - .expect("failed to parse timestamp") - .format("%H:%M"); + let start_time = self.start_time.format("le %d à %H:%M"); write!( f, - "{} ({}): à {}, durée {} h, inscrit·es : {}/{}", - self.shift_type.name, - self.shift_type.priority, + "{} ({}): {}, durée {} h, inscrit·es : {}/{}", + self.name(), + self.priority(), start_time, - self.shift_type.duration, + self.duration(), self.inscriptions.len(), - self.shift_type.persons + self.persons() ) } } @@ -146,7 +289,7 @@ pub fn get_priority(record: &RecordsListRecordsInner, column: &str) -> Res Ok(Priority::Secondaire), s if s.starts_with("3") => Ok(Priority::Bonus), s if s.starts_with("0") => Ok(Priority::Urgent), - _ => Err(anyhow!("can't parse priority")), + _ => Ok(Priority::Bonus), } } @@ -172,17 +315,17 @@ pub fn get_number_array(record: RecordsListRecordsInner, column: &str) -> Res Res { +pub async fn get_records(doc: &str, table: &str, sort: Option<&str>) -> Res { let conf = conf::get(); let api_key = conf.grist_api_key(); let grist_conf = Configuration::new(API_URL.into(), Some(api_key.into())); - list_records(&grist_conf, doc, table, None, None, None, None, None, None) + list_records(&grist_conf, doc, table, None, sort, None, None, None, None) .await .context("Failed to list records") } pub async fn get_shift_type(index: u64) -> Res { - let record_list = get_records(SHIFTS_DOC, SHIFT_TYPES_TABLE).await?; + let record_list = get_records(SHIFTS_DOC, SHIFT_TYPES_TABLE, None).await?; let record = record_list .records .iter() @@ -199,7 +342,7 @@ pub async fn get_shift_type(index: u64) -> Res { } pub async fn get_inscription(index: u64) -> Res { - let record_list = get_records(SHIFTS_DOC, INSCRIPTIONS_TABLE).await?; + let record_list = get_records(SHIFTS_DOC, INSCRIPTIONS_TABLE, None).await?; let record = record_list .records .iter() @@ -210,14 +353,36 @@ pub async fn get_inscription(index: u64) -> Res { Ok(name) } +pub fn get_shift_type_custom(record: &RecordsListRecordsInner) -> Res { + // Ok(CustomShiftValues::new( + // get_string(&record, "Nom_Custom")?, + // get_priority(&record, "Priority_Custom")?, + // get_number(&record, "Persons_Custom")?, + // get_number(&record, "Duration_Custom")?, + // get_string(&record, "Guide_Custom")?, + // )) + let name = get_string(&record, "Nom_Custom")?; + let priority = get_priority(&record, "Priorite_Custom")?; + let persons = get_number(&record, "Personnes_Custom")?; + let duration = get_number(&record, "Duree_Custom")?; + let guide = get_string(&record, "Guide_Custom")?; + Ok(CustomShiftValues::new( + name, priority, persons, duration, guide, + )) +} + pub async fn list_shifts() -> Res> { - let record_list = get_records(SHIFTS_DOC, SHIFTS_TABLE).await?; + let record_list = get_records(SHIFTS_DOC, SHIFTS_TABLE, Some("Heure_de_debut")).await?; debug!("{:?}", record_list); let mut shifts: Vec = Vec::new(); for record in record_list.records { let shift_type_ref = get_number(&record, "Type_de_shift").context("getting shift type ref")?; - let shift_type = get_shift_type(shift_type_ref).await?; + let shift_type = if shift_type_ref == 27 { + CustomOrDefined::Custom(get_shift_type_custom(&record)?) + } else { + CustomOrDefined::Defined(get_shift_type(shift_type_ref).await?) + }; let start_time = get_number(&record, "Heure_de_debut")?; let inscriptions_refs = get_number_array(record, "Inscriptions").context("getting inscriptions refs")?;