From d584df359640176ec4bc06f59d1e8d42ab17a413 Mon Sep 17 00:00:00 2001 From: Joris Date: Sat, 12 Mar 2022 13:27:29 +0100 Subject: Update and delete recurring events --- src/gui/form/mod.rs | 238 ++++++++++++++++++++++++++++++++++++++------- src/gui/form/repetition.rs | 47 +++++---- 2 files changed, 227 insertions(+), 58 deletions(-) (limited to 'src/gui/form') diff --git a/src/gui/form/mod.rs b/src/gui/form/mod.rs index 57ccac7..68e6539 100644 --- a/src/gui/form/mod.rs +++ b/src/gui/form/mod.rs @@ -2,8 +2,13 @@ mod repetition; use gtk4 as gtk; +use anyhow::Result; +use chrono::{NaiveDate, NaiveTime}; use gtk::glib; use gtk::prelude::*; +use rusqlite::Connection; +use thiserror::Error; +use uuid::Uuid; use crate::{ db, @@ -11,12 +16,70 @@ use crate::{ model::{event, event::Event}, }; -pub async fn show(app: &App, event: Event, is_new: bool) { +pub async fn repetition_dialog(app: &App, date: NaiveDate, event: Event) { let dialog = gtk::Dialog::builder() .transient_for(&*app.window) .modal(true) - .title(if is_new { "Ajouter" } else { "Modifier" }) - .css_classes(vec!["g-Form".to_string()]) + .title("Modifier") + .css_classes(vec!["g-Dialog".to_string()]) + .build(); + + let content_area = dialog.content_area(); + + let lines = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + content_area.append(&lines); + + let button = gtk::Button::builder() + .label("Cette occurence") + .margin_bottom(10) + .build(); + lines.append(&button); + let tx = app.tx.clone(); + button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| { + update::send(tx.clone(), Msg::ShowUpdateRepetitionForm { date, event: event.clone() }); + dialog.close() + })); + + let button = gtk::Button::builder() + .label("Toutes les occurences") + .build(); + lines.append(&button); + let tx = app.tx.clone(); + button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| { + update::send(tx.clone(), Msg::ShowUpdateForm { event: event.clone() }); + dialog.close() + })); + + dialog.run_future().await; +} + +#[derive(Clone)] +pub enum Target { + New { date: NaiveDate }, + Update { event: Event }, + UpdateRepetition { event: Event, date: NaiveDate }, +} + +pub async fn show(app: &App, target: Target) { + let event = match target { + Target::New { .. } => None, + Target::Update { ref event } => Some(event.clone()), + Target::UpdateRepetition { ref event, .. } => Some(event.clone()), + }; + + let title = if event.is_some() { + "Modifier" + } else { + "Ajouter" + }; + + let dialog = gtk::Dialog::builder() + .transient_for(&*app.window) + .modal(true) + .title(title) + .css_classes(vec!["g-Dialog".to_string()]) .build(); let content_area = dialog.content_area(); @@ -29,7 +92,6 @@ pub async fn show(app: &App, event: Event, is_new: bool) { let columns = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); - columns.add_css_class("g-Form__Columns"); lines.append(&columns); // First column @@ -37,79 +99,145 @@ pub async fn show(app: &App, event: Event, is_new: bool) { let column1 = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); - column1.add_css_class("g-Form__Inputs"); columns.append(&column1); - let name = entry(&event.name); + let name = event.as_ref().map(|e| entry(&e.name)).unwrap_or_default(); column1.append(&label("Événement")); column1.append(&name); - let date = entry(&event.date.format(event::DATE_FORMAT).to_string()); + let date = match target { + Target::New { date } => date, + Target::Update { ref event } => event.date, + Target::UpdateRepetition { date, .. } => date, + }; + let date = entry(&date.format(event::DATE_FORMAT).to_string()); column1.append(&label("Jour")); column1.append(&date); - let start = entry( - &event - .start - .map(event::pprint_time) - .unwrap_or_else(|| "".to_string()), - ); + let start = event + .as_ref() + .map(|e| time_entry(e.start)) + .unwrap_or_else(|| entry("")); column1.append(&label("Début")); column1.append(&start); - let end = entry( - &event - .end - .map(event::pprint_time) - .unwrap_or_else(|| "".to_string()), - ); + let end = event + .as_ref() + .map(|e| time_entry(e.end)) + .unwrap_or_else(|| entry("")); column1.append(&label("Fin")); column1.append(&end); // Second column - let repetition_model = repetition::view(&event); + let repetition = match target { + Target::Update { ref event } => event.repetition.as_ref(), + _ => None, + }; + let repetition_model = repetition::view(repetition); columns.append(&repetition_model.view); // Buttons + let button_title = match target { + Target::New { .. } => "Créer", + Target::Update { .. } => "Modifier", + Target::UpdateRepetition { .. } => "Modifier l’occurence", + }; + let button = gtk::Button::builder() - .label(if is_new { "Créer" } else { "Modifier" }) + .label(button_title) .margin_bottom(10) .build(); lines.append(&button); let conn = app.conn.clone(); let tx = app.tx.clone(); - button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| { + button.connect_clicked(glib::clone!(@weak dialog, @strong target, @strong event => move |_| { match repetition::validate(&repetition_model) { Ok(repetition) => { - match event::validate(event.id, date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text(), repetition) { + let id = match &target { + Target::Update {event} => event.id, + _ => Uuid::new_v4(), + }; + match event::validate(id, date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text(), repetition) { Some(new) => { - match if is_new { db::insert(&conn, &new) } else { db::update(&conn, &new) } { - Ok(_) => { - let msg = if is_new { Msg::AddEvent { new } } else { Msg::UpdateEvent { old: event.clone(), new } }; - update::send(tx.clone(), msg); - dialog.close() - }, - Err(err) => eprintln!("Error when upserting event: {err}") + match &target { + Target::New {..} => { + match db::insert(&conn, &new) { + Ok(_) => { + update::send(tx.clone(), Msg::AddEvent { new }); + dialog.close() + }, + Err(err) => eprintln!("Error when inserting event: {}", err) + } + } + Target::Update {event} => { + match db::update(&conn, &new) { + Ok(_) => { + update::send(tx.clone(), Msg::UpdateEvent { old: event.clone(), new }); + dialog.close() + }, + Err(err) => eprintln!("Error when updating event: {}", err) + } + } + Target::UpdateRepetition { event, date } => { + // TODO: improve intermediate error state + match delete_repetition_occurence(&conn, event, *date) { + Ok(occurence) => { + match db::insert(&conn, &new) { + Ok(_) => { + update::send(tx.clone(), Msg::UpdateEventOccurence { + event: event.clone(), + occurence, + date: *date, + new + }) + } + Err(err) => eprintln!("Error when updating repetition: {}", err) + }; + dialog.close() + }, + Err(err) => eprintln!("Error when updating repetition: {}", err) + } + } } } - None => eprintln!("Event is not valid: {event:?}") + None => eprintln!("Event is not valid.") } }, - Err(message) => eprintln!("{message}") + Err(message) => eprintln!("{}", message) } })); - if !is_new { - let button = gtk::Button::builder().label("Supprimer").build(); + if let Some(event) = event { + let label = match target { + Target::Update { .. } => "Supprimer", + _ => "Supprimer l’occurence", + }; + let button = gtk::Button::builder().label(label).build(); lines.append(&button); let conn = app.conn.clone(); let tx = app.tx.clone(); button.connect_clicked(glib::clone!(@weak dialog => move |_| { - if db::delete(&conn, &event.id).is_ok() { - update::send(tx.clone(), Msg::DeleteEvent { event: event.clone() }); - dialog.close() + match target { + Target::UpdateRepetition { date, .. } => { + match delete_repetition_occurence(&conn, &event, date) { + Ok(occurence) => { + update::send(tx.clone(), Msg::DeleteOccurence { event: event.clone(), date, occurence }); + dialog.close() + } + Err(err) => { + eprintln!("{:?}", err); + } + } + } + _ => { + let operation = db::delete(&conn, &event.id); + if operation.is_ok() { + update::send(tx.clone(), Msg::DeleteEvent { event: event.clone() }); + dialog.close() + } + } } })); } @@ -117,6 +245,42 @@ pub async fn show(app: &App, event: Event, is_new: bool) { dialog.run_future().await; } +#[derive(Error, Debug)] +enum DeleteError { + #[error("Repetition not found")] + RepetitionNotFound, + #[error("Occurence not found")] + OccurenceNotFound, +} + +fn delete_repetition_occurence( + conn: &Connection, + event: &Event, + occurence_date: NaiveDate, +) -> Result { + if let Some(ref repetition) = event.repetition { + if let Some(occurence) = repetition.occurence_index(event.date, occurence_date) { + let mut event = event.clone(); + let mut repetition = repetition.clone(); + repetition.removed_occurences.insert(occurence); + event.repetition = Some(repetition); + db::update(conn, &event).map(|_| occurence) + } else { + Err(anyhow::Error::new(DeleteError::OccurenceNotFound)) + } + } else { + Err(anyhow::Error::new(DeleteError::RepetitionNotFound)) + } +} + +fn time_entry(time: Option) -> gtk::Entry { + entry( + &time + .map(event::pprint_time) + .unwrap_or_else(|| "".to_string()), + ) +} + fn entry(text: &str) -> gtk::Entry { gtk::Entry::builder().text(text).margin_bottom(10).build() } diff --git a/src/gui/form/repetition.rs b/src/gui/form/repetition.rs index accb091..1d36765 100644 --- a/src/gui/form/repetition.rs +++ b/src/gui/form/repetition.rs @@ -2,13 +2,11 @@ use gtk4 as gtk; use chrono::{Weekday, Weekday::*}; use gtk::prelude::*; +use std::collections::HashSet; -use crate::{ - model::event::Event, - model::{ - repetition, - repetition::{DayOfMonth, Repetition}, - }, +use crate::model::{ + repetition, + repetition::{DayOfMonth, Frequency, Repetition}, }; static WEEKDAYS_STR: [&str; 7] = [ @@ -29,7 +27,7 @@ pub struct Model { yearly_radio: gtk::CheckButton, } -pub fn view(event: &Event) -> Model { +pub fn view(repetition: Option<&Repetition>) -> Model { let view = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); @@ -39,12 +37,14 @@ pub fn view(event: &Event) -> Model { let no_radio = gtk::CheckButton::builder() .label("Non") - .active(event.repetition.is_none()) + .active(repetition.is_none()) .build(); view.append(&no_radio); - let default = match event.repetition { - Some(Repetition::Daily { period }) => period.to_string(), + let frequency = repetition.as_ref().map(|r| r.frequency.clone()); + + let default = match frequency { + Some(Frequency::Daily { period }) => period.to_string(), _ => "".to_string(), }; let day_interval_entry = gtk::Entry::builder().text(&default).build(); @@ -56,8 +56,8 @@ pub fn view(event: &Event) -> Model { ); view.append(&day_interval_box); - let default = match event.repetition { - Some(Repetition::Monthly { + let default = match frequency { + Some(Frequency::Monthly { day: DayOfMonth::Day { day }, }) => day.to_string(), _ => "".to_string(), @@ -67,8 +67,8 @@ pub fn view(event: &Event) -> Model { radio_input(&no_radio, !default.is_empty(), &monthly_entry, "Mensuel"); view.append(&monthly_box); - let (active, default) = match event.repetition { - Some(Repetition::Monthly { + let (active, default) = match frequency { + Some(Frequency::Monthly { day: DayOfMonth::Weekday { weekday }, }) => (true, weekday), _ => (false, Mon), @@ -83,7 +83,7 @@ pub fn view(event: &Event) -> Model { let yearly_radio = gtk::CheckButton::builder() .group(&no_radio) .label("Annuel") - .active(event.repetition == Some(Repetition::Yearly)) + .active(frequency == Some(Frequency::Yearly)) .build(); view.append(&yearly_radio); @@ -127,24 +127,29 @@ fn label(text: &str) -> gtk::Label { } pub fn validate(model: &Model) -> Result, String> { - if model.no_radio.is_active() { + let frequency = if model.no_radio.is_active() { Ok(None) } else if model.day_interval_radio.is_active() { let period = repetition::validate_period(&model.day_interval_entry.buffer().text())?; - Ok(Some(Repetition::Daily { period })) + Ok(Some(Frequency::Daily { period })) } else if model.monthly_radio.is_active() { let day = repetition::validate_day(&model.monthly_entry.buffer().text())?; - Ok(Some(Repetition::Monthly { + Ok(Some(Frequency::Monthly { day: DayOfMonth::Day { day }, })) } else if model.first_day_radio.is_active() { let weekday = WEEKDAYS[model.first_day_dropdown.selected() as usize]; - Ok(Some(Repetition::Monthly { + Ok(Some(Frequency::Monthly { day: DayOfMonth::Weekday { weekday }, })) } else if model.yearly_radio.is_active() { - Ok(Some(Repetition::Yearly)) + Ok(Some(Frequency::Yearly)) } else { Err("Aucune option n’a été sélectionnée".to_string()) - } + }?; + + Ok(frequency.map(|frequency| Repetition { + frequency, + removed_occurences: HashSet::new(), + })) } -- cgit v1.2.3