allow adding custom shifts

This commit is contained in:
Pierre de Lacroix 2026-06-28 14:46:13 +02:00
parent 25af0af6f1
commit 42a4107162
Signed by: lateralus23
GPG key ID: 53E0CEC29C24EF39
2 changed files with 226 additions and 28 deletions

View file

@ -1,4 +1,5 @@
use crate::grist::{Priority, Shift}; use crate::grist::{Priority, Shift};
use chrono::{DateTime, FixedOffset, TimeZone};
use clap::{CommandFactory, Parser, Subcommand}; use clap::{CommandFactory, Parser, Subcommand};
use matrix_sdk::{Client, Room}; use matrix_sdk::{Client, Room};
@ -25,15 +26,38 @@ enum Action {
name: String, name: String,
/// Date de début, sous ce format: J#-HH-MM (ex: J1-14-30) /// Date de début, sous ce format: J#-HH-MM (ex: J1-14-30)
#[arg(id = "DATE", short = 'd', long = "date")] #[arg(id = "DATE", short = 't', long = "date", value_parser= parse_date)]
starting_time: String, start_time: DateTime<FixedOffset>,
/// Priorité /// Priorité
#[arg(id = "PRIORITÉ", short = 'p', long = "priorité", value_enum)] #[arg(id = "PRIORITÉ", short = 'p', long = "priorité", value_enum)]
priority: Priority, 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<DateTime<FixedOffset>> {
let day_index = &input[1..2].parse::<u32>()?;
let hour = &input[3..5].parse::<u32>()?;
let minute = &input[6..8].parse::<u32>()?;
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<OwnedEventId> { pub async fn send_room_msg(_client: Client, room: Room, content: String) -> Res<OwnedEventId> {
use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;
let content = RoomMessageEventContent::text_plain(content); let content = RoomMessageEventContent::text_plain(content);
@ -84,10 +108,19 @@ pub async fn handle(command: impl AsRef<str>, room: Room, client: Client) -> Res
} }
Action::Add { Action::Add {
name, name,
starting_time, start_time,
priority, priority,
persons,
duration,
guide,
} => { } => {
debug!("user asked for add"); 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?;
} }
} }
} }

View file

@ -1,11 +1,13 @@
prelude! {} prelude! {}
use chrono::DateTime; use chrono::{DateTime, FixedOffset};
use clap::ValueEnum; use clap::ValueEnum;
use clap::builder::PossibleValue; use clap::builder::PossibleValue;
use grist_client::apis::configuration::Configuration; use grist_client::apis::configuration::Configuration;
use grist_client::apis::records_api::list_records; use grist_client::apis::records_api::{add_records, list_records};
use grist_client::models::{RecordsList, RecordsListRecordsInner}; use grist_client::models::{
RecordsList, RecordsListRecordsInner, RecordsWithoutId, RecordsWithoutIdRecordsInner,
};
pub const API_URL: &str = "https://grist.interhacker.space/api"; pub const API_URL: &str = "https://grist.interhacker.space/api";
pub const SHIFTS_DOC: &str = "iwSaC82TsQiuD3MYSG9RXX"; pub const SHIFTS_DOC: &str = "iwSaC82TsQiuD3MYSG9RXX";
@ -21,20 +23,36 @@ pub enum Priority {
Urgent, 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 { impl Display for Priority {
fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes { fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes {
match self { match self {
Self::Prioritaire => write!(f, "<b>Prioritaire</b>"), Self::Prioritaire => write!(f, "<b>Prioritaire</b>"),
Self::Secondaire => write!(f, "Secondaire"), Self::Secondaire => write!(f, "Secondaire"),
Self::Bonus => write!(f, "Bonus"), Self::Bonus => write!(f, "Bonus"),
Self::Urgent => write!(f, "<font color=\"red\">Prioritaire</font>"), Self::Urgent => write!(f, "<font color=\"red\">Urgent</font>"),
} }
} }
} }
impl ValueEnum for Priority { impl ValueEnum for Priority {
fn value_variants<'a>() -> &'a [Self] { 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<PossibleValue> { fn to_possible_value(&self) -> Option<PossibleValue> {
@ -80,43 +98,168 @@ impl Display for ShiftType {
fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes { fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes {
write!( write!(
f, f,
"{} ({}): {} x/j, {} personnes, {} h", "<i>{}</i> ({}): {} x/j, {} personnes, {} h",
self.name, self.priority, self.frequency, self.persons, self.duration 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<String>,
priority: Priority,
persons: u64,
duration: u64,
guide: impl Into<String>,
) -> 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)} // {"Autres_infos": Null, "Heure_de_debut": Number(1782144000), "Inscriptions": Array [String("L"), Number(1)], "Type_de_shift": Number(23)}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Shift { pub struct Shift {
pub shift_type: ShiftType, pub shift_type: CustomOrDefined,
pub start_time: u64, pub start_time: DateTime<FixedOffset>,
pub inscriptions: Vec<String>, pub inscriptions: Vec<String>,
} }
impl Shift { impl Shift {
pub fn new(shift_type: ShiftType, start_time: u64, inscriptions: Vec<String>) -> Self { pub fn new(shift_type: CustomOrDefined, start_time: u64, inscriptions: Vec<String>) -> Self {
Self { Self {
shift_type: shift_type, 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, inscriptions: inscriptions,
} }
} }
pub fn create_custom(
name: String,
start_time: DateTime<FixedOffset>,
priority: Priority,
persons: u64,
duration: u64,
guide: impl Into<String>,
) -> 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 { impl Display for Shift {
fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes { fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes {
let start_time = DateTime::from_timestamp(self.start_time as i64, 0) let start_time = self.start_time.format("le %d à %H:%M");
.expect("failed to parse timestamp")
.format("%H:%M");
write!( write!(
f, f,
"{} ({}): à {}, durée {} h, inscrit·es: {}/{}", "<i>{}</i> ({}): {}, durée {} h, inscrit·es: {}/{}",
self.shift_type.name, self.name(),
self.shift_type.priority, self.priority(),
start_time, start_time,
self.shift_type.duration, self.duration(),
self.inscriptions.len(), self.inscriptions.len(),
self.shift_type.persons self.persons()
) )
} }
} }
@ -146,7 +289,7 @@ pub fn get_priority(record: &RecordsListRecordsInner, column: &str) -> Res<Prior
s if s.starts_with("2") => Ok(Priority::Secondaire), s if s.starts_with("2") => Ok(Priority::Secondaire),
s if s.starts_with("3") => Ok(Priority::Bonus), s if s.starts_with("3") => Ok(Priority::Bonus),
s if s.starts_with("0") => Ok(Priority::Urgent), 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<Ve
Ok(u64_array) Ok(u64_array)
} }
pub async fn get_records(doc: &str, table: &str) -> Res<RecordsList> { pub async fn get_records(doc: &str, table: &str, sort: Option<&str>) -> Res<RecordsList> {
let conf = conf::get(); let conf = conf::get();
let api_key = conf.grist_api_key(); let api_key = conf.grist_api_key();
let grist_conf = Configuration::new(API_URL.into(), Some(api_key.into())); 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 .await
.context("Failed to list records") .context("Failed to list records")
} }
pub async fn get_shift_type(index: u64) -> Res<ShiftType> { pub async fn get_shift_type(index: u64) -> Res<ShiftType> {
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 let record = record_list
.records .records
.iter() .iter()
@ -199,7 +342,7 @@ pub async fn get_shift_type(index: u64) -> Res<ShiftType> {
} }
pub async fn get_inscription(index: u64) -> Res<String> { pub async fn get_inscription(index: u64) -> Res<String> {
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 let record = record_list
.records .records
.iter() .iter()
@ -210,14 +353,36 @@ pub async fn get_inscription(index: u64) -> Res<String> {
Ok(name) Ok(name)
} }
pub fn get_shift_type_custom(record: &RecordsListRecordsInner) -> Res<CustomShiftValues> {
// 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<Vec<Shift>> { pub async fn list_shifts() -> Res<Vec<Shift>> {
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); debug!("{:?}", record_list);
let mut shifts: Vec<Shift> = Vec::new(); let mut shifts: Vec<Shift> = Vec::new();
for record in record_list.records { for record in record_list.records {
let shift_type_ref = let shift_type_ref =
get_number(&record, "Type_de_shift").context("getting 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 start_time = get_number(&record, "Heure_de_debut")?;
let inscriptions_refs = let inscriptions_refs =
get_number_array(record, "Inscriptions").context("getting inscriptions refs")?; get_number_array(record, "Inscriptions").context("getting inscriptions refs")?;