From 32d3793025b4a165989d92f5088984d0e3224802 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 23 May 2024 18:45:56 +0200 Subject: [PATCH 01/65] Update database structure --- .../down.sql | 2 ++ .../up.sql | 24 ++++++++++++++ geneit_backend/src/schema.rs | 32 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql create mode 100644 geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql diff --git a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql new file mode 100644 index 0000000..d6be2c6 --- /dev/null +++ b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS accomodations_reservations; +DROP TABLE IF EXISTS accomodations_list; \ No newline at end of file diff --git a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql new file mode 100644 index 0000000..e1456f7 --- /dev/null +++ b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql @@ -0,0 +1,24 @@ +-- Create tables +CREATE TABLE IF NOT EXISTS accommodations_list +( + id SERIAL PRIMARY KEY, + family_id integer NOT NULL REFERENCES families, + time_create BIGINT NOT NULL, + time_update BIGINT NOT NULL, + need_validation BOOLEAN, + description text NULL, + open_to_reservation BOOLEAN +); + +CREATE TABLE IF NOT EXISTS accommodations_reservations +( + id SERIAL PRIMARY KEY, + family_id integer NOT NULL REFERENCES families ON DELETE CASCADE, + accommodation_id integer NOT NULL REFERENCES accommodations_list ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + time_create BIGINT NOT NULL, + time_update BIGINT NOT NULL, + reservation_start BIGINT NOT NULL, + reservation_end BIGINT NOT NULL, + validated BOOLEAN +); \ No newline at end of file diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index f681f84..4ab6a0f 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -1,5 +1,31 @@ // @generated automatically by Diesel CLI. +diesel::table! { + accommodations_list (id) { + id -> Int4, + family_id -> Int4, + time_create -> Int8, + time_update -> Int8, + need_validation -> Nullable, + description -> Nullable, + open_to_reservation -> Nullable, + } +} + +diesel::table! { + accommodations_reservations (id) { + id -> Int4, + family_id -> Int4, + accommodation_id -> Int4, + user_id -> Int4, + time_create -> Int8, + time_update -> Int8, + reservation_start -> Int8, + reservation_end -> Int8, + validated -> Nullable, + } +} + diesel::table! { couples (id) { id -> Int4, @@ -119,6 +145,10 @@ diesel::table! { } } +diesel::joinable!(accommodations_list -> families (family_id)); +diesel::joinable!(accommodations_reservations -> accommodations_list (accommodation_id)); +diesel::joinable!(accommodations_reservations -> families (family_id)); +diesel::joinable!(accommodations_reservations -> users (user_id)); diesel::joinable!(couples -> families (family_id)); diesel::joinable!(couples -> photos (photo_id)); diesel::joinable!(members -> families (family_id)); @@ -127,6 +157,8 @@ diesel::joinable!(memberships -> families (family_id)); diesel::joinable!(memberships -> users (user_id)); diesel::allow_tables_to_appear_in_same_query!( + accommodations_list, + accommodations_reservations, couples, families, members, -- 2.45.2 From 2f1df6c117d3a08c457a1ecf14f7a3c58179e9cf Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 23 May 2024 19:28:29 +0200 Subject: [PATCH 02/65] Can toggle accommodations module --- geneit_app/src/api/FamilyApi.ts | 4 ++++ .../src/routes/family/FamilySettingsRoute.tsx | 17 ++++++++++++++--- .../down.sql | 3 +++ .../up.sql | 7 +++++++ .../src/controllers/families_controller.rs | 7 +++++++ geneit_backend/src/main.rs | 11 +++++++++++ geneit_backend/src/models.rs | 1 + geneit_backend/src/schema.rs | 1 + geneit_backend/src/services/families_service.rs | 1 + 9 files changed, 49 insertions(+), 3 deletions(-) diff --git a/geneit_app/src/api/FamilyApi.ts b/geneit_app/src/api/FamilyApi.ts index 31d97b6..590fdd1 100644 --- a/geneit_app/src/api/FamilyApi.ts +++ b/geneit_app/src/api/FamilyApi.ts @@ -88,10 +88,12 @@ export class Family implements FamilyAPI { export class ExtendedFamilyInfo extends Family { public disable_couple_photos: boolean; public enable_genealogy: boolean; + public enable_accommodations: boolean; constructor(p: any) { super(p); this.disable_couple_photos = p.disable_couple_photos; this.enable_genealogy = p.enable_genealogy; + this.enable_accommodations = p.enable_accommodations; } } @@ -235,6 +237,7 @@ export class FamilyApi { id: number; name?: string; enable_genealogy?: boolean; + enable_accommodations?: boolean; disable_couple_photos?: boolean; }): Promise { await APIClient.exec({ @@ -243,6 +246,7 @@ export class FamilyApi { jsonData: { name: settings.name, enable_genealogy: settings.enable_genealogy, + enable_accommodations: settings.enable_accommodations, disable_couple_photos: settings.disable_couple_photos, }, }); diff --git a/geneit_app/src/routes/family/FamilySettingsRoute.tsx b/geneit_app/src/routes/family/FamilySettingsRoute.tsx index 7c0d720..c14ddf1 100644 --- a/geneit_app/src/routes/family/FamilySettingsRoute.tsx +++ b/geneit_app/src/routes/family/FamilySettingsRoute.tsx @@ -71,6 +71,9 @@ function FamilySettingsCard(): React.ReactElement { const [enableGenealogy, setEnableGenealogy] = React.useState( family.family.enable_genealogy ); + const [enableAccommodations, setEnableAccommodations] = React.useState( + family.family.enable_accommodations + ); const canEdit = family.family.is_admin; @@ -86,6 +89,7 @@ function FamilySettingsCard(): React.ReactElement { id: family.family.family_id, name: newName, enable_genealogy: enableGenealogy, + enable_accommodations: enableAccommodations, }); family.reloadFamilyInfo(); @@ -118,14 +122,12 @@ function FamilySettingsCard(): React.ReactElement { label="Identifiant" value={family.family.family_id} /> - - - + setEnableAccommodations(c)} + /> + } + label="Activer le module de réservation de logements" + /> diff --git a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql index d6be2c6..ab5440a 100644 --- a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql +++ b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql @@ -1,2 +1,5 @@ +ALTER TABLE public.families + DROP COLUMN enable_accommodations; + DROP TABLE IF EXISTS accomodations_reservations; DROP TABLE IF EXISTS accomodations_list; \ No newline at end of file diff --git a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql index e1456f7..0262ead 100644 --- a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql +++ b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql @@ -1,3 +1,10 @@ +-- Add column to toggle accommodations module +ALTER TABLE public.families + ADD enable_accommodations boolean NOT NULL DEFAULT false; +COMMENT + ON COLUMN public.families.enable_accommodations IS 'Specify whether accommodations feature is enabled for the family'; + + -- Create tables CREATE TABLE IF NOT EXISTS accommodations_list ( diff --git a/geneit_backend/src/controllers/families_controller.rs b/geneit_backend/src/controllers/families_controller.rs index b5b4c3e..8183db1 100644 --- a/geneit_backend/src/controllers/families_controller.rs +++ b/geneit_backend/src/controllers/families_controller.rs @@ -80,6 +80,7 @@ struct RichFamilyInfo { #[serde(flatten)] membership: FamilyMembership, enable_genealogy: bool, + enable_accommodations: bool, disable_couple_photos: bool, } @@ -90,6 +91,7 @@ pub async fn single_info(f: FamilyInPath) -> HttpResult { Ok(HttpResponse::Ok().json(RichFamilyInfo { membership, enable_genealogy: family.enable_genealogy, + enable_accommodations: family.enable_accommodations, disable_couple_photos: family.disable_couple_photos, })) } @@ -105,6 +107,7 @@ pub async fn leave(f: FamilyInPath) -> HttpResult { pub struct UpdateFamilyBody { name: Option, enable_genealogy: Option, + enable_accommodations: Option, disable_couple_photos: Option, } @@ -127,6 +130,10 @@ pub async fn update( family.enable_genealogy = enable_genealogy; } + if let Some(enable_accommodations) = req.enable_accommodations { + family.enable_accommodations = enable_accommodations; + } + if let Some(disable_couple_photos) = req.disable_couple_photos { family.disable_couple_photos = disable_couple_photos; } diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 683dfcb..9fa9c26 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -204,6 +204,17 @@ async fn main() -> std::io::Result<()> { "/family/{id}/genealogy/data/import", web::put().to(data_controller::import_family), ) + // [ACCOMODATIONS] List controller + // TODO : create + // TODO : update + // TODO : delete + // TODO : list + // [ACCOMODATIONS] Reservations controller + // TODO : create + // TODO : update + // TODO : delete + // TODO : list + // TODO : validate or reject // Photos controller .route( "/photo/{id}", diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 596d092..d1dd017 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -66,6 +66,7 @@ pub struct Family { pub invitation_code: String, pub disable_couple_photos: bool, pub enable_genealogy: bool, + pub enable_accommodations: bool, } impl Family { diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index 4ab6a0f..7c4d774 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -56,6 +56,7 @@ diesel::table! { invitation_code -> Varchar, disable_couple_photos -> Bool, enable_genealogy -> Bool, + enable_accommodations -> Bool, } } diff --git a/geneit_backend/src/services/families_service.rs b/geneit_backend/src/services/families_service.rs index af6dd93..a1fc6e7 100644 --- a/geneit_backend/src/services/families_service.rs +++ b/geneit_backend/src/services/families_service.rs @@ -175,6 +175,7 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> { families::dsl::name.eq(family.name.clone()), families::dsl::invitation_code.eq(family.invitation_code.clone()), families::dsl::enable_genealogy.eq(family.enable_genealogy), + families::dsl::enable_accommodations.eq(family.enable_accommodations), families::dsl::disable_couple_photos.eq(family.disable_couple_photos), )) .execute(conn) -- 2.45.2 From c4fadce69f7813e1ba378c06b0eabfc49f2a0e59 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 23 May 2024 21:20:14 +0200 Subject: [PATCH 03/65] Can create new accommodations using the API --- geneit_app/src/api/ServerApi.ts | 2 + .../down.sql | 4 +- .../up.sql | 17 +-- geneit_backend/src/constants.rs | 6 ++ .../accommodations_list_controller.rs | 75 +++++++++++++ geneit_backend/src/controllers/mod.rs | 1 + geneit_backend/src/main.rs | 10 +- geneit_backend/src/models.rs | 35 +++++- geneit_backend/src/schema.rs | 6 +- .../services/accommodations_list_service.rs | 102 ++++++++++++++++++ .../src/services/families_service.rs | 7 +- geneit_backend/src/services/mod.rs | 1 + 12 files changed, 248 insertions(+), 18 deletions(-) create mode 100644 geneit_backend/src/controllers/accommodations_list_controller.rs create mode 100644 geneit_backend/src/services/accommodations_list_service.rs diff --git a/geneit_app/src/api/ServerApi.ts b/geneit_app/src/api/ServerApi.ts index 03a464c..2238e8d 100644 --- a/geneit_app/src/api/ServerApi.ts +++ b/geneit_app/src/api/ServerApi.ts @@ -32,6 +32,8 @@ interface Constraints { member_country: LenConstraint; member_sex: LenConstraint; member_note: LenConstraint; + accomodation_name_len: LenConstraint; + accomodation_description_len: LenConstraint; } interface OIDCProvider { diff --git a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql index ab5440a..3512fca 100644 --- a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql +++ b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql @@ -1,5 +1,5 @@ ALTER TABLE public.families DROP COLUMN enable_accommodations; -DROP TABLE IF EXISTS accomodations_reservations; -DROP TABLE IF EXISTS accomodations_list; \ No newline at end of file +DROP TABLE IF EXISTS accommodations_reservations; +DROP TABLE IF EXISTS accommodations_list; \ No newline at end of file diff --git a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql index 0262ead..6ce2823 100644 --- a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql +++ b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql @@ -8,13 +8,14 @@ COMMENT -- Create tables CREATE TABLE IF NOT EXISTS accommodations_list ( - id SERIAL PRIMARY KEY, - family_id integer NOT NULL REFERENCES families, - time_create BIGINT NOT NULL, - time_update BIGINT NOT NULL, - need_validation BOOLEAN, - description text NULL, - open_to_reservation BOOLEAN + id SERIAL PRIMARY KEY, + family_id integer NOT NULL REFERENCES families, + time_create BIGINT NOT NULL, + time_update BIGINT NOT NULL, + name VARCHAR(50) NOT NULL, + need_validation BOOLEAN NOT NULL DEFAULT true, + description text NULL, + open_to_reservations BOOLEAN NOT NULL DEFAULT false ); CREATE TABLE IF NOT EXISTS accommodations_reservations @@ -27,5 +28,5 @@ CREATE TABLE IF NOT EXISTS accommodations_reservations time_update BIGINT NOT NULL, reservation_start BIGINT NOT NULL, reservation_end BIGINT NOT NULL, - validated BOOLEAN + validated BOOLEAN NULL ); \ No newline at end of file diff --git a/geneit_backend/src/constants.rs b/geneit_backend/src/constants.rs index f1edc26..c8aca30 100644 --- a/geneit_backend/src/constants.rs +++ b/geneit_backend/src/constants.rs @@ -60,6 +60,9 @@ pub struct StaticConstraints { pub member_country: SizeConstraint, pub member_sex: SizeConstraint, pub member_note: SizeConstraint, + + pub accomodation_name_len: SizeConstraint, + pub accomodation_description_len: SizeConstraint, } impl Default for StaticConstraints { @@ -91,6 +94,9 @@ impl Default for StaticConstraints { member_country: SizeConstraint::new(0, 2), member_sex: SizeConstraint::new(0, 1), member_note: SizeConstraint::new(0, 35000), + + accomodation_name_len: SizeConstraint::new(1, 50), + accomodation_description_len: SizeConstraint::new(0, 500), } } } diff --git a/geneit_backend/src/controllers/accommodations_list_controller.rs b/geneit_backend/src/controllers/accommodations_list_controller.rs new file mode 100644 index 0000000..06cabc7 --- /dev/null +++ b/geneit_backend/src/controllers/accommodations_list_controller.rs @@ -0,0 +1,75 @@ +use crate::constants::StaticConstraints; +use crate::controllers::HttpResult; +use crate::extractors::family_extractor::FamilyInPathWithAdminMembership; +use crate::models::{Accommodation, FamilyID}; +use crate::services::accommodations_list_service; +use crate::services::couples_service::{delete, get_all_of_family}; +use actix_web::{web, HttpResponse}; + +#[derive(thiserror::Error, Debug)] +enum AccommodationListControllerErr { + #[error("Malformed name!")] + MalformedName, + #[error("Malformed description!")] + MalformedDescription, +} + +#[derive(serde::Deserialize, Clone)] +pub struct AccommodationRequest { + pub name: String, + pub need_validation: bool, + pub description: Option, + pub open_to_reservations: bool, +} + +impl AccommodationRequest { + pub async fn to_accommodation(self, accommodation: &mut Accommodation) -> anyhow::Result<()> { + let c = StaticConstraints::default(); + + if !c.accomodation_name_len.validate(&self.name) { + return Err(AccommodationListControllerErr::MalformedName.into()); + } + accommodation.name = self.name; + + if let Some(d) = &self.description { + if !c.accomodation_description_len.validate(d) { + return Err(AccommodationListControllerErr::MalformedDescription.into()); + } + } + accommodation.description.clone_from(&self.description); + + accommodation.need_validation = self.need_validation; + accommodation.open_to_reservations = self.open_to_reservations; + Ok(()) + } +} + +/// Create a new accommodation +pub async fn create( + m: FamilyInPathWithAdminMembership, + req: web::Json, +) -> HttpResult { + let mut accommodation = accommodations_list_service::create(m.family_id()).await?; + + if let Err(e) = req.0.to_accommodation(&mut accommodation).await { + log::error!("Failed to apply accommodation information! {e}"); + accommodations_list_service::delete(&mut accommodation).await?; + return Ok(HttpResponse::BadRequest().body(e.to_string())); + } + + if let Err(e) = accommodations_list_service::update(&mut accommodation).await { + log::error!("Failed to update accommodation information! {e}"); + accommodations_list_service::delete(&mut accommodation).await?; + return Ok(HttpResponse::InternalServerError().finish()); + } + + Ok(HttpResponse::Ok().json(accommodation)) +} + +/// Delete all the accommodations of a family +pub async fn delete_all_family(family_id: FamilyID) -> anyhow::Result<()> { + for mut m in get_all_of_family(family_id).await? { + delete(&mut m).await?; + } + Ok(()) +} diff --git a/geneit_backend/src/controllers/mod.rs b/geneit_backend/src/controllers/mod.rs index 0d22c59..4cd4c7d 100644 --- a/geneit_backend/src/controllers/mod.rs +++ b/geneit_backend/src/controllers/mod.rs @@ -5,6 +5,7 @@ use actix_web::HttpResponse; use std::fmt::{Debug, Display, Formatter}; use zip::result::ZipError; +pub mod accommodations_list_controller; pub mod auth_controller; pub mod couples_controller; pub mod data_controller; diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 9fa9c26..e44f561 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -6,8 +6,9 @@ use actix_web::{web, App, HttpServer}; use geneit_backend::app_config::AppConfig; use geneit_backend::connections::{db_connection, s3_connection}; use geneit_backend::controllers::{ - auth_controller, couples_controller, data_controller, families_controller, members_controller, - photos_controller, server_controller, users_controller, + accommodations_list_controller, auth_controller, couples_controller, data_controller, + families_controller, members_controller, photos_controller, server_controller, + users_controller, }; #[actix_web::main] @@ -205,7 +206,10 @@ async fn main() -> std::io::Result<()> { web::put().to(data_controller::import_family), ) // [ACCOMODATIONS] List controller - // TODO : create + .route( + "/family/{id}/accommodations/list/create", + web::post().to(accommodations_list_controller::create), + ) // TODO : update // TODO : delete // TODO : list diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index d1dd017..7055c4f 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -1,5 +1,5 @@ use crate::app_config::AppConfig; -use crate::schema::{couples, families, members, memberships, photos, users}; +use crate::schema::{accommodations_list, couples, families, members, memberships, photos, users}; use crate::utils::crypt_utils::sha256; use diesel::prelude::*; @@ -309,7 +309,7 @@ pub struct NewMember { pub time_update: i64, } -/// Member ID holder +/// Couple ID holder #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct CoupleID(pub i32); @@ -442,3 +442,34 @@ pub struct NewCouple { pub time_create: i64, pub time_update: i64, } + +/// Accommodation ID holder +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] +pub struct AccommodationID(pub i32); + +#[derive(Queryable, Debug, serde::Serialize)] +pub struct Accommodation { + id: i32, + family_id: i32, + time_create: i64, + pub time_update: i64, + pub name: String, + pub need_validation: bool, + pub description: Option, + pub open_to_reservations: bool, +} + +impl Accommodation { + pub fn id(&self) -> AccommodationID { + AccommodationID(self.id) + } +} + +#[derive(Insertable)] +#[diesel(table_name = accommodations_list)] +pub struct NewAccommodation { + pub family_id: i32, + pub name: String, + pub time_create: i64, + pub time_update: i64, +} diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index 7c4d774..14ad1a2 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -6,9 +6,11 @@ diesel::table! { family_id -> Int4, time_create -> Int8, time_update -> Int8, - need_validation -> Nullable, + #[max_length = 50] + name -> Varchar, + need_validation -> Bool, description -> Nullable, - open_to_reservation -> Nullable, + open_to_reservations -> Bool, } } diff --git a/geneit_backend/src/services/accommodations_list_service.rs b/geneit_backend/src/services/accommodations_list_service.rs new file mode 100644 index 0000000..3ebbebc --- /dev/null +++ b/geneit_backend/src/services/accommodations_list_service.rs @@ -0,0 +1,102 @@ +use crate::connections::db_connection; +use crate::models::{Accommodation, AccommodationID, FamilyID, NewAccommodation}; +use crate::schema::accommodations_list; +use crate::utils::time_utils::time; +use diesel::prelude::*; + +/// Create a new accommodation +pub async fn create(family_id: FamilyID) -> anyhow::Result { + db_connection::execute(|conn| { + let res: Accommodation = diesel::insert_into(accommodations_list::table) + .values(&NewAccommodation { + family_id: family_id.0, + name: "".to_string(), + time_create: time() as i64, + time_update: time() as i64, + }) + .get_result(conn)?; + + Ok(res) + }) +} + +/// Get the information of an accommodation +pub async fn get_by_id(id: AccommodationID) -> anyhow::Result { + db_connection::execute(|conn| { + accommodations_list::table + .filter(accommodations_list::dsl::id.eq(id.0)) + .first(conn) + }) +} + +/// Get all the couples of an accommodation +pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result> { + db_connection::execute(|conn| { + accommodations_list::table + .filter(accommodations_list::dsl::family_id.eq(id.0)) + .get_results(conn) + }) +} + +/// Check whether accommodation with a given id exists or not +pub async fn exists( + family_id: FamilyID, + accommodation_id: AccommodationID, +) -> anyhow::Result { + db_connection::execute(|conn| { + let count: i64 = accommodations_list::table + .filter( + accommodations_list::id + .eq(accommodation_id.0) + .and(accommodations_list::family_id.eq(family_id.0)), + ) + .count() + .get_result(conn)?; + + Ok(count != 0) + }) +} + +/// Update the information of a couple +pub async fn update(accommodation: &mut Accommodation) -> anyhow::Result<()> { + accommodation.time_update = time() as i64; + + db_connection::execute(|conn| { + diesel::update( + accommodations_list::dsl::accommodations_list + .filter(accommodations_list::dsl::id.eq(accommodation.id().0)), + ) + .set(( + accommodations_list::dsl::time_update.eq(accommodation.time_update), + accommodations_list::dsl::name.eq(accommodation.name.to_string()), + accommodations_list::dsl::need_validation.eq(accommodation.need_validation), + accommodations_list::dsl::description.eq(accommodation.description.clone()), + accommodations_list::dsl::open_to_reservations.eq(accommodation.open_to_reservations), + )) + .execute(conn) + })?; + + Ok(()) +} + +/// Delete an accommodation +pub async fn delete(accommodation: &mut Accommodation) -> anyhow::Result<()> { + // Remove the accommodation + db_connection::execute(|conn| { + diesel::delete( + accommodations_list::dsl::accommodations_list + .filter(accommodations_list::dsl::id.eq(accommodation.id().0)), + ) + .execute(conn) + })?; + + Ok(()) +} + +/// Delete all the accommodations of a family +pub async fn delete_all_family(family_id: FamilyID) -> anyhow::Result<()> { + for mut m in get_all_of_family(family_id).await? { + delete(&mut m).await?; + } + Ok(()) +} diff --git a/geneit_backend/src/services/families_service.rs b/geneit_backend/src/services/families_service.rs index a1fc6e7..6bfdccb 100644 --- a/geneit_backend/src/services/families_service.rs +++ b/geneit_backend/src/services/families_service.rs @@ -5,7 +5,9 @@ use crate::models::{ Family, FamilyID, FamilyMembership, Membership, NewFamily, NewMembership, UserID, }; use crate::schema::{families, memberships}; -use crate::services::{couples_service, members_service, users_service}; +use crate::services::{ + accommodations_list_service, couples_service, members_service, users_service, +}; use crate::utils::string_utils::rand_str; use crate::utils::time_utils::time; use diesel::prelude::*; @@ -186,6 +188,9 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> { /// Delete a family pub async fn delete_family(family_id: FamilyID) -> anyhow::Result<()> { + // Delete all family accommodations + accommodations_list_service::delete_all_family(family_id).await?; + // Delete all family couples couples_service::delete_all_family(family_id).await?; diff --git a/geneit_backend/src/services/mod.rs b/geneit_backend/src/services/mod.rs index 935229e..b5db75a 100644 --- a/geneit_backend/src/services/mod.rs +++ b/geneit_backend/src/services/mod.rs @@ -1,5 +1,6 @@ //! # Backend services +pub mod accommodations_list_service; pub mod couples_service; pub mod families_service; pub mod login_token_service; -- 2.45.2 From 18582fdff7912bb18be56b3c5c41a25d6f5ec117 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 23 May 2024 21:30:51 +0200 Subject: [PATCH 04/65] Can delete an accommodation --- .../accommodations_list_controller.rs | 14 ++-- .../src/extractors/accommodation_extractor.rs | 83 +++++++++++++++++++ geneit_backend/src/extractors/mod.rs | 1 + geneit_backend/src/main.rs | 6 +- geneit_backend/src/models.rs | 4 + 5 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 geneit_backend/src/extractors/accommodation_extractor.rs diff --git a/geneit_backend/src/controllers/accommodations_list_controller.rs b/geneit_backend/src/controllers/accommodations_list_controller.rs index 06cabc7..44ffe8b 100644 --- a/geneit_backend/src/controllers/accommodations_list_controller.rs +++ b/geneit_backend/src/controllers/accommodations_list_controller.rs @@ -1,9 +1,9 @@ use crate::constants::StaticConstraints; use crate::controllers::HttpResult; +use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath; use crate::extractors::family_extractor::FamilyInPathWithAdminMembership; -use crate::models::{Accommodation, FamilyID}; +use crate::models::Accommodation; use crate::services::accommodations_list_service; -use crate::services::couples_service::{delete, get_all_of_family}; use actix_web::{web, HttpResponse}; #[derive(thiserror::Error, Debug)] @@ -66,10 +66,8 @@ pub async fn create( Ok(HttpResponse::Ok().json(accommodation)) } -/// Delete all the accommodations of a family -pub async fn delete_all_family(family_id: FamilyID) -> anyhow::Result<()> { - for mut m in get_all_of_family(family_id).await? { - delete(&mut m).await?; - } - Ok(()) +/// Delete an accommodation +pub async fn delete(m: FamilyAndAccommodationInPath) -> HttpResult { + accommodations_list_service::delete(&mut m.to_accommodation()).await?; + Ok(HttpResponse::Ok().finish()) } diff --git a/geneit_backend/src/extractors/accommodation_extractor.rs b/geneit_backend/src/extractors/accommodation_extractor.rs new file mode 100644 index 0000000..76bfca7 --- /dev/null +++ b/geneit_backend/src/extractors/accommodation_extractor.rs @@ -0,0 +1,83 @@ +use crate::extractors::family_extractor::FamilyInPath; +use crate::models::{Accommodation, AccommodationID, FamilyID, Membership}; +use crate::services::accommodations_list_service; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest}; +use serde::Deserialize; +use std::ops::Deref; + +#[derive(thiserror::Error, Debug)] +enum AccommodationExtractorErr { + #[error("Accommodation {0:?} does not belong to family {1:?}!")] + AccommodationNotInFamily(AccommodationID, FamilyID), +} + +#[derive(Debug)] +pub struct FamilyAndAccommodationInPath(Membership, Accommodation); + +impl FamilyAndAccommodationInPath { + async fn load_accommodation_from_path( + family: FamilyInPath, + accommodation_id: AccommodationID, + ) -> anyhow::Result { + let accommodation = accommodations_list_service::get_by_id(accommodation_id).await?; + if accommodation.family_id() != family.family_id() { + return Err(AccommodationExtractorErr::AccommodationNotInFamily( + accommodation.id(), + family.family_id(), + ) + .into()); + } + + Ok(Self(family.into(), accommodation)) + } +} + +impl Deref for FamilyAndAccommodationInPath { + type Target = Accommodation; + + fn deref(&self) -> &Self::Target { + &self.1 + } +} + +impl FamilyAndAccommodationInPath { + pub fn membership(&self) -> &Membership { + &self.0 + } + + pub fn to_accommodation(self) -> Accommodation { + self.1 + } +} + +#[derive(Deserialize)] +struct AccommodationIDInPath { + accommodation_id: AccommodationID, +} + +impl FromRequest for FamilyAndAccommodationInPath { + type Error = actix_web::Error; + type Future = futures_util::future::LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let req = req.clone(); + Box::pin(async move { + let family = FamilyInPath::extract(&req).await?; + + let accommodation_id = actix_web::web::Path::::from_request( + &req, + &mut Payload::None, + ) + .await? + .accommodation_id; + + FamilyAndAccommodationInPath::load_accommodation_from_path(family, accommodation_id) + .await + .map_err(|e| { + log::error!("Failed to extract accommodation ID from URL! {}", e); + actix_web::error::ErrorNotFound("Could not fetch accommodation information!") + }) + }) + } +} diff --git a/geneit_backend/src/extractors/mod.rs b/geneit_backend/src/extractors/mod.rs index 0cafc82..8859615 100644 --- a/geneit_backend/src/extractors/mod.rs +++ b/geneit_backend/src/extractors/mod.rs @@ -1,3 +1,4 @@ +pub mod accommodation_extractor; pub mod couple_extractor; pub mod family_extractor; pub mod member_extractor; diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index e44f561..002334d 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -211,8 +211,12 @@ async fn main() -> std::io::Result<()> { web::post().to(accommodations_list_controller::create), ) // TODO : update - // TODO : delete + .route( + "/family/{id}/accommodations/list/{accommodation_id}", + web::delete().to(accommodations_list_controller::delete), + ) // TODO : list + // TODO : get single // [ACCOMODATIONS] Reservations controller // TODO : create // TODO : update diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 7055c4f..498a10d 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -463,6 +463,10 @@ impl Accommodation { pub fn id(&self) -> AccommodationID { AccommodationID(self.id) } + + pub fn family_id(&self) -> FamilyID { + FamilyID(self.family_id) + } } #[derive(Insertable)] -- 2.45.2 From bc800e7cf6b138dd6a12c02a5cf405f4cdd7e15c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 23 May 2024 21:34:28 +0200 Subject: [PATCH 05/65] Only an admin can delete an accommodation --- .../src/controllers/accommodations_list_controller.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/geneit_backend/src/controllers/accommodations_list_controller.rs b/geneit_backend/src/controllers/accommodations_list_controller.rs index 44ffe8b..d38ec79 100644 --- a/geneit_backend/src/controllers/accommodations_list_controller.rs +++ b/geneit_backend/src/controllers/accommodations_list_controller.rs @@ -67,7 +67,10 @@ pub async fn create( } /// Delete an accommodation -pub async fn delete(m: FamilyAndAccommodationInPath) -> HttpResult { +pub async fn delete( + m: FamilyAndAccommodationInPath, + _admin: FamilyInPathWithAdminMembership, +) -> HttpResult { accommodations_list_service::delete(&mut m.to_accommodation()).await?; Ok(HttpResponse::Ok().finish()) } -- 2.45.2 From 49f36770815c78c9e55e942c83d4a175ac09da82 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 25 May 2024 07:50:11 +0200 Subject: [PATCH 06/65] Get full list of accommodations --- .../src/controllers/accommodations_list_controller.rs | 8 +++++++- geneit_backend/src/main.rs | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/geneit_backend/src/controllers/accommodations_list_controller.rs b/geneit_backend/src/controllers/accommodations_list_controller.rs index d38ec79..505a7c5 100644 --- a/geneit_backend/src/controllers/accommodations_list_controller.rs +++ b/geneit_backend/src/controllers/accommodations_list_controller.rs @@ -1,7 +1,7 @@ use crate::constants::StaticConstraints; use crate::controllers::HttpResult; use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath; -use crate::extractors::family_extractor::FamilyInPathWithAdminMembership; +use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership}; use crate::models::Accommodation; use crate::services::accommodations_list_service; use actix_web::{web, HttpResponse}; @@ -66,6 +66,12 @@ pub async fn create( Ok(HttpResponse::Ok().json(accommodation)) } +/// Get the full list of accommodations +pub async fn get_full_list(m: FamilyInPath) -> HttpResult { + Ok(HttpResponse::Ok() + .json(accommodations_list_service::get_all_of_family(m.family_id()).await?)) +} + /// Delete an accommodation pub async fn delete( m: FamilyAndAccommodationInPath, diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 002334d..92c749a 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -210,13 +210,16 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/list/create", web::post().to(accommodations_list_controller::create), ) - // TODO : update + .route( + "/family/{id}/accommodations/list/list", + web::get().to(accommodations_list_controller::get_full_list), + ) .route( "/family/{id}/accommodations/list/{accommodation_id}", web::delete().to(accommodations_list_controller::delete), ) - // TODO : list // TODO : get single + // TODO : update // [ACCOMODATIONS] Reservations controller // TODO : create // TODO : update -- 2.45.2 From 9a4da0462a73f2155acc38f18ba86af4ad4c5bae Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 25 May 2024 07:54:34 +0200 Subject: [PATCH 07/65] Get single accommodation information --- .../src/controllers/accommodations_list_controller.rs | 5 +++++ geneit_backend/src/main.rs | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/geneit_backend/src/controllers/accommodations_list_controller.rs b/geneit_backend/src/controllers/accommodations_list_controller.rs index 505a7c5..45613bc 100644 --- a/geneit_backend/src/controllers/accommodations_list_controller.rs +++ b/geneit_backend/src/controllers/accommodations_list_controller.rs @@ -72,6 +72,11 @@ pub async fn get_full_list(m: FamilyInPath) -> HttpResult { .json(accommodations_list_service::get_all_of_family(m.family_id()).await?)) } +/// Get the information of a single accommodation +pub async fn get_single(m: FamilyAndAccommodationInPath) -> HttpResult { + Ok(HttpResponse::Ok().json(&m.to_accommodation())) +} + /// Delete an accommodation pub async fn delete( m: FamilyAndAccommodationInPath, diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 92c749a..d861667 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -214,12 +214,15 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/list/list", web::get().to(accommodations_list_controller::get_full_list), ) + .route( + "/family/{id}/accommodations/list/{accommodation_id}", + web::get().to(accommodations_list_controller::get_single), + ) + // TODO : update .route( "/family/{id}/accommodations/list/{accommodation_id}", web::delete().to(accommodations_list_controller::delete), ) - // TODO : get single - // TODO : update // [ACCOMODATIONS] Reservations controller // TODO : create // TODO : update -- 2.45.2 From d0d1169c7d7855e8c7094c18aec516479abe7f2d Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 25 May 2024 07:57:50 +0200 Subject: [PATCH 08/65] Can update accommodation information --- .../accommodations_list_controller.rs | 18 ++++++++++++++++++ geneit_backend/src/main.rs | 5 ++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/geneit_backend/src/controllers/accommodations_list_controller.rs b/geneit_backend/src/controllers/accommodations_list_controller.rs index 45613bc..665bcb2 100644 --- a/geneit_backend/src/controllers/accommodations_list_controller.rs +++ b/geneit_backend/src/controllers/accommodations_list_controller.rs @@ -77,6 +77,24 @@ pub async fn get_single(m: FamilyAndAccommodationInPath) -> HttpResult { Ok(HttpResponse::Ok().json(&m.to_accommodation())) } +/// Update an accommodation +pub async fn update( + m: FamilyAndAccommodationInPath, + req: web::Json, + _admin: FamilyInPathWithAdminMembership, +) -> HttpResult { + let mut accommodation = m.to_accommodation(); + + if let Err(e) = req.0.to_accommodation(&mut accommodation).await { + log::error!("Failed to parse accommodation information! {e}"); + return Ok(HttpResponse::BadRequest().body(e.to_string())); + } + + accommodations_list_service::update(&mut accommodation).await?; + + Ok(HttpResponse::Accepted().finish()) +} + /// Delete an accommodation pub async fn delete( m: FamilyAndAccommodationInPath, diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index d861667..4dbaa40 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -218,7 +218,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/list/{accommodation_id}", web::get().to(accommodations_list_controller::get_single), ) - // TODO : update + .route( + "/family/{id}/accommodations/list/{accommodation_id}", + web::put().to(accommodations_list_controller::update), + ) .route( "/family/{id}/accommodations/list/{accommodation_id}", web::delete().to(accommodations_list_controller::delete), -- 2.45.2 From 936b095d46c2b0b5b0ab8f262dd27b6dc04a240e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 25 May 2024 08:48:13 +0200 Subject: [PATCH 09/65] Can get the full list of accommodation reservations for a family --- .../accommodations_reservations_controller.rs | 10 ++++++ geneit_backend/src/controllers/mod.rs | 1 + geneit_backend/src/main.rs | 13 ++++--- geneit_backend/src/models.rs | 34 ++++++++++++++++++- .../services/accommodations_list_service.rs | 4 +-- .../accommodations_reservations_service.rs | 13 +++++++ geneit_backend/src/services/mod.rs | 1 + 7 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 geneit_backend/src/controllers/accommodations_reservations_controller.rs create mode 100644 geneit_backend/src/services/accommodations_reservations_service.rs diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs new file mode 100644 index 0000000..b3ec476 --- /dev/null +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -0,0 +1,10 @@ +use crate::controllers::HttpResult; +use crate::extractors::family_extractor::FamilyInPath; +use crate::services::accommodations_reservations_service; +use actix_web::HttpResponse; + +/// Get the full list of accommodations reservations for a family +pub async fn full_list(m: FamilyInPath) -> HttpResult { + Ok(HttpResponse::Ok() + .json(accommodations_reservations_service::get_all_of_family(m.family_id()).await?)) +} diff --git a/geneit_backend/src/controllers/mod.rs b/geneit_backend/src/controllers/mod.rs index 4cd4c7d..ccfb812 100644 --- a/geneit_backend/src/controllers/mod.rs +++ b/geneit_backend/src/controllers/mod.rs @@ -6,6 +6,7 @@ use std::fmt::{Debug, Display, Formatter}; use zip::result::ZipError; pub mod accommodations_list_controller; +pub mod accommodations_reservations_controller; pub mod auth_controller; pub mod couples_controller; pub mod data_controller; diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 4dbaa40..86d5600 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -6,9 +6,9 @@ use actix_web::{web, App, HttpServer}; use geneit_backend::app_config::AppConfig; use geneit_backend::connections::{db_connection, s3_connection}; use geneit_backend::controllers::{ - accommodations_list_controller, auth_controller, couples_controller, data_controller, - families_controller, members_controller, photos_controller, server_controller, - users_controller, + accommodations_list_controller, accommodations_reservations_controller, auth_controller, + couples_controller, data_controller, families_controller, members_controller, + photos_controller, server_controller, users_controller, }; #[actix_web::main] @@ -228,9 +228,14 @@ async fn main() -> std::io::Result<()> { ) // [ACCOMODATIONS] Reservations controller // TODO : create + // TODO : get single // TODO : update // TODO : delete - // TODO : list + // TODO : list for an accommodation + .route( + "/family/{id}/accommodations/reservations/full_list", + web::get().to(accommodations_reservations_controller::full_list), + ) // TODO : validate or reject // Photos controller .route( diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 498a10d..f30bd1b 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -1,5 +1,8 @@ use crate::app_config::AppConfig; -use crate::schema::{accommodations_list, couples, families, members, memberships, photos, users}; +use crate::schema::{ + accommodations_list, accommodations_reservations, couples, families, members, memberships, + photos, users, +}; use crate::utils::crypt_utils::sha256; use diesel::prelude::*; @@ -477,3 +480,32 @@ pub struct NewAccommodation { pub time_create: i64, pub time_update: i64, } + +/// Accommodation reservation ID holder +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] +pub struct AccommodationReservationID(pub i32); + +#[derive(Queryable, Debug, serde::Serialize)] +pub struct AccommodationReservation { + id: i32, + family_id: i32, + accommodation_id: i32, + user_id: i32, + time_create: i64, + pub time_update: i64, + pub reservation_start: i64, + pub reservation_end: i64, + pub validated: Option, +} + +#[derive(Insertable)] +#[diesel(table_name = accommodations_reservations)] +pub struct NewAccommodationReservation { + pub family_id: i32, + pub accommodation_id: i32, + pub user_id: i32, + pub time_create: i64, + pub time_update: i64, + pub reservation_start: i64, + pub reservation_end: i64, +} diff --git a/geneit_backend/src/services/accommodations_list_service.rs b/geneit_backend/src/services/accommodations_list_service.rs index 3ebbebc..6d44bc0 100644 --- a/geneit_backend/src/services/accommodations_list_service.rs +++ b/geneit_backend/src/services/accommodations_list_service.rs @@ -29,7 +29,7 @@ pub async fn get_by_id(id: AccommodationID) -> anyhow::Result { }) } -/// Get all the couples of an accommodation +/// Get all the accommodations of a family pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result> { db_connection::execute(|conn| { accommodations_list::table @@ -57,7 +57,7 @@ pub async fn exists( }) } -/// Update the information of a couple +/// Update the information of an accommodation pub async fn update(accommodation: &mut Accommodation) -> anyhow::Result<()> { accommodation.time_update = time() as i64; diff --git a/geneit_backend/src/services/accommodations_reservations_service.rs b/geneit_backend/src/services/accommodations_reservations_service.rs new file mode 100644 index 0000000..ef3c257 --- /dev/null +++ b/geneit_backend/src/services/accommodations_reservations_service.rs @@ -0,0 +1,13 @@ +use crate::connections::db_connection; +use crate::models::{AccommodationReservation, FamilyID}; +use crate::schema::accommodations_reservations; +use diesel::prelude::*; + +/// Get all the accommodations reservations of a family +pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result> { + db_connection::execute(|conn| { + accommodations_reservations::table + .filter(accommodations_reservations::dsl::family_id.eq(id.0)) + .get_results(conn) + }) +} diff --git a/geneit_backend/src/services/mod.rs b/geneit_backend/src/services/mod.rs index b5db75a..2205bef 100644 --- a/geneit_backend/src/services/mod.rs +++ b/geneit_backend/src/services/mod.rs @@ -1,6 +1,7 @@ //! # Backend services pub mod accommodations_list_service; +pub mod accommodations_reservations_service; pub mod couples_service; pub mod families_service; pub mod login_token_service; -- 2.45.2 From 1332b001c87552f5e021ded1145cf5e860c5ced5 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 28 May 2024 21:43:24 +0200 Subject: [PATCH 10/65] Can get all the reservations of a given accommodation --- .../accommodations_reservations_controller.rs | 7 +++++++ geneit_backend/src/main.rs | 6 +++++- .../accommodations_reservations_service.rs | 15 +++++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index b3ec476..f76d491 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -1,8 +1,15 @@ use crate::controllers::HttpResult; +use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath; use crate::extractors::family_extractor::FamilyInPath; use crate::services::accommodations_reservations_service; use actix_web::HttpResponse; +/// Get the reservations for a given accommodation +pub async fn get_accommodation_reservations(a: FamilyAndAccommodationInPath) -> HttpResult { + Ok(HttpResponse::Ok() + .json(accommodations_reservations_service::get_all_of_accommodation(a.id()).await?)) +} + /// Get the full list of accommodations reservations for a family pub async fn full_list(m: FamilyInPath) -> HttpResult { Ok(HttpResponse::Ok() diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 86d5600..8f1ca0e 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -231,7 +231,11 @@ async fn main() -> std::io::Result<()> { // TODO : get single // TODO : update // TODO : delete - // TODO : list for an accommodation + .route( + "/family/{id}/accommodations/reservations/accommodation/{accommodation_id}", + web::get() + .to(accommodations_reservations_controller::get_accommodation_reservations), + ) .route( "/family/{id}/accommodations/reservations/full_list", web::get().to(accommodations_reservations_controller::full_list), diff --git a/geneit_backend/src/services/accommodations_reservations_service.rs b/geneit_backend/src/services/accommodations_reservations_service.rs index ef3c257..ca76976 100644 --- a/geneit_backend/src/services/accommodations_reservations_service.rs +++ b/geneit_backend/src/services/accommodations_reservations_service.rs @@ -1,9 +1,20 @@ use crate::connections::db_connection; -use crate::models::{AccommodationReservation, FamilyID}; +use crate::models::{AccommodationID, AccommodationReservation, FamilyID}; use crate::schema::accommodations_reservations; use diesel::prelude::*; -/// Get all the accommodations reservations of a family +/// Get all the reservations of an accommodation +pub async fn get_all_of_accommodation( + id: AccommodationID, +) -> anyhow::Result> { + db_connection::execute(|conn| { + accommodations_reservations::table + .filter(accommodations_reservations::dsl::accommodation_id.eq(id.0)) + .get_results(conn) + }) +} + +/// Get all the reservations of a family pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result> { db_connection::execute(|conn| { accommodations_reservations::table -- 2.45.2 From 3efae7bfff39bcb4b4d3c1f2487c6acc338528e2 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 28 May 2024 22:12:17 +0200 Subject: [PATCH 11/65] Can get single accommodation reservation information --- .../accommodations_reservations_controller.rs | 6 ++ .../src/extractors/accommodation_extractor.rs | 2 +- .../accommodation_reservation_extractor.rs | 99 +++++++++++++++++++ geneit_backend/src/extractors/mod.rs | 1 + geneit_backend/src/main.rs | 11 ++- geneit_backend/src/models.rs | 14 +++ .../accommodations_reservations_service.rs | 13 ++- 7 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 geneit_backend/src/extractors/accommodation_reservation_extractor.rs diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index f76d491..516f25f 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -1,5 +1,6 @@ use crate::controllers::HttpResult; use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath; +use crate::extractors::accommodation_reservation_extractor::FamilyAndAccommodationReservationInPath; use crate::extractors::family_extractor::FamilyInPath; use crate::services::accommodations_reservations_service; use actix_web::HttpResponse; @@ -15,3 +16,8 @@ pub async fn full_list(m: FamilyInPath) -> HttpResult { Ok(HttpResponse::Ok() .json(accommodations_reservations_service::get_all_of_family(m.family_id()).await?)) } + +/// Get a single accommodation reservation +pub async fn get_single(m: FamilyAndAccommodationReservationInPath) -> HttpResult { + Ok(HttpResponse::Ok().json(m.to_reservation())) +} diff --git a/geneit_backend/src/extractors/accommodation_extractor.rs b/geneit_backend/src/extractors/accommodation_extractor.rs index 76bfca7..f772ddf 100644 --- a/geneit_backend/src/extractors/accommodation_extractor.rs +++ b/geneit_backend/src/extractors/accommodation_extractor.rs @@ -72,7 +72,7 @@ impl FromRequest for FamilyAndAccommodationInPath { .await? .accommodation_id; - FamilyAndAccommodationInPath::load_accommodation_from_path(family, accommodation_id) + Self::load_accommodation_from_path(family, accommodation_id) .await .map_err(|e| { log::error!("Failed to extract accommodation ID from URL! {}", e); diff --git a/geneit_backend/src/extractors/accommodation_reservation_extractor.rs b/geneit_backend/src/extractors/accommodation_reservation_extractor.rs new file mode 100644 index 0000000..483028f --- /dev/null +++ b/geneit_backend/src/extractors/accommodation_reservation_extractor.rs @@ -0,0 +1,99 @@ +use crate::extractors::family_extractor::FamilyInPath; +use crate::models::{ + Accommodation, AccommodationReservation, AccommodationReservationID, FamilyID, Membership, +}; +use crate::services::{accommodations_list_service, accommodations_reservations_service}; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest}; +use serde::Deserialize; +use std::fmt::Debug; +use std::ops::Deref; + +#[derive(thiserror::Error, Debug)] +enum AccommodationReservationExtractorErr { + #[error("Accommodation reservation {0:?} does not belong to family {1:?}!")] + AccommodationNotInFamily(AccommodationReservationID, FamilyID), +} + +#[derive(Debug)] +pub struct FamilyAndAccommodationReservationInPath( + Membership, + Accommodation, + AccommodationReservation, +); + +impl FamilyAndAccommodationReservationInPath { + async fn load_accommodation_reservation_from_path( + family: FamilyInPath, + reservation_id: AccommodationReservationID, + ) -> anyhow::Result { + let reservation = accommodations_reservations_service::get_by_id(reservation_id).await?; + let accommodation = + accommodations_list_service::get_by_id(reservation.accommodation_id()).await?; + + if accommodation.family_id() != family.family_id() + || reservation.family_id() != family.family_id() + { + return Err( + AccommodationReservationExtractorErr::AccommodationNotInFamily( + reservation.id(), + family.family_id(), + ) + .into(), + ); + } + + Ok(Self(family.into(), accommodation, reservation)) + } +} + +impl Deref for FamilyAndAccommodationReservationInPath { + type Target = AccommodationReservation; + + fn deref(&self) -> &Self::Target { + &self.2 + } +} + +impl FamilyAndAccommodationReservationInPath { + pub fn membership(&self) -> &Membership { + &self.0 + } + + pub fn to_accommodation(self) -> Accommodation { + self.1 + } + + pub fn to_reservation(self) -> AccommodationReservation { + self.2 + } +} + +#[derive(Deserialize)] +struct ReservationIDInPath { + reservation_id: AccommodationReservationID, +} + +impl FromRequest for FamilyAndAccommodationReservationInPath { + type Error = actix_web::Error; + type Future = futures_util::future::LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let req = req.clone(); + Box::pin(async move { + let family = FamilyInPath::extract(&req).await?; + + let reservation_id = + actix_web::web::Path::::from_request(&req, &mut Payload::None) + .await? + .reservation_id; + + Self::load_accommodation_reservation_from_path(family, reservation_id) + .await + .map_err(|e| { + log::error!("Failed to extract accommodation ID from URL! {}", e); + actix_web::error::ErrorNotFound("Could not fetch accommodation information!") + }) + }) + } +} diff --git a/geneit_backend/src/extractors/mod.rs b/geneit_backend/src/extractors/mod.rs index 8859615..868b912 100644 --- a/geneit_backend/src/extractors/mod.rs +++ b/geneit_backend/src/extractors/mod.rs @@ -1,4 +1,5 @@ pub mod accommodation_extractor; +pub mod accommodation_reservation_extractor; pub mod couple_extractor; pub mod family_extractor; pub mod member_extractor; diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 8f1ca0e..be557d3 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -227,10 +227,6 @@ async fn main() -> std::io::Result<()> { web::delete().to(accommodations_list_controller::delete), ) // [ACCOMODATIONS] Reservations controller - // TODO : create - // TODO : get single - // TODO : update - // TODO : delete .route( "/family/{id}/accommodations/reservations/accommodation/{accommodation_id}", web::get() @@ -240,6 +236,13 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/reservations/full_list", web::get().to(accommodations_reservations_controller::full_list), ) + // TODO : create + .route( + "/family/{id}/accommodations/reservation/{reservation_id}", + web::get().to(accommodations_reservations_controller::get_single), + ) + // TODO : update + // TODO : delete // TODO : validate or reject // Photos controller .route( diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index f30bd1b..2acf822 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -498,6 +498,20 @@ pub struct AccommodationReservation { pub validated: Option, } +impl AccommodationReservation { + pub fn id(&self) -> AccommodationReservationID { + AccommodationReservationID(self.id) + } + + pub fn accommodation_id(&self) -> AccommodationID { + AccommodationID(self.accommodation_id) + } + + pub fn family_id(&self) -> FamilyID { + FamilyID(self.family_id) + } +} + #[derive(Insertable)] #[diesel(table_name = accommodations_reservations)] pub struct NewAccommodationReservation { diff --git a/geneit_backend/src/services/accommodations_reservations_service.rs b/geneit_backend/src/services/accommodations_reservations_service.rs index ca76976..95dbf05 100644 --- a/geneit_backend/src/services/accommodations_reservations_service.rs +++ b/geneit_backend/src/services/accommodations_reservations_service.rs @@ -1,5 +1,7 @@ use crate::connections::db_connection; -use crate::models::{AccommodationID, AccommodationReservation, FamilyID}; +use crate::models::{ + AccommodationID, AccommodationReservation, AccommodationReservationID, FamilyID, +}; use crate::schema::accommodations_reservations; use diesel::prelude::*; @@ -22,3 +24,12 @@ pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result anyhow::Result { + db_connection::execute(|conn| { + accommodations_reservations::table + .filter(accommodations_reservations::dsl::id.eq(id.0)) + .get_result(conn) + }) +} -- 2.45.2 From 5b9d82889cf6f1e1c73f52ac3ed0b59abf16c882 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 28 May 2024 22:14:34 +0200 Subject: [PATCH 12/65] Update TODO list --- geneit_backend/src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index be557d3..32c5f5e 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -244,6 +244,11 @@ async fn main() -> std::io::Result<()> { // TODO : update // TODO : delete // TODO : validate or reject + // [ACCOMMODATIONS] Calendars controller + // TODO : create + // TODO : list + // TODO : delete + // TODO : anonymous URL access // Photos controller .route( "/photo/{id}", -- 2.45.2 From 5f25a516e93ad962c6355f2cac38d046cc3a77e9 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 29 May 2024 22:16:50 +0200 Subject: [PATCH 13/65] Can create reservation requests --- .../accommodations_reservations_controller.rs | 63 ++++++++++++++++++- geneit_backend/src/main.rs | 5 +- .../accommodations_reservations_service.rs | 50 +++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index 516f25f..1635ae3 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -2,8 +2,69 @@ use crate::controllers::HttpResult; use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath; use crate::extractors::accommodation_reservation_extractor::FamilyAndAccommodationReservationInPath; use crate::extractors::family_extractor::FamilyInPath; +use crate::models::NewAccommodationReservation; use crate::services::accommodations_reservations_service; -use actix_web::HttpResponse; +use crate::utils::time_utils::time; +use actix_web::{web, HttpResponse}; + +#[derive(serde::Deserialize)] +pub struct CreateReservationQuery { + start: usize, + end: usize, +} + +/// Create a reservation +pub async fn create_reservation( + a: FamilyAndAccommodationInPath, + req: web::Json, +) -> HttpResult { + if !a.open_to_reservations { + return Ok(HttpResponse::ExpectationFailed() + .json("The accommodation is not open to reservations!")); + } + + if (req.start as i64) < (time() as i64 - 3600 * 24 * 30) { + return Ok(HttpResponse::BadRequest().json("Start time is too far in the past!")); + } + + if req.start > req.end { + return Ok(HttpResponse::BadRequest().json("End time happens before start time!")); + } + + let existing = accommodations_reservations_service::get_reservations_for_time_interval( + a.id(), + req.start, + req.end, + ) + .await?; + + if existing.iter().any(|r| r.validated != Some(false)) { + return Ok( + HttpResponse::Conflict().json("This reservation is in conflict with another one!") + ); + } + + let mut reservation = + accommodations_reservations_service::create(&NewAccommodationReservation { + family_id: a.family_id().0, + accommodation_id: a.id().0, + user_id: a.membership().user_id().0, + time_create: time() as i64, + time_update: time() as i64, + reservation_start: req.start as i64, + reservation_end: req.end as i64, + }) + .await?; + + // Auto validate reservation if requested + if !a.need_validation { + reservation.validated = Some(true); + + accommodations_reservations_service::update(&mut reservation).await?; + } + + Ok(HttpResponse::Ok().json(reservation)) +} /// Get the reservations for a given accommodation pub async fn get_accommodation_reservations(a: FamilyAndAccommodationInPath) -> HttpResult { diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 32c5f5e..3e898d3 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -236,7 +236,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/reservations/full_list", web::get().to(accommodations_reservations_controller::full_list), ) - // TODO : create + .route( + "/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/create", + web::post().to(accommodations_reservations_controller::create_reservation), + ) .route( "/family/{id}/accommodations/reservation/{reservation_id}", web::get().to(accommodations_reservations_controller::get_single), diff --git a/geneit_backend/src/services/accommodations_reservations_service.rs b/geneit_backend/src/services/accommodations_reservations_service.rs index 95dbf05..fa00c4a 100644 --- a/geneit_backend/src/services/accommodations_reservations_service.rs +++ b/geneit_backend/src/services/accommodations_reservations_service.rs @@ -1,10 +1,42 @@ use crate::connections::db_connection; use crate::models::{ AccommodationID, AccommodationReservation, AccommodationReservationID, FamilyID, + NewAccommodationReservation, }; use crate::schema::accommodations_reservations; +use crate::utils::time_utils::time; use diesel::prelude::*; +/// Create a new reservation +pub async fn create(new: &NewAccommodationReservation) -> anyhow::Result { + db_connection::execute(|conn| { + let res: AccommodationReservation = diesel::insert_into(accommodations_reservations::table) + .values(new) + .get_result(conn)?; + + Ok(res) + }) +} + +/// Update a reservation +pub async fn update(r: &mut AccommodationReservation) -> anyhow::Result<()> { + r.time_update = time() as i64; + + db_connection::execute(|conn| { + diesel::update( + accommodations_reservations::dsl::accommodations_reservations + .filter(accommodations_reservations::dsl::id.eq(r.id().0)), + ) + .set(( + accommodations_reservations::dsl::time_update.eq(r.time_update), + accommodations_reservations::dsl::validated.eq(r.validated), + )) + .execute(conn) + })?; + + Ok(()) +} + /// Get all the reservations of an accommodation pub async fn get_all_of_accommodation( id: AccommodationID, @@ -33,3 +65,21 @@ pub async fn get_by_id(id: AccommodationReservationID) -> anyhow::Result anyhow::Result> { + db_connection::execute(|conn| { + accommodations_reservations::table + .filter( + accommodations_reservations::dsl::accommodation_id + .eq(id.0) + .and(accommodations_reservations::dsl::reservation_start.lt((end) as i64)) + .and(accommodations_reservations::dsl::reservation_end.gt((start) as i64)), + ) + .get_results(conn) + }) +} -- 2.45.2 From 70d8020610d0ec4594f0c74b5a74015e7198f4ce Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 29 May 2024 22:18:35 +0200 Subject: [PATCH 14/65] Prohibit similar start time and end time --- .../src/controllers/accommodations_reservations_controller.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index 1635ae3..eac0f7b 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -27,6 +27,10 @@ pub async fn create_reservation( return Ok(HttpResponse::BadRequest().json("Start time is too far in the past!")); } + if req.start == req.end { + return Ok(HttpResponse::BadRequest().json("Start and end time must be different!")); + } + if req.start > req.end { return Ok(HttpResponse::BadRequest().json("End time happens before start time!")); } -- 2.45.2 From 33b03a4d74de24a9c1f6930fb7660d8effaf9297 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 29 May 2024 22:28:38 +0200 Subject: [PATCH 15/65] Can delete a reservation --- .../accommodations_reservations_controller.rs | 13 +++++++++++++ geneit_backend/src/main.rs | 5 ++++- geneit_backend/src/models.rs | 4 ++++ .../accommodations_reservations_service.rs | 14 ++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index eac0f7b..3159d3a 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -86,3 +86,16 @@ pub async fn full_list(m: FamilyInPath) -> HttpResult { pub async fn get_single(m: FamilyAndAccommodationReservationInPath) -> HttpResult { Ok(HttpResponse::Ok().json(m.to_reservation())) } + +/// Delete a reservation +pub async fn delete(m: FamilyAndAccommodationReservationInPath) -> HttpResult { + if m.membership().user_id() != m.user_id() { + return Ok( + HttpResponse::BadRequest().json("Only the owner of a reservation can delete it!") + ); + } + + accommodations_reservations_service::delete(m.to_reservation()).await?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 3e898d3..f9c0250 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -245,7 +245,10 @@ async fn main() -> std::io::Result<()> { web::get().to(accommodations_reservations_controller::get_single), ) // TODO : update - // TODO : delete + .route( + "/family/{id}/accommodations/reservation/{reservation_id}", + web::delete().to(accommodations_reservations_controller::delete), + ) // TODO : validate or reject // [ACCOMMODATIONS] Calendars controller // TODO : create diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 2acf822..5bbf860 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -510,6 +510,10 @@ impl AccommodationReservation { pub fn family_id(&self) -> FamilyID { FamilyID(self.family_id) } + + pub fn user_id(&self) -> UserID { + UserID(self.user_id) + } } #[derive(Insertable)] diff --git a/geneit_backend/src/services/accommodations_reservations_service.rs b/geneit_backend/src/services/accommodations_reservations_service.rs index fa00c4a..f3425cc 100644 --- a/geneit_backend/src/services/accommodations_reservations_service.rs +++ b/geneit_backend/src/services/accommodations_reservations_service.rs @@ -37,6 +37,20 @@ pub async fn update(r: &mut AccommodationReservation) -> anyhow::Result<()> { Ok(()) } +/// Delete a reservation +pub async fn delete(r: AccommodationReservation) -> anyhow::Result<()> { + // Remove the reservation + db_connection::execute(|conn| { + diesel::delete( + accommodations_reservations::dsl::accommodations_reservations + .filter(accommodations_reservations::dsl::id.eq(r.id().0)), + ) + .execute(conn) + })?; + + Ok(()) +} + /// Get all the reservations of an accommodation pub async fn get_all_of_accommodation( id: AccommodationID, -- 2.45.2 From f07a6a89238fb284490c5b28809b97db1f9fa389 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 30 May 2024 19:54:05 +0200 Subject: [PATCH 16/65] Can change a reservation time --- .../accommodations_reservations_controller.rs | 109 +++++++++++++----- .../accommodation_reservation_extractor.rs | 4 + geneit_backend/src/main.rs | 5 +- .../accommodations_reservations_service.rs | 2 + 4 files changed, 88 insertions(+), 32 deletions(-) diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index 3159d3a..de15350 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -2,50 +2,67 @@ use crate::controllers::HttpResult; use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath; use crate::extractors::accommodation_reservation_extractor::FamilyAndAccommodationReservationInPath; use crate::extractors::family_extractor::FamilyInPath; -use crate::models::NewAccommodationReservation; +use crate::models::{Accommodation, AccommodationReservationID, NewAccommodationReservation}; use crate::services::accommodations_reservations_service; use crate::utils::time_utils::time; use actix_web::{web, HttpResponse}; #[derive(serde::Deserialize)] -pub struct CreateReservationQuery { +pub struct UpdateReservationQuery { start: usize, end: usize, } +impl UpdateReservationQuery { + /// Check whether a reservation request is valid or not + async fn validate( + &self, + a: &Accommodation, + resa_id: Option, + ) -> anyhow::Result> { + if !a.open_to_reservations { + return Ok(Some( + "The accommodation is not open to reservations create / update!", + )); + } + + if (self.start as i64) < (time() as i64 - 3600 * 24 * 30) { + return Ok(Some("Start time is too far in the past!")); + } + + if self.start == self.end { + return Ok(Some("Start and end time must be different!")); + } + + if self.start > self.end { + return Ok(Some("End time happens before start time!")); + } + + let existing = accommodations_reservations_service::get_reservations_for_time_interval( + a.id(), + self.start, + self.end, + ) + .await?; + + if existing + .iter() + .any(|r| r.validated != Some(false) && resa_id != Some(r.id())) + { + return Ok(Some("This reservation is in conflict with another one!")); + } + + Ok(None) + } +} + /// Create a reservation pub async fn create_reservation( a: FamilyAndAccommodationInPath, - req: web::Json, + req: web::Json, ) -> HttpResult { - if !a.open_to_reservations { - return Ok(HttpResponse::ExpectationFailed() - .json("The accommodation is not open to reservations!")); - } - - if (req.start as i64) < (time() as i64 - 3600 * 24 * 30) { - return Ok(HttpResponse::BadRequest().json("Start time is too far in the past!")); - } - - if req.start == req.end { - return Ok(HttpResponse::BadRequest().json("Start and end time must be different!")); - } - - if req.start > req.end { - return Ok(HttpResponse::BadRequest().json("End time happens before start time!")); - } - - let existing = accommodations_reservations_service::get_reservations_for_time_interval( - a.id(), - req.start, - req.end, - ) - .await?; - - if existing.iter().any(|r| r.validated != Some(false)) { - return Ok( - HttpResponse::Conflict().json("This reservation is in conflict with another one!") - ); + if let Some(err) = req.validate(&a, None).await? { + return Ok(HttpResponse::BadRequest().json(err)); } let mut reservation = @@ -87,6 +104,36 @@ pub async fn get_single(m: FamilyAndAccommodationReservationInPath) -> HttpResul Ok(HttpResponse::Ok().json(m.to_reservation())) } +/// Update a reservation +pub async fn update_single( + m: FamilyAndAccommodationReservationInPath, + req: web::Json, +) -> HttpResult { + if let Some(err) = req.validate(m.as_accommodation(), Some(m.id())).await? { + return Ok(HttpResponse::BadRequest().json(err)); + } + + if m.membership().user_id() != m.user_id() { + return Ok( + HttpResponse::BadRequest().json("Only the owner of a reservation can change it!") + ); + } + + let need_validation = m.as_accommodation().need_validation; + + let mut reservation = m.to_reservation(); + reservation.reservation_start = req.start as i64; + reservation.reservation_end = req.end as i64; + + if need_validation { + reservation.validated = None; + } + + accommodations_reservations_service::update(&mut reservation).await?; + + Ok(HttpResponse::Accepted().finish()) +} + /// Delete a reservation pub async fn delete(m: FamilyAndAccommodationReservationInPath) -> HttpResult { if m.membership().user_id() != m.user_id() { diff --git a/geneit_backend/src/extractors/accommodation_reservation_extractor.rs b/geneit_backend/src/extractors/accommodation_reservation_extractor.rs index 483028f..d62d63c 100644 --- a/geneit_backend/src/extractors/accommodation_reservation_extractor.rs +++ b/geneit_backend/src/extractors/accommodation_reservation_extractor.rs @@ -60,6 +60,10 @@ impl FamilyAndAccommodationReservationInPath { &self.0 } + pub fn as_accommodation(&self) -> &Accommodation { + &self.1 + } + pub fn to_accommodation(self) -> Accommodation { self.1 } diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index f9c0250..5799638 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -244,7 +244,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/reservation/{reservation_id}", web::get().to(accommodations_reservations_controller::get_single), ) - // TODO : update + .route( + "/family/{id}/accommodations/reservation/{reservation_id}", + web::patch().to(accommodations_reservations_controller::update_single), + ) .route( "/family/{id}/accommodations/reservation/{reservation_id}", web::delete().to(accommodations_reservations_controller::delete), diff --git a/geneit_backend/src/services/accommodations_reservations_service.rs b/geneit_backend/src/services/accommodations_reservations_service.rs index f3425cc..0cd018b 100644 --- a/geneit_backend/src/services/accommodations_reservations_service.rs +++ b/geneit_backend/src/services/accommodations_reservations_service.rs @@ -30,6 +30,8 @@ pub async fn update(r: &mut AccommodationReservation) -> anyhow::Result<()> { .set(( accommodations_reservations::dsl::time_update.eq(r.time_update), accommodations_reservations::dsl::validated.eq(r.validated), + accommodations_reservations::dsl::reservation_start.eq(r.reservation_start), + accommodations_reservations::dsl::reservation_end.eq(r.reservation_end), )) .execute(conn) })?; -- 2.45.2 From 82dbf11b42659b4d2d7fd10fc26b613d67304b16 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 30 May 2024 19:56:35 +0200 Subject: [PATCH 17/65] Fix typo --- .../src/controllers/accommodations_reservations_controller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index de15350..67469fb 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -22,7 +22,7 @@ impl UpdateReservationQuery { ) -> anyhow::Result> { if !a.open_to_reservations { return Ok(Some( - "The accommodation is not open to reservations create / update!", + "The accommodation is not open to reservations creation / update!", )); } -- 2.45.2 From 5075e8843bb306148c457ed53fdafe29f3df6762 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 30 May 2024 21:43:27 +0200 Subject: [PATCH 18/65] Can validate or reject reservation --- .../accommodations_reservations_controller.rs | 48 +++++++++++++++++++ geneit_backend/src/main.rs | 5 +- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index 67469fb..afee380 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -146,3 +146,51 @@ pub async fn delete(m: FamilyAndAccommodationReservationInPath) -> HttpResult { Ok(HttpResponse::Accepted().finish()) } + +#[derive(serde::Deserialize)] +pub struct ValidateQuery { + validate: bool, +} + +/// Validate or reject a reservation +pub async fn validate_or_reject( + m: FamilyAndAccommodationReservationInPath, + q: web::Json, +) -> HttpResult { + if !m.membership().is_admin { + return Ok(HttpResponse::BadRequest().json("Only an admin can validate a reservation!")); + } + + if m.validated == Some(q.validate) { + return Ok( + HttpResponse::AlreadyReported().json("This reservation has already been processed!") + ); + } + + // In case of re-validation, check that the time is still available + if m.validated == Some(false) && q.validate { + let potential_conflicts = + accommodations_reservations_service::get_reservations_for_time_interval( + m.accommodation_id(), + m.reservation_start as usize, + m.reservation_end as usize, + ) + .await?; + + if potential_conflicts + .iter() + .any(|a| a.validated != Some(false)) + { + return Ok(HttpResponse::Conflict().json( + "This cannot be accepted as it would create a conflict with another reservation!", + )); + } + } + + // Update reservation validation status + let mut reservation = m.to_reservation(); + reservation.validated = Some(q.validate); + accommodations_reservations_service::update(&mut reservation).await?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 5799638..0de9626 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -252,7 +252,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/reservation/{reservation_id}", web::delete().to(accommodations_reservations_controller::delete), ) - // TODO : validate or reject + .route( + "/family/{id}/accommodations/reservation/{reservation_id}/validate", + web::post().to(accommodations_reservations_controller::validate_or_reject), + ) // [ACCOMMODATIONS] Calendars controller // TODO : create // TODO : list -- 2.45.2 From 9f72cd9b9c3efc74a7a2406a0ce83392e4597c8f Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 30 May 2024 21:46:55 +0200 Subject: [PATCH 19/65] Fix bad assumption --- .../src/controllers/accommodations_reservations_controller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index afee380..16c66f1 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -158,7 +158,7 @@ pub async fn validate_or_reject( q: web::Json, ) -> HttpResult { if !m.membership().is_admin { - return Ok(HttpResponse::BadRequest().json("Only an admin can validate a reservation!")); + return Ok(HttpResponse::BadRequest().json("Only a family admin can validate a reservation!")); } if m.validated == Some(q.validate) { -- 2.45.2 From 2346c90be8aced7a803a1c0a55655bd101a74970 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 30 May 2024 21:59:00 +0200 Subject: [PATCH 20/65] Add new table to define iCal calendars URLs --- .../down.sql | 1 + .../up.sql | 21 ++++++++++++++++++- .../accommodations_reservations_controller.rs | 4 +++- geneit_backend/src/schema.rs | 19 +++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql index 3512fca..bf3bfb7 100644 --- a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql +++ b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/down.sql @@ -1,5 +1,6 @@ ALTER TABLE public.families DROP COLUMN enable_accommodations; +DROP TABLE IF EXISTS accommodations_reservations_cals_urls; DROP TABLE IF EXISTS accommodations_reservations; DROP TABLE IF EXISTS accommodations_list; \ No newline at end of file diff --git a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql index 6ce2823..79d45ec 100644 --- a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql +++ b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql @@ -18,6 +18,9 @@ CREATE TABLE IF NOT EXISTS accommodations_list open_to_reservations BOOLEAN NOT NULL DEFAULT false ); +COMMENT ON COLUMN accommodations_list.need_validation is 'true if family admin review is required for validation. False otherwise'; +COMMENT ON COLUMN accommodations_list.open_to_reservations is 'true if reservations can be created / updated. False otherwise'; + CREATE TABLE IF NOT EXISTS accommodations_reservations ( id SERIAL PRIMARY KEY, @@ -29,4 +32,20 @@ CREATE TABLE IF NOT EXISTS accommodations_reservations reservation_start BIGINT NOT NULL, reservation_end BIGINT NOT NULL, validated BOOLEAN NULL -); \ No newline at end of file +); + +COMMENT ON COLUMN accommodations_reservations.validated is 'null if not reviewed yet. true if reservation is accepted. false if reservation is rejected'; + +CREATE TABLE IF NOT EXISTS accommodations_reservations_cals_urls +( + id SERIAL PRIMARY KEY, + family_id integer NOT NULL REFERENCES families ON DELETE CASCADE, + accommodation_id integer NULL REFERENCES accommodations_list ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + name VARCHAR(50) NOT NULL, + token VARCHAR(50) NOT NULL, + time_create BIGINT NOT NULL, + time_used BIGINT NOT NULL +); + +COMMENT ON COLUMN accommodations_reservations_cals_urls.accommodation_id is 'null to get reservations of all accommodations. otherwise get the reservations of the specified accommodation only'; diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index 16c66f1..bda86ca 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -158,7 +158,9 @@ pub async fn validate_or_reject( q: web::Json, ) -> HttpResult { if !m.membership().is_admin { - return Ok(HttpResponse::BadRequest().json("Only a family admin can validate a reservation!")); + return Ok( + HttpResponse::BadRequest().json("Only a family admin can validate a reservation!") + ); } if m.validated == Some(q.validate) { diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index 14ad1a2..6f6200d 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -28,6 +28,21 @@ diesel::table! { } } +diesel::table! { + accommodations_reservations_cals_urls (id) { + id -> Int4, + family_id -> Int4, + accommodation_id -> Nullable, + user_id -> Int4, + #[max_length = 50] + name -> Varchar, + #[max_length = 50] + token -> Varchar, + time_create -> Int8, + time_used -> Int8, + } +} + diesel::table! { couples (id) { id -> Int4, @@ -152,6 +167,9 @@ diesel::joinable!(accommodations_list -> families (family_id)); diesel::joinable!(accommodations_reservations -> accommodations_list (accommodation_id)); diesel::joinable!(accommodations_reservations -> families (family_id)); diesel::joinable!(accommodations_reservations -> users (user_id)); +diesel::joinable!(accommodations_reservations_cals_urls -> accommodations_list (accommodation_id)); +diesel::joinable!(accommodations_reservations_cals_urls -> families (family_id)); +diesel::joinable!(accommodations_reservations_cals_urls -> users (user_id)); diesel::joinable!(couples -> families (family_id)); diesel::joinable!(couples -> photos (photo_id)); diesel::joinable!(members -> families (family_id)); @@ -162,6 +180,7 @@ diesel::joinable!(memberships -> users (user_id)); diesel::allow_tables_to_appear_in_same_query!( accommodations_list, accommodations_reservations, + accommodations_reservations_cals_urls, couples, families, members, -- 2.45.2 From b34959df33980459b155cc9b3ff5d089a5e74c52 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 31 May 2024 21:41:58 +0200 Subject: [PATCH 21/65] Can create calendars --- geneit_app/src/api/ServerApi.ts | 4 +- geneit_backend/src/constants.rs | 13 +++-- .../accommodations_list_controller.rs | 4 +- ...tions_reservations_calendars_controller.rs | 52 +++++++++++++++++++ geneit_backend/src/controllers/mod.rs | 1 + geneit_backend/src/main.rs | 12 +++-- geneit_backend/src/models.rs | 32 +++++++++++- ...odations_reservations_calendars_service.rs | 35 +++++++++++++ geneit_backend/src/services/mod.rs | 1 + 9 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs create mode 100644 geneit_backend/src/services/accommodations_reservations_calendars_service.rs diff --git a/geneit_app/src/api/ServerApi.ts b/geneit_app/src/api/ServerApi.ts index 2238e8d..0ac625f 100644 --- a/geneit_app/src/api/ServerApi.ts +++ b/geneit_app/src/api/ServerApi.ts @@ -32,8 +32,8 @@ interface Constraints { member_country: LenConstraint; member_sex: LenConstraint; member_note: LenConstraint; - accomodation_name_len: LenConstraint; - accomodation_description_len: LenConstraint; + accommodation_name_len: LenConstraint; + accommodation_description_len: LenConstraint; } interface OIDCProvider { diff --git a/geneit_backend/src/constants.rs b/geneit_backend/src/constants.rs index c8aca30..69254f1 100644 --- a/geneit_backend/src/constants.rs +++ b/geneit_backend/src/constants.rs @@ -61,8 +61,9 @@ pub struct StaticConstraints { pub member_sex: SizeConstraint, pub member_note: SizeConstraint, - pub accomodation_name_len: SizeConstraint, - pub accomodation_description_len: SizeConstraint, + pub accommodation_name_len: SizeConstraint, + pub accommodation_description_len: SizeConstraint, + pub accommodation_calendar_name_len: SizeConstraint, } impl Default for StaticConstraints { @@ -95,8 +96,9 @@ impl Default for StaticConstraints { member_sex: SizeConstraint::new(0, 1), member_note: SizeConstraint::new(0, 35000), - accomodation_name_len: SizeConstraint::new(1, 50), - accomodation_description_len: SizeConstraint::new(0, 500), + accommodation_name_len: SizeConstraint::new(1, 50), + accommodation_description_len: SizeConstraint::new(0, 500), + accommodation_calendar_name_len: SizeConstraint::new(2, 50), } } } @@ -140,3 +142,6 @@ pub const THUMB_WIDTH: u32 = 350; /// Thumbnail height pub const THUMB_HEIGHT: u32 = 350; + +/// Accommodations reservations calendars tokens len +pub const ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN: usize = 50; diff --git a/geneit_backend/src/controllers/accommodations_list_controller.rs b/geneit_backend/src/controllers/accommodations_list_controller.rs index 665bcb2..6d0028f 100644 --- a/geneit_backend/src/controllers/accommodations_list_controller.rs +++ b/geneit_backend/src/controllers/accommodations_list_controller.rs @@ -26,13 +26,13 @@ impl AccommodationRequest { pub async fn to_accommodation(self, accommodation: &mut Accommodation) -> anyhow::Result<()> { let c = StaticConstraints::default(); - if !c.accomodation_name_len.validate(&self.name) { + if !c.accommodation_name_len.validate(&self.name) { return Err(AccommodationListControllerErr::MalformedName.into()); } accommodation.name = self.name; if let Some(d) = &self.description { - if !c.accomodation_description_len.validate(d) { + if !c.accommodation_description_len.validate(d) { return Err(AccommodationListControllerErr::MalformedDescription.into()); } } diff --git a/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs new file mode 100644 index 0000000..c283774 --- /dev/null +++ b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs @@ -0,0 +1,52 @@ +use crate::constants::StaticConstraints; +use crate::controllers::HttpResult; +use crate::extractors::family_extractor::FamilyInPath; +use crate::models::AccommodationID; +use crate::services::{accommodations_list_service, accommodations_reservations_calendars_service}; +use actix_web::{web, HttpResponse}; + +#[derive(serde::Deserialize)] +pub struct CreateCalendarQuery { + accommodation_id: Option, + name: String, +} + +/// Create a calendar +pub async fn create(a: FamilyInPath, req: web::Json) -> HttpResult { + let accommodation_id = match req.accommodation_id { + Some(i) => { + let accommodation = match accommodations_list_service::get_by_id(i).await { + Ok(a) => a, + Err(e) => { + log::error!("Failed to get accommodation information! {e}"); + return Ok(HttpResponse::NotFound() + .json("The accommodation was not found in the family!")); + } + }; + + if accommodation.family_id() != a.family_id() { + return Ok( + HttpResponse::NotFound().json("The accommodation was not found in the family!") + ); + } + + Some(accommodation.id()) + } + None => None, + }; + + let conf = StaticConstraints::default(); + if !conf.accommodation_calendar_name_len.validate(&req.name) { + return Ok(HttpResponse::BadRequest().json("Invalid accommodation name!")); + } + + let calendar = accommodations_reservations_calendars_service::create( + a.user_id(), + a.family_id(), + accommodation_id, + &req.name, + ) + .await?; + + Ok(HttpResponse::Ok().json(calendar)) +} diff --git a/geneit_backend/src/controllers/mod.rs b/geneit_backend/src/controllers/mod.rs index ccfb812..17b70a8 100644 --- a/geneit_backend/src/controllers/mod.rs +++ b/geneit_backend/src/controllers/mod.rs @@ -6,6 +6,7 @@ use std::fmt::{Debug, Display, Formatter}; use zip::result::ZipError; pub mod accommodations_list_controller; +pub mod accommodations_reservations_calendars_controller; pub mod accommodations_reservations_controller; pub mod auth_controller; pub mod couples_controller; diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 0de9626..c8d0265 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -6,9 +6,10 @@ use actix_web::{web, App, HttpServer}; use geneit_backend::app_config::AppConfig; use geneit_backend::connections::{db_connection, s3_connection}; use geneit_backend::controllers::{ - accommodations_list_controller, accommodations_reservations_controller, auth_controller, - couples_controller, data_controller, families_controller, members_controller, - photos_controller, server_controller, users_controller, + accommodations_list_controller, accommodations_reservations_calendars_controller, + accommodations_reservations_controller, auth_controller, couples_controller, data_controller, + families_controller, members_controller, photos_controller, server_controller, + users_controller, }; #[actix_web::main] @@ -257,7 +258,10 @@ async fn main() -> std::io::Result<()> { web::post().to(accommodations_reservations_controller::validate_or_reject), ) // [ACCOMMODATIONS] Calendars controller - // TODO : create + .route( + "/family/{id}/accommodations/reservations_calendars/create", + web::post().to(accommodations_reservations_calendars_controller::create), + ) // TODO : list // TODO : delete // TODO : anonymous URL access diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 5bbf860..3896239 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -1,7 +1,7 @@ use crate::app_config::AppConfig; use crate::schema::{ - accommodations_list, accommodations_reservations, couples, families, members, memberships, - photos, users, + accommodations_list, accommodations_reservations, accommodations_reservations_cals_urls, + couples, families, members, memberships, photos, users, }; use crate::utils::crypt_utils::sha256; use diesel::prelude::*; @@ -527,3 +527,31 @@ pub struct NewAccommodationReservation { pub reservation_start: i64, pub reservation_end: i64, } + +/// Accommodation reservation calendar ID holder +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] +pub struct AccommodationReservationCalendarID(pub i32); + +#[derive(Queryable, Debug, serde::Serialize)] +pub struct AccommodationReservationCalendar { + id: i32, + family_id: i32, + accommodation_id: Option, + user_id: i32, + name: String, + token: String, + pub time_create: i64, + pub time_used: i64, +} + +#[derive(Insertable)] +#[diesel(table_name = accommodations_reservations_cals_urls)] +pub struct NewAccommodationReservationCalendar { + pub family_id: i32, + pub accommodation_id: Option, + pub user_id: i32, + pub name: String, + pub token: String, + pub time_create: i64, + pub time_used: i64, +} diff --git a/geneit_backend/src/services/accommodations_reservations_calendars_service.rs b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs new file mode 100644 index 0000000..aa5ab7a --- /dev/null +++ b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs @@ -0,0 +1,35 @@ +use crate::connections::db_connection; +use crate::constants; +use crate::models::{ + AccommodationID, AccommodationReservationCalendar, FamilyID, + NewAccommodationReservationCalendar, UserID, +}; +use crate::schema::accommodations_reservations_cals_urls; +use crate::utils::string_utils::rand_str; +use crate::utils::time_utils::time; +use diesel::prelude::*; + +/// Create a new reservation calendar entry +pub async fn create( + user_id: UserID, + family_id: FamilyID, + accommodation_id: Option, + name: &str, +) -> anyhow::Result { + db_connection::execute(|conn| { + let res: AccommodationReservationCalendar = + diesel::insert_into(accommodations_reservations_cals_urls::table) + .values(&NewAccommodationReservationCalendar { + family_id: family_id.0, + accommodation_id: accommodation_id.map(|i| i.0), + user_id: user_id.0, + name: name.to_string(), + token: rand_str(constants::ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN), + time_create: time() as i64, + time_used: time() as i64, + }) + .get_result(conn)?; + + Ok(res) + }) +} diff --git a/geneit_backend/src/services/mod.rs b/geneit_backend/src/services/mod.rs index 2205bef..cd5c67f 100644 --- a/geneit_backend/src/services/mod.rs +++ b/geneit_backend/src/services/mod.rs @@ -1,6 +1,7 @@ //! # Backend services pub mod accommodations_list_service; +pub mod accommodations_reservations_calendars_service; pub mod accommodations_reservations_service; pub mod couples_service; pub mod families_service; -- 2.45.2 From 51f8aaccb68b6d9bb52ee8a81ed320bb21640771 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 31 May 2024 22:09:33 +0200 Subject: [PATCH 22/65] Can get accommodations reservations of a family --- ...odations_reservations_calendars_controller.rs | 8 ++++++++ geneit_backend/src/main.rs | 5 ++++- ...ommodations_reservations_calendars_service.rs | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs index c283774..a67c805 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs @@ -50,3 +50,11 @@ pub async fn create(a: FamilyInPath, req: web::Json) -> Htt Ok(HttpResponse::Ok().json(calendar)) } + +/// Get the list of calendars of the user +pub async fn get_list(a: FamilyInPath) -> HttpResult { + let users = + accommodations_reservations_calendars_service::get_all_of_user(a.user_id(), a.family_id()) + .await?; + Ok(HttpResponse::Ok().json(users)) +} diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index c8d0265..1abc649 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -262,7 +262,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/reservations_calendars/create", web::post().to(accommodations_reservations_calendars_controller::create), ) - // TODO : list + .route( + "/family/{id}/accommodations/reservations_calendars/list", + web::get().to(accommodations_reservations_calendars_controller::get_list), + ) // TODO : delete // TODO : anonymous URL access // Photos controller diff --git a/geneit_backend/src/services/accommodations_reservations_calendars_service.rs b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs index aa5ab7a..20cc6e3 100644 --- a/geneit_backend/src/services/accommodations_reservations_calendars_service.rs +++ b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs @@ -33,3 +33,19 @@ pub async fn create( Ok(res) }) } + +/// Get all the calendars of a user +pub async fn get_all_of_user( + user: UserID, + family: FamilyID, +) -> anyhow::Result> { + db_connection::execute(|conn| { + accommodations_reservations_cals_urls::table + .filter( + accommodations_reservations_cals_urls::dsl::family_id + .eq(family.0) + .and(accommodations_reservations_cals_urls::dsl::user_id.eq(user.0)), + ) + .get_results(conn) + }) +} -- 2.45.2 From e62f536c03063cba65a2d30c62725a0957313bbd Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 31 May 2024 22:31:23 +0200 Subject: [PATCH 23/65] Can delete a reservation --- ...tions_reservations_calendars_controller.rs | 11 ++- ...modation_reservation_calendar_extractor.rs | 93 +++++++++++++++++++ geneit_backend/src/extractors/mod.rs | 1 + geneit_backend/src/main.rs | 5 +- geneit_backend/src/models.rs | 18 ++++ ...odations_reservations_calendars_service.rs | 29 +++++- 6 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 geneit_backend/src/extractors/accommodation_reservation_calendar_extractor.rs diff --git a/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs index a67c805..e9b85bd 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs @@ -1,7 +1,8 @@ use crate::constants::StaticConstraints; use crate::controllers::HttpResult; +use crate::extractors::accommodation_reservation_calendar_extractor::FamilyAndAccommodationReservationCalendarInPath; use crate::extractors::family_extractor::FamilyInPath; -use crate::models::AccommodationID; +use crate::models::{AccommodationID, AccommodationReservationCalendarID}; use crate::services::{accommodations_list_service, accommodations_reservations_calendars_service}; use actix_web::{web, HttpResponse}; @@ -51,10 +52,16 @@ pub async fn create(a: FamilyInPath, req: web::Json) -> Htt Ok(HttpResponse::Ok().json(calendar)) } -/// Get the list of calendars of the user +/// Get the list of calendars of a user pub async fn get_list(a: FamilyInPath) -> HttpResult { let users = accommodations_reservations_calendars_service::get_all_of_user(a.user_id(), a.family_id()) .await?; Ok(HttpResponse::Ok().json(users)) } + +/// Delete a calendar +pub async fn delete(resa: FamilyAndAccommodationReservationCalendarInPath) -> HttpResult { + accommodations_reservations_calendars_service::delete(resa.to_reservation()).await?; + Ok(HttpResponse::Ok().json("Calendar successfully deleted")) +} diff --git a/geneit_backend/src/extractors/accommodation_reservation_calendar_extractor.rs b/geneit_backend/src/extractors/accommodation_reservation_calendar_extractor.rs new file mode 100644 index 0000000..449be99 --- /dev/null +++ b/geneit_backend/src/extractors/accommodation_reservation_calendar_extractor.rs @@ -0,0 +1,93 @@ +use crate::extractors::family_extractor::FamilyInPath; +use crate::models::{ + AccommodationReservationCalendar, AccommodationReservationCalendarID, FamilyID, Membership, +}; +use crate::services::accommodations_reservations_calendars_service; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest}; +use serde::Deserialize; +use std::ops::Deref; + +#[derive(thiserror::Error, Debug)] +enum AccommodationCalendarExtractorErr { + #[error("Calendar {0:?} does not belong to user or family {1:?}!")] + CalendarNotOfUserOrFamily(AccommodationReservationCalendarID, FamilyID), +} + +#[derive(Debug)] +pub struct FamilyAndAccommodationReservationCalendarInPath( + Membership, + AccommodationReservationCalendar, +); + +impl FamilyAndAccommodationReservationCalendarInPath { + async fn load_calendar_from_path( + family: FamilyInPath, + calendar_id: AccommodationReservationCalendarID, + ) -> anyhow::Result { + let accommodation = + accommodations_reservations_calendars_service::get_by_id(calendar_id).await?; + if accommodation.family_id() != family.family_id() + || accommodation.user_id() != family.user_id() + { + return Err( + AccommodationCalendarExtractorErr::CalendarNotOfUserOrFamily( + accommodation.id(), + family.family_id(), + ) + .into(), + ); + } + + Ok(Self(family.into(), accommodation)) + } +} + +impl Deref for FamilyAndAccommodationReservationCalendarInPath { + type Target = AccommodationReservationCalendar; + + fn deref(&self) -> &Self::Target { + &self.1 + } +} + +impl FamilyAndAccommodationReservationCalendarInPath { + pub fn membership(&self) -> &Membership { + &self.0 + } + + pub fn to_reservation(self) -> AccommodationReservationCalendar { + self.1 + } +} + +#[derive(Deserialize)] +struct AccommodationIDInPath { + cal_id: AccommodationReservationCalendarID, +} + +impl FromRequest for FamilyAndAccommodationReservationCalendarInPath { + type Error = actix_web::Error; + type Future = futures_util::future::LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let req = req.clone(); + Box::pin(async move { + let family = FamilyInPath::extract(&req).await?; + + let accommodation_id = actix_web::web::Path::::from_request( + &req, + &mut Payload::None, + ) + .await? + .cal_id; + + Self::load_calendar_from_path(family, accommodation_id) + .await + .map_err(|e| { + log::error!("Failed to extract calendar ID from URL! {}", e); + actix_web::error::ErrorNotFound("Could not fetch calendar information!") + }) + }) + } +} diff --git a/geneit_backend/src/extractors/mod.rs b/geneit_backend/src/extractors/mod.rs index 868b912..05594ba 100644 --- a/geneit_backend/src/extractors/mod.rs +++ b/geneit_backend/src/extractors/mod.rs @@ -1,4 +1,5 @@ pub mod accommodation_extractor; +pub mod accommodation_reservation_calendar_extractor; pub mod accommodation_reservation_extractor; pub mod couple_extractor; pub mod family_extractor; diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 1abc649..940ce31 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -266,7 +266,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/reservations_calendars/list", web::get().to(accommodations_reservations_calendars_controller::get_list), ) - // TODO : delete + .route( + "/family/{id}/accommodations/reservations_calendars/{cal_id}", + web::delete().to(accommodations_reservations_calendars_controller::delete), + ) // TODO : anonymous URL access // Photos controller .route( diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 3896239..80e9ec5 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -544,6 +544,24 @@ pub struct AccommodationReservationCalendar { pub time_used: i64, } +impl AccommodationReservationCalendar { + pub fn id(&self) -> AccommodationReservationCalendarID { + AccommodationReservationCalendarID(self.id) + } + + pub fn accommodation_id(&self) -> Option { + self.accommodation_id.map(AccommodationID) + } + + pub fn family_id(&self) -> FamilyID { + FamilyID(self.family_id) + } + + pub fn user_id(&self) -> UserID { + UserID(self.user_id) + } +} + #[derive(Insertable)] #[diesel(table_name = accommodations_reservations_cals_urls)] pub struct NewAccommodationReservationCalendar { diff --git a/geneit_backend/src/services/accommodations_reservations_calendars_service.rs b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs index 20cc6e3..296292d 100644 --- a/geneit_backend/src/services/accommodations_reservations_calendars_service.rs +++ b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs @@ -1,8 +1,8 @@ use crate::connections::db_connection; use crate::constants; use crate::models::{ - AccommodationID, AccommodationReservationCalendar, FamilyID, - NewAccommodationReservationCalendar, UserID, + AccommodationID, AccommodationReservationCalendar, AccommodationReservationCalendarID, + FamilyID, NewAccommodationReservationCalendar, UserID, }; use crate::schema::accommodations_reservations_cals_urls; use crate::utils::string_utils::rand_str; @@ -49,3 +49,28 @@ pub async fn get_all_of_user( .get_results(conn) }) } + +/// Get a single calendar by its id +pub async fn get_by_id( + id: AccommodationReservationCalendarID, +) -> anyhow::Result { + db_connection::execute(|conn| { + accommodations_reservations_cals_urls::table + .filter(accommodations_reservations_cals_urls::dsl::id.eq(id.0)) + .get_result(conn) + }) +} + +/// Delete a calendar +pub async fn delete(r: AccommodationReservationCalendar) -> anyhow::Result<()> { + // Remove the reservation + db_connection::execute(|conn| { + diesel::delete( + accommodations_reservations_cals_urls::dsl::accommodations_reservations_cals_urls + .filter(accommodations_reservations_cals_urls::dsl::id.eq(r.id().0)), + ) + .execute(conn) + })?; + + Ok(()) +} -- 2.45.2 From 7626d91ece89839277af3ff460c1bfe64ef7c3f5 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 31 May 2024 22:35:22 +0200 Subject: [PATCH 24/65] Remove comment --- .../services/accommodations_reservations_calendars_service.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/geneit_backend/src/services/accommodations_reservations_calendars_service.rs b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs index 296292d..538bb3c 100644 --- a/geneit_backend/src/services/accommodations_reservations_calendars_service.rs +++ b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs @@ -63,7 +63,6 @@ pub async fn get_by_id( /// Delete a calendar pub async fn delete(r: AccommodationReservationCalendar) -> anyhow::Result<()> { - // Remove the reservation db_connection::execute(|conn| { diesel::delete( accommodations_reservations_cals_urls::dsl::accommodations_reservations_cals_urls -- 2.45.2 From 0f0b5978b6f5dc9b2ceddfa54738939e74a39493 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 31 May 2024 23:07:56 +0200 Subject: [PATCH 25/65] Fix unused warning --- .../accommodations_reservations_calendars_controller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs index e9b85bd..f5211ec 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs @@ -2,7 +2,7 @@ use crate::constants::StaticConstraints; use crate::controllers::HttpResult; use crate::extractors::accommodation_reservation_calendar_extractor::FamilyAndAccommodationReservationCalendarInPath; use crate::extractors::family_extractor::FamilyInPath; -use crate::models::{AccommodationID, AccommodationReservationCalendarID}; +use crate::models::AccommodationID; use crate::services::{accommodations_list_service, accommodations_reservations_calendars_service}; use actix_web::{web, HttpResponse}; -- 2.45.2 From df6a9e82926e42507d130b72ec87d3926333dc0c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 3 Jun 2024 22:47:39 +0200 Subject: [PATCH 26/65] Can generate calendars --- geneit_backend/Cargo.lock | 13 +++ geneit_backend/Cargo.toml | 2 + ...tions_reservations_calendars_controller.rs | 96 ++++++++++++++++++- geneit_backend/src/main.rs | 5 +- geneit_backend/src/models.rs | 14 +++ ...odations_reservations_calendars_service.rs | 9 ++ .../src/services/families_service.rs | 6 +- 7 files changed, 138 insertions(+), 7 deletions(-) diff --git a/geneit_backend/Cargo.lock b/geneit_backend/Cargo.lock index 96f8ca9..ede325a 100644 --- a/geneit_backend/Cargo.lock +++ b/geneit_backend/Cargo.lock @@ -727,8 +727,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.5", ] @@ -1417,12 +1419,14 @@ dependencies = [ "anyhow", "base64 0.22.1", "bcrypt", + "chrono", "clap", "diesel", "diesel_migrations", "env_logger", "futures-util", "httpdate", + "ical", "image", "lazy_static", "lettre", @@ -1776,6 +1780,15 @@ dependencies = [ "cc", ] +[[package]] +name = "ical" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6" +dependencies = [ + "thiserror", +] + [[package]] name = "ident_case" version = "1.0.1" diff --git a/geneit_backend/Cargo.toml b/geneit_backend/Cargo.toml index 13bafbf..7a4fc50 100644 --- a/geneit_backend/Cargo.toml +++ b/geneit_backend/Cargo.toml @@ -38,3 +38,5 @@ zip = "2.0.0" mime_guess = "2.0.4" tempfile = "3.10.1" base64 = "0.22.0" +ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] } +chrono = "0.4.38" \ No newline at end of file diff --git a/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs index f5211ec..9eeacc0 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_calendars_controller.rs @@ -1,10 +1,17 @@ +use ical::{generator::*, *}; + +use actix_web::{web, HttpResponse}; +use chrono::DateTime; + use crate::constants::StaticConstraints; use crate::controllers::HttpResult; use crate::extractors::accommodation_reservation_calendar_extractor::FamilyAndAccommodationReservationCalendarInPath; use crate::extractors::family_extractor::FamilyInPath; -use crate::models::AccommodationID; -use crate::services::{accommodations_list_service, accommodations_reservations_calendars_service}; -use actix_web::{web, HttpResponse}; +use crate::models::{AccommodationID, ReservationStatus}; +use crate::services::{ + accommodations_list_service, accommodations_reservations_calendars_service, + accommodations_reservations_service, families_service, +}; #[derive(serde::Deserialize)] pub struct CreateCalendarQuery { @@ -65,3 +72,86 @@ pub async fn delete(resa: FamilyAndAccommodationReservationCalendarInPath) -> Ht accommodations_reservations_calendars_service::delete(resa.to_reservation()).await?; Ok(HttpResponse::Ok().json("Calendar successfully deleted")) } + +fn fmt_date(time: i64) -> String { + let res = DateTime::from_timestamp(time, 0).expect("Failed to parse date"); + + /*format!( + "{:0>4}{:0>2}{:0>2}T{:0>2}{:0>2}", + res.year(), + res.month(), + res.day(), + res.minute(), + res.second() + )*/ + + res.format("%Y%m%dT%H%M%S").to_string() +} + +#[derive(serde::Deserialize)] +pub struct AnonymousAccessURL { + token: String, +} + +/// Get the content of the calendar +pub async fn anonymous_access(req: web::Path) -> HttpResult { + let calendar = + match accommodations_reservations_calendars_service::get_by_token(&req.token).await { + Ok(c) => c, + Err(e) => { + log::error!("Calendar information could not be retrieved: {e}"); + return Ok(HttpResponse::NotFound().body("Calendar not found!")); + } + }; + + let accommodations = + accommodations_list_service::get_all_of_family(calendar.family_id()).await?; + let members = families_service::get_memberships_of_family(calendar.family_id()).await?; + + // Get calendar associated events + let events = match calendar.accommodation_id() { + None => { + accommodations_reservations_service::get_all_of_family(calendar.family_id()).await? + } + Some(a) => accommodations_reservations_service::get_all_of_accommodation(a).await?, + }; + + let mut cal = IcalCalendarBuilder::version("2.0") + .gregorian() + .prodid("-//geneit//") + .build(); + + for ev in events { + let accommodation = accommodations + .iter() + .find(|a| a.id() == ev.accommodation_id()) + .unwrap(); + let member_name = members + .iter() + .find(|a| a.membership.user_id() == ev.user_id()) + .map(|m| m.user_name.as_str()) + .unwrap_or("other user"); + + let event = IcalEventBuilder::tzid("Europe/Paris") + .uid(format!("resa-{}", ev.id().0)) + .changed(fmt_date(ev.time_update)) + .start(fmt_date(ev.reservation_start)) + .end(fmt_date(ev.reservation_end)) + .set(ical_property!("SUMMARY", member_name)) + .set(ical_property!("LOCATION", &accommodation.name)) + .set(ical_property!( + "STATUS", + match ev.status() { + ReservationStatus::Pending => "TENTATIVE", + ReservationStatus::Accepted => "CONFIRMED", + ReservationStatus::Rejected => "CANCELLED", + } + )) + .build(); + cal.events.push(event); + } + + Ok(HttpResponse::Ok() + .content_type("text/calendar") + .body(cal.generate())) +} diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 940ce31..ff815fc 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -270,7 +270,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/accommodations/reservations_calendars/{cal_id}", web::delete().to(accommodations_reservations_calendars_controller::delete), ) - // TODO : anonymous URL access + .route( + "/acccommodations_calendar/{token}", + web::get().to(accommodations_reservations_calendars_controller::anonymous_access), + ) // Photos controller .route( "/photo/{id}", diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 80e9ec5..720a39d 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -485,6 +485,12 @@ pub struct NewAccommodation { #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct AccommodationReservationID(pub i32); +pub enum ReservationStatus { + Pending, + Accepted, + Rejected, +} + #[derive(Queryable, Debug, serde::Serialize)] pub struct AccommodationReservation { id: i32, @@ -514,6 +520,14 @@ impl AccommodationReservation { pub fn user_id(&self) -> UserID { UserID(self.user_id) } + + pub fn status(&self) -> ReservationStatus { + match self.validated { + None => ReservationStatus::Pending, + Some(true) => ReservationStatus::Accepted, + Some(false) => ReservationStatus::Rejected, + } + } } #[derive(Insertable)] diff --git a/geneit_backend/src/services/accommodations_reservations_calendars_service.rs b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs index 538bb3c..ff07470 100644 --- a/geneit_backend/src/services/accommodations_reservations_calendars_service.rs +++ b/geneit_backend/src/services/accommodations_reservations_calendars_service.rs @@ -61,6 +61,15 @@ pub async fn get_by_id( }) } +/// Get a single calendar by its token +pub async fn get_by_token(token: &str) -> anyhow::Result { + db_connection::execute(|conn| { + accommodations_reservations_cals_urls::table + .filter(accommodations_reservations_cals_urls::dsl::token.eq(token)) + .get_result(conn) + }) +} + /// Delete a calendar pub async fn delete(r: AccommodationReservationCalendar) -> anyhow::Result<()> { db_connection::execute(|conn| { diff --git a/geneit_backend/src/services/families_service.rs b/geneit_backend/src/services/families_service.rs index 6bfdccb..5cb47b5 100644 --- a/geneit_backend/src/services/families_service.rs +++ b/geneit_backend/src/services/families_service.rs @@ -129,9 +129,9 @@ pub async fn update_membership(membership: &Membership) -> anyhow::Result<()> { #[derive(serde::Serialize)] pub struct FamilyMember { #[serde(flatten)] - membership: Membership, - user_name: String, - user_mail: String, + pub membership: Membership, + pub user_name: String, + pub user_mail: String, } /// Get information about the users of a family -- 2.45.2 From f83cbe1386a4210088e4c9439b446d84e2f9e626 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 10 Jun 2024 21:20:26 +0200 Subject: [PATCH 27/65] Start to create accommodations UI --- geneit_app/src/App.tsx | 39 ++++++---- .../accommodations/AccommodationListApi.tsx | 67 +++++++++++++++++ .../AccommodationsSettingsRoute.tsx | 50 +++++++++++++ geneit_app/src/widgets/BaseFamilyRoute.tsx | 9 +++ .../BaseAccommodationsRoute.tsx | 72 +++++++++++++++++++ .../widgets/genealogy/BaseGenealogyRoute.tsx | 6 +- 6 files changed, 227 insertions(+), 16 deletions(-) create mode 100644 geneit_app/src/api/accommodations/AccommodationListApi.tsx create mode 100644 geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx create mode 100644 geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx diff --git a/geneit_app/src/App.tsx b/geneit_app/src/App.tsx index a07dcd1..0eaed2f 100644 --- a/geneit_app/src/App.tsx +++ b/geneit_app/src/App.tsx @@ -16,29 +16,31 @@ import { NewAccountRoute } from "./routes/auth/NewAccountRoute"; import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute"; import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute"; -import { FamilyHomeRoute } from "./routes/family/genealogy/FamilyHomeRoute"; -import { - FamilyCreateMemberRoute, - FamilyEditMemberRoute, - FamilyMemberRoute, -} from "./routes/family/genealogy/FamilyMemberRoute"; import { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute"; import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute"; -import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; -import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute"; -import { BaseLoginPage } from "./widgets/BaseLoginpage"; -import { FamilyMembersListRoute } from "./routes/family/genealogy/FamilyMembersListRoute"; +import { AccommodationsSettingsRoute } from "./routes/family/accommodations/AccommodationsSettingsRoute"; import { FamilyCoupleRoute, FamilyCreateCoupleRoute, FamilyEditCoupleRoute, } from "./routes/family/genealogy/FamilyCoupleRoute"; import { FamilyCouplesListRoute } from "./routes/family/genealogy/FamilyCouplesListRoute"; -import { FamilyTreeRoute } from "./routes/family/genealogy/FamilyTreeRoute"; +import { FamilyHomeRoute } from "./routes/family/genealogy/FamilyHomeRoute"; +import { + FamilyCreateMemberRoute, + FamilyEditMemberRoute, + FamilyMemberRoute, +} from "./routes/family/genealogy/FamilyMemberRoute"; import { FamilyMemberTreeRoute } from "./routes/family/genealogy/FamilyMemberTreeRoute"; -import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute"; -import { BaseGenealogyRoute } from "./widgets/genealogy/BaseGenealogyRoute"; +import { FamilyMembersListRoute } from "./routes/family/genealogy/FamilyMembersListRoute"; +import { FamilyTreeRoute } from "./routes/family/genealogy/FamilyTreeRoute"; import { GenalogySettingsRoute } from "./routes/family/genealogy/GenalogySettingsRoute"; +import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute"; +import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; +import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute"; +import { BaseLoginPage } from "./widgets/BaseLoginpage"; +import { BaseAccommodationsRoute } from "./widgets/accommodations/BaseAccommodationsRoute"; +import { BaseGenealogyRoute } from "./widgets/genealogy/BaseGenealogyRoute"; interface AuthContext { signedIn: boolean; @@ -110,6 +112,17 @@ export function App(): React.ReactElement { } /> + } + > + } + /> + } /> + + } /> } /> } /> diff --git a/geneit_app/src/api/accommodations/AccommodationListApi.tsx b/geneit_app/src/api/accommodations/AccommodationListApi.tsx new file mode 100644 index 0000000..b343a09 --- /dev/null +++ b/geneit_app/src/api/accommodations/AccommodationListApi.tsx @@ -0,0 +1,67 @@ +import { APIClient } from "../ApiClient"; +import { Family } from "../FamilyApi"; + +export interface Accommodation { + id: number; + family_id: number; + time_create: number; + time_update: number; + name: string; + need_validation: boolean; + description: string; + open_to_reservations: boolean; +} + +export class AccommodationsList { + private list: Accommodation[]; + private map: Map; + + constructor(list: Accommodation[]) { + this.list = list; + this.map = new Map(); + + for (const m of list) { + this.map.set(m.id, m); + } + + this.list.sort((a, b) => + a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase()) + ); + } + + public get isEmpty(): boolean { + return this.list.length === 0; + } + + public get size(): number { + return this.list.length; + } + + public get fullList(): Accommodation[] { + return this.list; + } + + filter(predicate: (m: Accommodation) => boolean): Accommodation[] { + return this.list.filter(predicate); + } + + get(id: number): Accommodation | undefined { + return this.map.get(id); + } +} + +export class AccommodationListApi { + /** + * Get the list of accommodation of a family + */ + static async GetListOfFamily(family: Family): Promise { + const data = ( + await APIClient.exec({ + method: "GET", + uri: `/family/${family.family_id}/accommodations/list/list`, + }) + ).data; + + return new AccommodationsList(data); + } +} diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx new file mode 100644 index 0000000..76a7725 --- /dev/null +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -0,0 +1,50 @@ +import { CardContent, Typography, Alert, Button } from "@mui/material"; +import React from "react"; +import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; +import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; +import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { FamilyCard } from "../../../widgets/FamilyCard"; +import AddIcon from "@mui/icons-material/Add"; + +export function AccommodationsSettingsRoute(): React.ReactElement { + return ( + <> + + + ); +} + +function AccommodationsListCard(): React.ReactElement { + const loading = useLoadingMessage(); + const confirm = useConfirm(); + const alert = useAlert(); + + const family = useFamily(); + + const [error, setError] = React.useState(); + const [success, setSuccess] = React.useState(); + + const createAccommodation = () => {}; + + return ( + + + + Logements + + + + + ); +} diff --git a/geneit_app/src/widgets/BaseFamilyRoute.tsx b/geneit_app/src/widgets/BaseFamilyRoute.tsx index 38e6830..5206921 100644 --- a/geneit_app/src/widgets/BaseFamilyRoute.tsx +++ b/geneit_app/src/widgets/BaseFamilyRoute.tsx @@ -5,6 +5,7 @@ import { mdiCrowd, mdiFamilyTree, mdiFileTree, + mdiHomeGroup, mdiHumanMaleFemale, mdiLockCheck, mdiPlus, @@ -207,6 +208,14 @@ export function BaseFamilyRoute(): React.ReactElement { /> )} + {family?.enable_accommodations && ( + } + label="Logements" + uri="accommodations/settings" + /> + )} + {/* Invitation code */} Promise; +} + +const AccommodationsContextK = + React.createContext(null); + +export function BaseAccommodationsRoute(): React.ReactElement { + const family = useFamily(); + + const [accommodations, setAccommodations] = + React.useState(null); + + const loadKey = React.useRef(1); + + const loadPromise = React.useRef<() => void>(); + + const load = async () => { + setAccommodations( + await AccommodationListApi.GetListOfFamily(family.family) + ); + }; + + const onReload = async () => { + loadKey.current += 1; + setAccommodations(null); + + return new Promise((res, _rej) => { + loadPromise.current = () => res(); + }); + }; + + return ( + { + if (loadPromise.current != null) { + loadPromise.current?.(); + loadPromise.current = undefined; + } + + return ( + + + + ); + }} + /> + ); +} + +export function useAccommodations(): AccommodationsContext { + return React.useContext(AccommodationsContextK)!; +} diff --git a/geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx b/geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx index 247663c..0f0e5d9 100644 --- a/geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx +++ b/geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx @@ -5,14 +5,14 @@ import { MemberApi, MembersList } from "../../api/genealogy/MemberApi"; import { AsyncWidget } from "../AsyncWidget"; import { useFamily } from "../BaseFamilyRoute"; -interface FamilyContext { +interface GenealogyContext { members: MembersList; couples: CouplesList; reloadMembersList: () => Promise; reloadCouplesList: () => Promise; } -const GenealogyContextK = React.createContext(null); +const GenealogyContextK = React.createContext(null); export function BaseGenealogyRoute(): React.ReactElement { const family = useFamily(); @@ -68,6 +68,6 @@ export function BaseGenealogyRoute(): React.ReactElement { ); } -export function useGenealogy(): FamilyContext { +export function useGenealogy(): GenealogyContext { return React.useContext(GenealogyContextK)!; } -- 2.45.2 From 7d64ea219ff5cf159e936cf7f25b81afb370da42 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 10 Jun 2024 22:00:30 +0200 Subject: [PATCH 28/65] Can create accommodation from WebUI --- .../accommodations/AccommodationListApi.tsx | 23 +++ .../UpdateAccommodationDialog.tsx | 140 ++++++++++++++++++ .../UpdateAccommodationDialogProvider.tsx | 64 ++++++++ .../AccommodationsSettingsRoute.tsx | 36 ++++- geneit_app/src/utils/from_utils.ts | 21 +++ .../BaseAccommodationsRoute.tsx | 5 +- geneit_app/src/widgets/forms/PropCheckbox.tsx | 10 +- geneit_app/src/widgets/forms/PropEdit.tsx | 2 + 8 files changed, 296 insertions(+), 5 deletions(-) create mode 100644 geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx create mode 100644 geneit_app/src/hooks/context_providers/accommodations/UpdateAccommodationDialogProvider.tsx create mode 100644 geneit_app/src/utils/from_utils.ts diff --git a/geneit_app/src/api/accommodations/AccommodationListApi.tsx b/geneit_app/src/api/accommodations/AccommodationListApi.tsx index b343a09..f51d2e1 100644 --- a/geneit_app/src/api/accommodations/AccommodationListApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationListApi.tsx @@ -50,6 +50,13 @@ export class AccommodationsList { } } +export interface UpdateAccommodation { + name: string; + need_validation: boolean; + description?: string; + open_to_reservations: boolean; +} + export class AccommodationListApi { /** * Get the list of accommodation of a family @@ -64,4 +71,20 @@ export class AccommodationListApi { return new AccommodationsList(data); } + + /** + * Create a new accommodation + */ + static async Create( + family: Family, + accommodation: UpdateAccommodation + ): Promise { + return ( + await APIClient.exec({ + method: "POST", + uri: `/family/${family.family_id}/accommodations/list/create`, + jsonData: accommodation, + }) + ).data; + } } diff --git a/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx new file mode 100644 index 0000000..a780f3c --- /dev/null +++ b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx @@ -0,0 +1,140 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Tooltip, +} from "@mui/material"; +import React from "react"; +import { ServerApi } from "../../api/ServerApi"; +import { UpdateAccommodation } from "../../api/accommodations/AccommodationListApi"; +import { checkConstraint } from "../../utils/from_utils"; +import { PropCheckbox } from "../../widgets/forms/PropCheckbox"; +import { PropEdit } from "../../widgets/forms/PropEdit"; + +export function UpdateAccommodationDialog(p: { + open: boolean; + create: boolean; + onClose: () => void; + onSubmitted: (c: UpdateAccommodation) => void; + accommodation: UpdateAccommodation | undefined; +}): React.ReactElement { + const [accommodation, setAccommodation] = React.useState< + UpdateAccommodation | undefined + >(); + + const nameErr = checkConstraint( + ServerApi.Config.constraints.accommodation_name_len, + accommodation?.name + ); + const descriptionErr = checkConstraint( + ServerApi.Config.constraints.accommodation_description_len, + accommodation?.description + ); + + const clearForm = () => { + setAccommodation(undefined); + }; + + const cancel = () => { + clearForm(); + p.onClose(); + }; + + const submit = async () => { + clearForm(); + p.onSubmitted(accommodation!); + }; + + React.useEffect(() => { + if (!accommodation) setAccommodation(p.accommodation); + }, [p.open, p.accommodation]); + + return ( + + + {p.create ? "Création" : "Mise à jour"} d'un logement + + + + setAccommodation((a) => { + return { + ...a!, + name: s!, + }; + }) + } + size={ServerApi.Config.constraints.accommodation_name_len} + helperText={nameErr} + /> + + + setAccommodation((a) => { + return { + ...a!, + description: s!, + }; + }) + } + size={ServerApi.Config.constraints.accommodation_description_len} + helperText={descriptionErr} + /> + + + setAccommodation((a) => { + return { + ...a!, + open_to_reservations: c, + }; + }) + } + /> + + + + setAccommodation((a) => { + return { + ...a!, + need_validation: c, + }; + }) + } + /> + + + + + + + + ); +} diff --git a/geneit_app/src/hooks/context_providers/accommodations/UpdateAccommodationDialogProvider.tsx b/geneit_app/src/hooks/context_providers/accommodations/UpdateAccommodationDialogProvider.tsx new file mode 100644 index 0000000..63a6b77 --- /dev/null +++ b/geneit_app/src/hooks/context_providers/accommodations/UpdateAccommodationDialogProvider.tsx @@ -0,0 +1,64 @@ +import React, { PropsWithChildren } from "react"; +import { UpdateAccommodation } from "../../../api/accommodations/AccommodationListApi"; +import { UpdateAccommodationDialog } from "../../../dialogs/accommodations/UpdateAccommodationDialog"; + +type DialogContext = ( + accommodation: UpdateAccommodation, + create: boolean +) => Promise; + +const DialogContextK = React.createContext(null); + +export function UpdateAccommodationDialogProvider( + p: PropsWithChildren +): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [accommodation, setAccommodation] = React.useState< + UpdateAccommodation | undefined + >(undefined); + const [create, setCreate] = React.useState(false); + + const cb = React.useRef< + null | ((a: UpdateAccommodation | undefined) => void) + >(null); + + const handleClose = (res?: UpdateAccommodation) => { + setOpen(false); + + if (cb.current !== null) cb.current(res); + cb.current = null; + }; + + const hook: DialogContext = (accommodation, create) => { + setAccommodation(accommodation); + setCreate(create); + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + return ( + <> + + {p.children} + + + {open && ( + + )} + + ); +} + +export function useUpdateAccommodation(): DialogContext { + return React.useContext(DialogContextK)!; +} diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index 76a7725..c26267f 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -6,6 +6,10 @@ import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessa import { useFamily } from "../../../widgets/BaseFamilyRoute"; import { FamilyCard } from "../../../widgets/FamilyCard"; import AddIcon from "@mui/icons-material/Add"; +import { useUpdateAccommodation } from "../../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; +import { AccommodationListApi } from "../../../api/accommodations/AccommodationListApi"; +import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider"; +import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; export function AccommodationsSettingsRoute(): React.ReactElement { return ( @@ -19,13 +23,43 @@ function AccommodationsListCard(): React.ReactElement { const loading = useLoadingMessage(); const confirm = useConfirm(); const alert = useAlert(); + const snackbar = useSnackbar(); const family = useFamily(); + const accommodations = useAccommodations(); const [error, setError] = React.useState(); const [success, setSuccess] = React.useState(); - const createAccommodation = () => {}; + const updateAccommodation = useUpdateAccommodation(); + + const createAccommodation = async () => { + try { + const accommodation = await updateAccommodation( + { + name: "", + open_to_reservations: true, + need_validation: false, + }, + true + ); + + if (!accommodation) return; + + loading.show("Création du logement en cours..."); + + await AccommodationListApi.Create(family.family, accommodation); + + snackbar("Le logement a été créé avec succès !"); + + await accommodations.reloadAccommodationsList(); + } catch (e) { + console.error("Failed to create accommodation!", e); + alert(`Echec de la création du logement! ${e}`); + } finally { + loading.hide(); + } + }; return ( diff --git a/geneit_app/src/utils/from_utils.ts b/geneit_app/src/utils/from_utils.ts new file mode 100644 index 0000000..1be31df --- /dev/null +++ b/geneit_app/src/utils/from_utils.ts @@ -0,0 +1,21 @@ +import { LenConstraint } from "../api/ServerApi"; + +/** + * Check if a constraint was respected or not + * + * @returns An error message appropriate for the constraint + * violation, if any, or undefined otherwise + */ +export function checkConstraint( + constraint: LenConstraint, + value: string | undefined +): string | undefined { + value = value ?? ""; + if (value.length < constraint.min) + return `Veuillez indiquer au moins ${constraint.min} caractères !`; + + if (value.length > constraint.max) + return `Veuillez indiquer au maximum ${constraint.min} caractères !`; + + return undefined; +} diff --git a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx index 68f0d8e..c543ac9 100644 --- a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx +++ b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx @@ -6,6 +6,7 @@ import { } from "../../api/accommodations/AccommodationListApi"; import { AsyncWidget } from "../AsyncWidget"; import { useFamily } from "../BaseFamilyRoute"; +import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; interface AccommodationsContext { accommodations: AccommodationsList; @@ -59,7 +60,9 @@ export function BaseAccommodationsRoute(): React.ReactElement { reloadAccommodationsList: onReload, }} > - + + + ); }} diff --git a/geneit_app/src/widgets/forms/PropCheckbox.tsx b/geneit_app/src/widgets/forms/PropCheckbox.tsx index 1316622..8c26c29 100644 --- a/geneit_app/src/widgets/forms/PropCheckbox.tsx +++ b/geneit_app/src/widgets/forms/PropCheckbox.tsx @@ -5,16 +5,20 @@ export function PropCheckbox(p: { label: string; checked: boolean | undefined; onValueChange: (v: boolean) => void; + checkboxAlwaysVisible?: boolean; }): React.ReactElement { - if (!p.editable && p.checked) - return {p.label}; + if (!p.checkboxAlwaysVisible) { + if (!p.editable && p.checked) + return {p.label}; - if (!p.editable) return <>; + if (!p.editable) return <>; + } return ( p.onValueChange(e.target.checked)} /> diff --git a/geneit_app/src/widgets/forms/PropEdit.tsx b/geneit_app/src/widgets/forms/PropEdit.tsx index 217d359..f365bf2 100644 --- a/geneit_app/src/widgets/forms/PropEdit.tsx +++ b/geneit_app/src/widgets/forms/PropEdit.tsx @@ -14,6 +14,7 @@ export function PropEdit(p: { multiline?: boolean; minRows?: number; maxRows?: number; + helperText?: string; }): React.ReactElement { if (((!p.editable && p.value) ?? "") === "") return <>; @@ -44,6 +45,7 @@ export function PropEdit(p: { !p.checkValue(p.value)) || false } + helperText={p.helperText} /> ); } -- 2.45.2 From 3a218cd3fb0db86ac8d5ecd25ec3bab8de9c4e06 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 10 Jun 2024 22:03:18 +0200 Subject: [PATCH 29/65] Minor fixes --- .../src/dialogs/accommodations/UpdateAccommodationDialog.tsx | 2 +- .../family/accommodations/AccommodationsSettingsRoute.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx index a780f3c..3ffaf1a 100644 --- a/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx @@ -111,7 +111,7 @@ export function UpdateAccommodationDialog(p: { setAccommodation((a) => { diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index c26267f..43613fc 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -55,7 +55,7 @@ function AccommodationsListCard(): React.ReactElement { await accommodations.reloadAccommodationsList(); } catch (e) { console.error("Failed to create accommodation!", e); - alert(`Echec de la création du logement! ${e}`); + alert(`Échec de la création du logement! ${e}`); } finally { loading.hide(); } -- 2.45.2 From 91d0b1e0be9fb4fd985991de9f444281f2459a83 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 13 Jun 2024 20:44:49 +0200 Subject: [PATCH 30/65] Can update / delete accommodations --- .../accommodations/AccommodationListApi.tsx | 28 +++ .../AccommodationsSettingsRoute.tsx | 161 ++++++++++++++++-- 2 files changed, 174 insertions(+), 15 deletions(-) diff --git a/geneit_app/src/api/accommodations/AccommodationListApi.tsx b/geneit_app/src/api/accommodations/AccommodationListApi.tsx index f51d2e1..9125d3f 100644 --- a/geneit_app/src/api/accommodations/AccommodationListApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationListApi.tsx @@ -87,4 +87,32 @@ export class AccommodationListApi { }) ).data; } + + /** + * Update an accommodation + */ + static async Update( + accommodation: Accommodation, + update: UpdateAccommodation + ): Promise { + return ( + await APIClient.exec({ + method: "PUT", + uri: `/family/${accommodation.family_id}/accommodations/list/${accommodation.id}`, + jsonData: update, + }) + ).data; + } + + /** + * Delete an accommodation + */ + static async Delete(accommodation: Accommodation): Promise { + return ( + await APIClient.exec({ + method: "DELETE", + uri: `/family/${accommodation.family_id}/accommodations/list/${accommodation.id}`, + }) + ).data; + } } diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index 43613fc..24bd50b 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -1,4 +1,11 @@ -import { CardContent, Typography, Alert, Button } from "@mui/material"; +import { + CardContent, + Typography, + Alert, + Button, + Card, + CardActions, +} from "@mui/material"; import React from "react"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; @@ -7,9 +14,15 @@ import { useFamily } from "../../../widgets/BaseFamilyRoute"; import { FamilyCard } from "../../../widgets/FamilyCard"; import AddIcon from "@mui/icons-material/Add"; import { useUpdateAccommodation } from "../../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; -import { AccommodationListApi } from "../../../api/accommodations/AccommodationListApi"; +import { + Accommodation, + AccommodationListApi, +} from "../../../api/accommodations/AccommodationListApi"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; +import { TimeWidget } from "../../../widgets/TimeWidget"; +import CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; export function AccommodationsSettingsRoute(): React.ReactElement { return ( @@ -34,6 +47,8 @@ function AccommodationsListCard(): React.ReactElement { const updateAccommodation = useUpdateAccommodation(); const createAccommodation = async () => { + setError(undefined); + setSuccess(undefined); try { const accommodation = await updateAccommodation( { @@ -51,11 +66,55 @@ function AccommodationsListCard(): React.ReactElement { await AccommodationListApi.Create(family.family, accommodation); snackbar("Le logement a été créé avec succès !"); - await accommodations.reloadAccommodationsList(); } catch (e) { console.error("Failed to create accommodation!", e); - alert(`Échec de la création du logement! ${e}`); + setError(`Échec de la création du logement! ${e}`); + } finally { + loading.hide(); + } + }; + + const requestUpdateAccommodation = async (a: Accommodation) => { + setError(undefined); + setSuccess(undefined); + try { + const update = await updateAccommodation(a, false); + if (!update) return; + + loading.show("Mise à jour du logement en cours..."); + + await AccommodationListApi.Update(a, update); + + snackbar("Le logement a été créé avec succès !"); + await accommodations.reloadAccommodationsList(); + } catch (e) { + console.error("Failed to update accommodation!", e); + setError(`Échec de la mise à jour du logement! ${e}`); + } finally { + loading.hide(); + } + }; + + const deleteAccommodation = async (a: Accommodation) => { + setError(undefined); + setSuccess(undefined); + try { + if ( + !(await confirm( + `Voulez-vous vraiment supprimer le logement '${a.name}' ? Cette opération est définitive !` + )) + ) + return; + loading.show("Suppression du logement en cours..."); + + await AccommodationListApi.Delete(a); + + snackbar("Le logement a été supprimé avec succès !"); + await accommodations.reloadAccommodationsList(); + } catch (e) { + console.error("Failed to delete accommodation!", e); + setError(`Échec de la suppression du logement! ${e}`); } finally { loading.hide(); } @@ -67,18 +126,90 @@ function AccommodationsListCard(): React.ReactElement { Logements - + + {/* Display the list of accommodations */} + {accommodations.accommodations.isEmpty && ( +
+ Aucun logement enregistré pour le moment ! +
+ )} + {accommodations.accommodations.fullList.map((a) => ( + + ))} + + {family.family.is_admin && ( + + )}
); } + +function AccommodationCard(p: { + accommodation: Accommodation; + onRequestUpdate: (a: Accommodation) => void; + onRequestDelete: (a: Accommodation) => void; +}): React.ReactElement { + const family = useFamily(); + return ( + + + + Mis à jour il y a + + + {p.accommodation.name} + + + {p.accommodation.description} + + + Ouvert aux + réservations +
+ Réservation + sans validation d'un administrateur +
+
+ {family.family.is_admin && ( + + + + + + )} +
+ ); +} + +function BoolIcon(p: { checked?: boolean }): React.ReactElement { + return p.checked ? ( + + ) : ( + + ); +} -- 2.45.2 From 7525e78009851078c5bcacbe6b8b6d30bbae6bf3 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 13 Jun 2024 21:44:36 +0200 Subject: [PATCH 31/65] Can create accommodation calendars URL from UI --- geneit_app/src/api/ServerApi.ts | 1 + .../AccommodationsCalendarURLApi.tsx | 36 +++++++ .../CreateAccommodationCalendarURLDialog.tsx | 92 ++++++++++++++++++ ...AccommodationCalendarURLDialogProvider.tsx | 52 ++++++++++ .../AccommodationsSettingsRoute.tsx | 96 +++++++++++++++---- geneit_app/src/widgets/FamilyCard.tsx | 8 +- .../BaseAccommodationsRoute.tsx | 7 +- geneit_app/src/widgets/forms/PropSelect.tsx | 3 +- 8 files changed, 274 insertions(+), 21 deletions(-) create mode 100644 geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx create mode 100644 geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx create mode 100644 geneit_app/src/hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider.tsx diff --git a/geneit_app/src/api/ServerApi.ts b/geneit_app/src/api/ServerApi.ts index 0ac625f..d017de6 100644 --- a/geneit_app/src/api/ServerApi.ts +++ b/geneit_app/src/api/ServerApi.ts @@ -34,6 +34,7 @@ interface Constraints { member_note: LenConstraint; accommodation_name_len: LenConstraint; accommodation_description_len: LenConstraint; + accommodation_calendar_name_len: LenConstraint; } interface OIDCProvider { diff --git a/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx b/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx new file mode 100644 index 0000000..7a1d27d --- /dev/null +++ b/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx @@ -0,0 +1,36 @@ +import { APIClient } from "../ApiClient"; +import { Family } from "../FamilyApi"; + +export interface NewCalendarURL { + accommodation_id?: number; + name: string; +} + +export interface AccommodationCalendarURL { + id: number; + family_id: number; + accommodation_id: number; + user_id: number; + name: string; + token: string; + time_create: number; + time_used: number; +} + +export class AccommodationsCalendarURLApi { + /** + * Create a new accommodation calendar URL + */ + static async Create( + family: Family, + calendar: NewCalendarURL + ): Promise { + return ( + await APIClient.exec({ + method: "POST", + uri: `/family/${family.family_id}/accommodations/reservations_calendars/create`, + jsonData: calendar, + }) + ).data; + } +} diff --git a/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx b/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx new file mode 100644 index 0000000..f1fe0b8 --- /dev/null +++ b/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx @@ -0,0 +1,92 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from "@mui/material"; +import React from "react"; +import { ServerApi } from "../../api/ServerApi"; +import { NewCalendarURL } from "../../api/accommodations/AccommodationsCalendarURLApi"; +import { checkConstraint } from "../../utils/from_utils"; +import { PropEdit } from "../../widgets/forms/PropEdit"; +import { PropSelect } from "../../widgets/forms/PropSelect"; +import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute"; + +export function CreateAccommodationCalendarURLDialog(p: { + open: boolean; + onClose: () => void; + onSubmitted: (c: NewCalendarURL) => void; +}): React.ReactElement { + const [calendar, setCalendar] = React.useState({ name: "" }); + + const accommodations = useAccommodations(); + + const nameErr = checkConstraint( + ServerApi.Config.constraints.accommodation_calendar_name_len, + calendar?.name + ); + + const clearForm = () => { + setCalendar({ name: "" }); + }; + + const cancel = () => { + clearForm(); + p.onClose(); + }; + + const submit = async () => { + clearForm(); + p.onSubmitted(calendar!); + }; + + return ( + + Création d'un calendrier + + + setCalendar((a) => { + return { + ...a!, + name: s!, + }; + }) + } + size={ServerApi.Config.constraints.accommodation_calendar_name_len} + helperText={nameErr} + /> + + { + setCalendar((a) => { + return { + ...a!, + accommodation_id: v !== "A" && v ? Number(v) : undefined, + }; + }); + }} + options={[ + { label: "Tous les logements", value: "A" }, + ...accommodations.accommodations.fullList.map((a) => { + return { label: a.name, value: a.id.toString() }; + }), + ]} + value={calendar.accommodation_id?.toString() ?? "A"} + /> + + + + + + + ); +} diff --git a/geneit_app/src/hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider.tsx b/geneit_app/src/hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider.tsx new file mode 100644 index 0000000..376329d --- /dev/null +++ b/geneit_app/src/hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider.tsx @@ -0,0 +1,52 @@ +import React, { PropsWithChildren } from "react"; +import { NewCalendarURL } from "../../../api/accommodations/AccommodationsCalendarURLApi"; +import { CreateAccommodationCalendarURLDialog } from "../../../dialogs/accommodations/CreateAccommodationCalendarURLDialog"; + +type DialogContext = () => Promise; + +const DialogContextK = React.createContext(null); + +export function CreateAccommodationCalendarURLDialogProvider( + p: PropsWithChildren +): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const cb = React.useRef void)>( + null + ); + + const handleClose = (res?: NewCalendarURL) => { + setOpen(false); + + if (cb.current !== null) cb.current(res); + cb.current = null; + }; + + const hook: DialogContext = () => { + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + return ( + <> + + {p.children} + + + {open && ( + + )} + + ); +} + +export function useCreateAccommodationCalendarURL(): DialogContext { + return React.useContext(DialogContextK)!; +} diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index 24bd50b..dfb4495 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -1,33 +1,35 @@ +import AddIcon from "@mui/icons-material/Add"; +import CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; import { - CardContent, - Typography, - Alert, Button, Card, CardActions, + CardContent, + Typography, } from "@mui/material"; import React from "react"; -import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; -import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; -import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider"; -import { useFamily } from "../../../widgets/BaseFamilyRoute"; -import { FamilyCard } from "../../../widgets/FamilyCard"; -import AddIcon from "@mui/icons-material/Add"; -import { useUpdateAccommodation } from "../../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; import { Accommodation, AccommodationListApi, } from "../../../api/accommodations/AccommodationListApi"; +import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; +import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; +import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider"; -import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; +import { useUpdateAccommodation } from "../../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { FamilyCard } from "../../../widgets/FamilyCard"; import { TimeWidget } from "../../../widgets/TimeWidget"; -import CheckIcon from "@mui/icons-material/Check"; -import CloseIcon from "@mui/icons-material/Close"; +import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; +import { useCreateAccommodationCalendarURL } from "../../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider"; +import { AccommodationsCalendarURLApi } from "../../../api/accommodations/AccommodationsCalendarURLApi"; export function AccommodationsSettingsRoute(): React.ReactElement { return ( <> + ); } @@ -35,7 +37,6 @@ export function AccommodationsSettingsRoute(): React.ReactElement { function AccommodationsListCard(): React.ReactElement { const loading = useLoadingMessage(); const confirm = useConfirm(); - const alert = useAlert(); const snackbar = useSnackbar(); const family = useFamily(); @@ -121,7 +122,7 @@ function AccommodationsListCard(): React.ReactElement { }; return ( - + Logements @@ -150,7 +151,7 @@ function AccommodationsListCard(): React.ReactElement { onClick={createAccommodation} size={"large"} > - Ajouter un nouveau logement + Ajouter un logement )} @@ -213,3 +214,66 @@ function BoolIcon(p: { checked?: boolean }): React.ReactElement { ); } + +function AccommodationsCalURLsCard(): React.ReactElement { + const loading = useLoadingMessage(); + + const [error, setError] = React.useState(); + const [success, setSuccess] = React.useState(); + + const family = useFamily(); + + const createCalendarURLDialog = useCreateAccommodationCalendarURL(); + + const createCalendarURL = async () => { + try { + const newCal = await createCalendarURLDialog(); + + if (!newCal) return; + + loading.show("Création du logement en cours..."); + + const cal = await AccommodationsCalendarURLApi.Create( + family.family, + newCal + ); + + setSuccess("Le calendrier a été créé avec succès !"); + + // TODO : reload URLS list + // TODO : show QrCode dialog + console.log(cal); + } catch (e) { + console.error("Failed to create new accommodation calendar URL!", e); + setError(`Échec de la création du calendrier! ${e}`); + } finally { + loading.hide(); + } + }; + + return ( + + + + URL de calendriers + + + Vous pouvez, si vous le souhaitez, importer dans votre application de + calendrier le planning de réservation des logement. Pour ce faire, il + vous suffit de créer une URL de calendrier. + + + + + + ); +} diff --git a/geneit_app/src/widgets/FamilyCard.tsx b/geneit_app/src/widgets/FamilyCard.tsx index c33f300..eb2bb7d 100644 --- a/geneit_app/src/widgets/FamilyCard.tsx +++ b/geneit_app/src/widgets/FamilyCard.tsx @@ -2,10 +2,14 @@ import { Alert, Card } from "@mui/material"; import { PropsWithChildren } from "react"; export function FamilyCard( - p: PropsWithChildren<{ error?: string; success?: string }> + p: PropsWithChildren<{ + error?: string; + success?: string; + style?: React.CSSProperties | undefined; + }> ): React.ReactElement { return ( - + {p.error && {p.error}} {p.success && {p.success}} diff --git a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx index c543ac9..9516652 100644 --- a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx +++ b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx @@ -4,9 +4,10 @@ import { AccommodationListApi, AccommodationsList, } from "../../api/accommodations/AccommodationListApi"; +import { CreateAccommodationCalendarURLDialogProvider } from "../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider"; +import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; import { AsyncWidget } from "../AsyncWidget"; import { useFamily } from "../BaseFamilyRoute"; -import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; interface AccommodationsContext { accommodations: AccommodationsList; @@ -61,7 +62,9 @@ export function BaseAccommodationsRoute(): React.ReactElement { }} > - + + + ); diff --git a/geneit_app/src/widgets/forms/PropSelect.tsx b/geneit_app/src/widgets/forms/PropSelect.tsx index e329784..b09a94c 100644 --- a/geneit_app/src/widgets/forms/PropSelect.tsx +++ b/geneit_app/src/widgets/forms/PropSelect.tsx @@ -2,7 +2,7 @@ import { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; import { PropEdit } from "./PropEdit"; export interface SelectOption { - value: string; + value: string | undefined; label: string; } @@ -19,6 +19,7 @@ export function PropSelect(p: { const value = p.options.find((o) => o.value === p.value)?.label; return ; } + return ( {p.label} -- 2.45.2 From 572117745a655299b177aed204f9c35a9121f512 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 14 Jun 2024 19:47:00 +0200 Subject: [PATCH 32/65] Add install calendar dialog --- geneit_app/package-lock.json | 24 ++++++ geneit_app/package.json | 1 + .../AccommodationsCalendarURLApi.tsx | 7 ++ .../accommodations/InstallCalendarDialog.tsx | 76 +++++++++++++++++++ .../InstallCalendarDialogProvider.tsx | 44 +++++++++++ .../AccommodationsSettingsRoute.tsx | 7 +- geneit_app/src/widgets/CopyToClipboard.tsx | 30 ++++++++ .../BaseAccommodationsRoute.tsx | 5 +- 8 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx create mode 100644 geneit_app/src/hooks/context_providers/accommodations/InstallCalendarDialogProvider.tsx create mode 100644 geneit_app/src/widgets/CopyToClipboard.tsx diff --git a/geneit_app/package-lock.json b/geneit_app/package-lock.json index ef5ee26..e179448 100644 --- a/geneit_app/package-lock.json +++ b/geneit_app/package-lock.json @@ -33,6 +33,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-easy-crop": "^5.0.7", + "react-qr-code": "^2.0.14", "react-router-dom": "^6.23.1", "react-zoom-pan-pinch": "^3.4.4", "svg2pdf.js": "^2.2.3", @@ -3483,6 +3484,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -3533,6 +3539,24 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/react-qr-code": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.14.tgz", + "integrity": "sha512-xvAUqmXzFzf7X6aQAAKb6T02YYk9grBBFeqpp1MiVhUAKG3Rg9+hFiOKRYg4+rWc2MiXNxkri0ulAJgS12xh7Q==", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native-svg": "*" + }, + "peerDependenciesMeta": { + "react-native-svg": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/geneit_app/package.json b/geneit_app/package.json index 4259423..2730de4 100644 --- a/geneit_app/package.json +++ b/geneit_app/package.json @@ -29,6 +29,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-easy-crop": "^5.0.7", + "react-qr-code": "^2.0.14", "react-router-dom": "^6.23.1", "react-zoom-pan-pinch": "^3.4.4", "svg2pdf.js": "^2.2.3", diff --git a/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx b/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx index 7a1d27d..b898904 100644 --- a/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx @@ -33,4 +33,11 @@ export class AccommodationsCalendarURLApi { }) ).data; } + + /** + * Get accommodation calendar URL route + */ + static CalendarURL(c: AccommodationCalendarURL): string { + return `${APIClient.backendURL()}/acccommodations_calendar/${c.token}`; + } } diff --git a/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx b/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx new file mode 100644 index 0000000..bddd61d --- /dev/null +++ b/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx @@ -0,0 +1,76 @@ +import { + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, + Button, + Typography, + FormControl, + IconButton, + InputAdornment, + InputLabel, + OutlinedInput, +} from "@mui/material"; +import { + AccommodationCalendarURL, + AccommodationsCalendarURLApi, +} from "../../api/accommodations/AccommodationsCalendarURLApi"; +import { VisibilityOff, Visibility } from "@mui/icons-material"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import { CopyToClipboard } from "../../widgets/CopyToClipboard"; +import QRCode from "react-qr-code"; + +export function InstallCalendarDialog(p: { + cal?: AccommodationCalendarURL; + onClose: () => void; +}): React.ReactElement { + if (!p.cal) return <>; + + return ( + + Installation du calendrier + + + + Afin d'installer le calendrier {p.cal.name} sur votre + appareil, veuillez utiliser l'URL suivante : + +
+ + URL + + + + + + + + } + label="Password" + /> +
+ +
+
+
+
+ + + +
+ ); +} diff --git a/geneit_app/src/hooks/context_providers/accommodations/InstallCalendarDialogProvider.tsx b/geneit_app/src/hooks/context_providers/accommodations/InstallCalendarDialogProvider.tsx new file mode 100644 index 0000000..61d05cd --- /dev/null +++ b/geneit_app/src/hooks/context_providers/accommodations/InstallCalendarDialogProvider.tsx @@ -0,0 +1,44 @@ +import React, { PropsWithChildren } from "react"; +import { AccommodationCalendarURL } from "../../../api/accommodations/AccommodationsCalendarURLApi"; +import { InstallCalendarDialog } from "../../../dialogs/accommodations/InstallCalendarDialog"; + +type DialogContext = (cal: AccommodationCalendarURL) => Promise; + +const DialogContextK = React.createContext(null); + +export function InstallCalendarDialogProvider( + p: PropsWithChildren +): React.ReactElement { + const [cal, setCal] = React.useState(); + + const cb = React.useRef void)>(null); + + const handleClose = () => { + setCal(undefined); + + if (cb.current !== null) cb.current(); + cb.current = null; + }; + + const hook: DialogContext = (c) => { + setCal(c); + + return new Promise((res) => { + cb.current = res; + }); + }; + + return ( + <> + + {p.children} + + + {cal && } + + ); +} + +export function useInstallCalendarDialog(): DialogContext { + return React.useContext(DialogContextK)!; +} diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index dfb4495..534c51d 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -24,6 +24,8 @@ import { TimeWidget } from "../../../widgets/TimeWidget"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; import { useCreateAccommodationCalendarURL } from "../../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider"; import { AccommodationsCalendarURLApi } from "../../../api/accommodations/AccommodationsCalendarURLApi"; +import { useInstallCalendarDialog } from "../../../hooks/context_providers/accommodations/InstallCalendarDialogProvider"; +import { InstallCalendarDialog } from "../../../dialogs/accommodations/InstallCalendarDialog"; export function AccommodationsSettingsRoute(): React.ReactElement { return ( @@ -224,6 +226,7 @@ function AccommodationsCalURLsCard(): React.ReactElement { const family = useFamily(); const createCalendarURLDialog = useCreateAccommodationCalendarURL(); + const calendarURLDialog = useInstallCalendarDialog(); const createCalendarURL = async () => { try { @@ -241,8 +244,8 @@ function AccommodationsCalURLsCard(): React.ReactElement { setSuccess("Le calendrier a été créé avec succès !"); // TODO : reload URLS list - // TODO : show QrCode dialog - console.log(cal); + + calendarURLDialog(cal); } catch (e) { console.error("Failed to create new accommodation calendar URL!", e); setError(`Échec de la création du calendrier! ${e}`); diff --git a/geneit_app/src/widgets/CopyToClipboard.tsx b/geneit_app/src/widgets/CopyToClipboard.tsx new file mode 100644 index 0000000..72ea019 --- /dev/null +++ b/geneit_app/src/widgets/CopyToClipboard.tsx @@ -0,0 +1,30 @@ +import { ButtonBase } from "@mui/material"; +import { PropsWithChildren } from "react"; +import { useSnackbar } from "../hooks/context_providers/SnackbarProvider"; + +export function CopyToClipboard( + p: PropsWithChildren<{ content: string }> +): React.ReactElement { + const snackbar = useSnackbar(); + + const copy = () => { + navigator.clipboard.writeText(p.content); + snackbar(`${p.content} copied to clipboard.`); + }; + + return ( + + {p.children} + + ); +} diff --git a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx index 9516652..5e6b24b 100644 --- a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx +++ b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx @@ -8,6 +8,7 @@ import { CreateAccommodationCalendarURLDialogProvider } from "../../hooks/contex import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; import { AsyncWidget } from "../AsyncWidget"; import { useFamily } from "../BaseFamilyRoute"; +import { InstallCalendarDialogProvider } from "../../hooks/context_providers/accommodations/InstallCalendarDialogProvider"; interface AccommodationsContext { accommodations: AccommodationsList; @@ -63,7 +64,9 @@ export function BaseAccommodationsRoute(): React.ReactElement { > - + + + -- 2.45.2 From 0b0fe6b49e845d73072d6279277f231406ad6903 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 14 Jun 2024 19:48:26 +0200 Subject: [PATCH 33/65] Update --- .../accommodations/InstallCalendarDialog.tsx | 14 ++++++-------- .../accommodations/AccommodationsSettingsRoute.tsx | 8 +++----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx b/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx index bddd61d..2c6ac89 100644 --- a/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx @@ -1,25 +1,24 @@ +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import { + Button, Dialog, - DialogTitle, + DialogActions, DialogContent, DialogContentText, - DialogActions, - Button, - Typography, + DialogTitle, FormControl, IconButton, InputAdornment, InputLabel, OutlinedInput, + Typography, } from "@mui/material"; +import QRCode from "react-qr-code"; import { AccommodationCalendarURL, AccommodationsCalendarURLApi, } from "../../api/accommodations/AccommodationsCalendarURLApi"; -import { VisibilityOff, Visibility } from "@mui/icons-material"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import { CopyToClipboard } from "../../widgets/CopyToClipboard"; -import QRCode from "react-qr-code"; export function InstallCalendarDialog(p: { cal?: AccommodationCalendarURL; @@ -52,7 +51,6 @@ export function InstallCalendarDialog(p: { } - label="Password" />
Date: Sat, 15 Jun 2024 00:55:29 +0200 Subject: [PATCH 34/65] Display the list of calendars on accommodations settings page --- .../AccommodationsCalendarURLApi.tsx | 26 ++++ .../accommodations/InstallCalendarDialog.tsx | 2 - .../AccommodationsSettingsRoute.tsx | 125 +++++++++++++++++- 3 files changed, 148 insertions(+), 5 deletions(-) diff --git a/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx b/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx index b898904..c263beb 100644 --- a/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationsCalendarURLApi.tsx @@ -40,4 +40,30 @@ export class AccommodationsCalendarURLApi { static CalendarURL(c: AccommodationCalendarURL): string { return `${APIClient.backendURL()}/acccommodations_calendar/${c.token}`; } + + /** + * Get accommodations calendars list + */ + static async GetList(family: Family): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: `/family/${family.family_id}/accommodations/reservations_calendars/list`, + }) + ).data; + } + + /** + * Delete an accommodation calendar + */ + static async Delete( + calendar: AccommodationCalendarURL + ): Promise { + return ( + await APIClient.exec({ + method: "DELETE", + uri: `/family/${calendar.family_id}/accommodations/reservations_calendars/${calendar.id}`, + }) + ).data; + } } diff --git a/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx b/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx index 2c6ac89..91a92fb 100644 --- a/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/InstallCalendarDialog.tsx @@ -9,7 +9,6 @@ import { FormControl, IconButton, InputAdornment, - InputLabel, OutlinedInput, Typography, } from "@mui/material"; @@ -37,7 +36,6 @@ export function InstallCalendarDialog(p: {
- URL (); const [success, setSuccess] = React.useState(); + const [list, setList] = React.useState< + AccommodationCalendarURL[] | undefined + >(); + const family = useFamily(); const createCalendarURLDialog = useCreateAccommodationCalendarURL(); const calendarURLDialog = useInstallCalendarDialog(); + const load = async () => { + setList(await AccommodationsCalendarURLApi.GetList(family.family)); + }; + + const reload = () => { + key.current += 1; + setList(undefined); + }; + + const onRequestDelete = async (c: AccommodationCalendarURL) => { + setError(undefined); + setSuccess(undefined); + try { + if ( + !(await confirm( + `Voulez-vous vraiment supprimer le calendrier '${c.name}' ? Cette opération est définitive !` + )) + ) + return; + + loading.show("Suppression du calendrier en cours..."); + + await AccommodationsCalendarURLApi.Delete(c); + + snackbar("Le calendrier a été supprimé avec succès !"); + reload(); + } catch (e) { + console.error("Failed to delete accommodation!", e); + setError(`Échec de la suppression du logement! ${e}`); + } finally { + loading.hide(); + } + }; + const createCalendarURL = async () => { try { const newCal = await createCalendarURLDialog(); if (!newCal) return; - loading.show("Création du logement en cours..."); + loading.show("Création du calendrier en cours..."); const cal = await AccommodationsCalendarURLApi.Create( family.family, @@ -241,7 +287,7 @@ function AccommodationsCalURLsCard(): React.ReactElement { setSuccess("Le calendrier a été créé avec succès !"); - // TODO : reload URLS list + reload(); calendarURLDialog(cal); } catch (e) { @@ -274,7 +320,80 @@ function AccommodationsCalURLsCard(): React.ReactElement { > Créer un calendrier + +
+
+ + + list?.length === 0 ? ( + <> +

+ Vous n'avez créé aucun calendrier pour le moment ! +

+ + ) : ( + <> + {list?.map((c) => ( + + ))} + + ) + } + /> ); } + +function CalendarItem(p: { + c: AccommodationCalendarURL; + onRequestDelete: (c: AccommodationCalendarURL) => void; +}): React.ReactElement { + const accommodations = useAccommodations(); + + const installCal = useInstallCalendarDialog(); + + return ( + + + + + {p.c.name} + + + {p.c.accommodation_id + ? accommodations.accommodations.get(p.c.accommodation_id)?.name + : "Tous les logements"} + + + Créé il y a +
+ Utilisé il y a +
+
+ + + + + + +
+ ); +} -- 2.45.2 From b72374481e975f6d6d42c90ab4627b08d70cb8b2 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 15 Jun 2024 00:57:08 +0200 Subject: [PATCH 35/65] Use alert when possible --- .../family/accommodations/AccommodationsSettingsRoute.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index b13f647..9e8104a 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -222,7 +222,6 @@ function BoolIcon(p: { checked?: boolean }): React.ReactElement { function AccommodationsCalURLsCard(): React.ReactElement { const key = React.useRef(0); - const snackbar = useSnackbar(); const confirm = useConfirm(); const loading = useLoadingMessage(); @@ -262,7 +261,7 @@ function AccommodationsCalURLsCard(): React.ReactElement { await AccommodationsCalendarURLApi.Delete(c); - snackbar("Le calendrier a été supprimé avec succès !"); + setSuccess("Le calendrier a été supprimé avec succès !"); reload(); } catch (e) { console.error("Failed to delete accommodation!", e); -- 2.45.2 From f965ddc99efeb12acb1fdb70c329326f224263ea Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 15 Jun 2024 09:58:27 +0200 Subject: [PATCH 36/65] Add accommodations home route --- geneit_app/src/App.tsx | 2 ++ .../AccommodationsHomeRoute.tsx | 21 +++++++++++++++++++ geneit_app/src/widgets/BaseFamilyRoute.tsx | 13 ++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 geneit_app/src/routes/family/accommodations/AccommodationsHomeRoute.tsx diff --git a/geneit_app/src/App.tsx b/geneit_app/src/App.tsx index 0eaed2f..bc3cfd6 100644 --- a/geneit_app/src/App.tsx +++ b/geneit_app/src/App.tsx @@ -18,6 +18,7 @@ import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute"; import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute"; import { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute"; import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute"; +import { AccommodationsHomeRoute } from "./routes/family/accommodations/AccommodationsHomeRoute"; import { AccommodationsSettingsRoute } from "./routes/family/accommodations/AccommodationsSettingsRoute"; import { FamilyCoupleRoute, @@ -116,6 +117,7 @@ export function App(): React.ReactElement { path="accommodations/*" element={} > + } /> } diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsHomeRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsHomeRoute.tsx new file mode 100644 index 0000000..861573b --- /dev/null +++ b/geneit_app/src/routes/family/accommodations/AccommodationsHomeRoute.tsx @@ -0,0 +1,21 @@ +import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; +import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; + +export function AccommodationsHomeRoute(): React.ReactElement { + const accommodations = useAccommodations(); + return ( + <> + +
+

+ Depuis cette section de l'application, vous pouvez effectuer des + réservations de logements. +

+

 

+

+ Nombre de logements définis : {accommodations.accommodations.size} +

+
+ + ); +} diff --git a/geneit_app/src/widgets/BaseFamilyRoute.tsx b/geneit_app/src/widgets/BaseFamilyRoute.tsx index 5206921..7f215db 100644 --- a/geneit_app/src/widgets/BaseFamilyRoute.tsx +++ b/geneit_app/src/widgets/BaseFamilyRoute.tsx @@ -185,6 +185,19 @@ export function BaseFamilyRoute(): React.ReactElement { )} + {family?.enable_accommodations && ( + <> + + Logements + + } + label="Accueil" + uri="accommodations" + /> + + )} + Administration -- 2.45.2 From d6c4f38176223d3aed4a4bded3439ca07d0d866b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 15 Jun 2024 10:35:02 +0200 Subject: [PATCH 37/65] Ready to implement reservations route --- geneit_app/package-lock.json | 174 ++++++++++++++++++ geneit_app/package.json | 2 + geneit_app/src/App.tsx | 5 + .../AccommodationsReservationsRoute.tsx | 3 + .../AccommodationsSettingsRoute.tsx | 6 +- geneit_app/src/widgets/BaseFamilyRoute.tsx | 6 + 6 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx diff --git a/geneit_app/package-lock.json b/geneit_app/package-lock.json index f726b02..f88cf8f 100644 --- a/geneit_app/package-lock.json +++ b/geneit_app/package-lock.json @@ -24,6 +24,7 @@ "@testing-library/user-event": "^14.0.0", "@types/jest": "^29.0.0", "@types/react": "^18.3.2", + "@types/react-big-calendar": "^1.8.9", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.2.1", "date-and-time": "^3.2.0", @@ -31,6 +32,7 @@ "filesize": "^10.1.2", "jspdf": "^2.5.1", "react": "^18.3.1", + "react-big-calendar": "^1.13.0", "react-dom": "^18.3.1", "react-easy-crop": "^5.0.7", "react-qr-code": "^2.0.14", @@ -1556,6 +1558,17 @@ "node": ">=14.0.0" } }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.17.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", @@ -2042,6 +2055,11 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/date-arithmetic": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz", + "integrity": "sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==" + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2134,6 +2152,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-big-calendar": { + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/@types/react-big-calendar/-/react-big-calendar-1.8.9.tgz", + "integrity": "sha512-HIHLUxR3PzWHrFdZ00VnCMvDjAh5uzlL0vMC2b7tL3bKaAJsqq9T8h+x0GVeDbZfMfHAd1cs5tZBhVvourNJXQ==", + "dependencies": { + "@types/date-arithmetic": "*", + "@types/prop-types": "*", + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", @@ -2155,6 +2183,11 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -2482,6 +2515,16 @@ "integrity": "sha512-UguWfh9LkUecVrGSE0B7SpAnGRMPATmpwSoSij24/lDnwET3A641abfDBD/TdL0T+E04f8NWlbMkD9BscVvIZg==", "license": "MIT" }, + "node_modules/date-arithmetic": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", + "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==" + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2690,6 +2733,11 @@ "node": ">=6.9.0" } }, + "node_modules/globalize": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz", + "integrity": "sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==" + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2777,6 +2825,14 @@ "node": ">=8" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3259,6 +3315,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3278,6 +3339,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -3287,6 +3356,11 @@ "lz-string": "bin/bin.js" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -3307,6 +3381,25 @@ "node": ">=4" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3509,6 +3602,41 @@ "node": ">=0.10.0" } }, + "node_modules/react-big-calendar": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.13.0.tgz", + "integrity": "sha512-3ewolEKeBC5CjuxxDbo+IfQXjcd6jIBLSOoMzn1/lVMf+BYhPneifuOjMseXCIIaA4UlGZcy625BIdYgtAx+cA==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "clsx": "^1.2.1", + "date-arithmetic": "^4.1.0", + "dayjs": "^1.11.7", + "dom-helpers": "^5.2.1", + "globalize": "^0.1.1", + "invariant": "^2.2.4", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "luxon": "^3.2.1", + "memoize-one": "^6.0.0", + "moment": "^2.29.4", + "moment-timezone": "^0.5.40", + "prop-types": "^15.8.1", + "react-overlays": "^5.2.1", + "uncontrollable": "^7.2.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17 || ^18", + "react-dom": "^16.14.0 || ^17 || ^18" + } + }, + "node_modules/react-big-calendar/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -3539,6 +3667,30 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-overlays": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", + "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", + "dependencies": { + "@babel/runtime": "^7.13.8", + "@popperjs/core": "^2.11.6", + "@restart/hooks": "^0.4.7", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, "node_modules/react-qr-code": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.14.tgz", @@ -3922,6 +4074,20 @@ "node": ">=14.17" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -4038,6 +4204,14 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/web-vitals": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.2.tgz", diff --git a/geneit_app/package.json b/geneit_app/package.json index 2730de4..f3008b8 100644 --- a/geneit_app/package.json +++ b/geneit_app/package.json @@ -20,6 +20,7 @@ "@testing-library/user-event": "^14.0.0", "@types/jest": "^29.0.0", "@types/react": "^18.3.2", + "@types/react-big-calendar": "^1.8.9", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.2.1", "date-and-time": "^3.2.0", @@ -27,6 +28,7 @@ "filesize": "^10.1.2", "jspdf": "^2.5.1", "react": "^18.3.1", + "react-big-calendar": "^1.13.0", "react-dom": "^18.3.1", "react-easy-crop": "^5.0.7", "react-qr-code": "^2.0.14", diff --git a/geneit_app/src/App.tsx b/geneit_app/src/App.tsx index bc3cfd6..9299040 100644 --- a/geneit_app/src/App.tsx +++ b/geneit_app/src/App.tsx @@ -19,6 +19,7 @@ import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute"; import { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute"; import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute"; import { AccommodationsHomeRoute } from "./routes/family/accommodations/AccommodationsHomeRoute"; +import { AccommodationsReservationsRoute } from "./routes/family/accommodations/AccommodationsReservationsRoute"; import { AccommodationsSettingsRoute } from "./routes/family/accommodations/AccommodationsSettingsRoute"; import { FamilyCoupleRoute, @@ -118,6 +119,10 @@ export function App(): React.ReactElement { element={} > } /> + } + /> } diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx new file mode 100644 index 0000000..310e67e --- /dev/null +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -0,0 +1,3 @@ +export function AccommodationsReservationsRoute(): React.ReactElement { + return <>TODO; +} diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index 9e8104a..eb7f187 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -29,6 +29,8 @@ import { FamilyCard } from "../../../widgets/FamilyCard"; import { TimeWidget } from "../../../widgets/TimeWidget"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; +const CARDS_WIDTH = "500px"; + export function AccommodationsSettingsRoute(): React.ReactElement { return ( <> @@ -126,7 +128,7 @@ function AccommodationsListCard(): React.ReactElement { }; return ( - + Logements @@ -298,7 +300,7 @@ function AccommodationsCalURLsCard(): React.ReactElement { }; return ( - + URL de calendriers diff --git a/geneit_app/src/widgets/BaseFamilyRoute.tsx b/geneit_app/src/widgets/BaseFamilyRoute.tsx index 7f215db..bb1c64f 100644 --- a/geneit_app/src/widgets/BaseFamilyRoute.tsx +++ b/geneit_app/src/widgets/BaseFamilyRoute.tsx @@ -12,6 +12,7 @@ import { mdiRefresh, } from "@mdi/js"; import Icon from "@mdi/react"; +import CalendarMonthIcon from "@mui/icons-material/CalendarMonth"; import HomeIcon from "@mui/icons-material/Home"; import { Box, @@ -195,6 +196,11 @@ export function BaseFamilyRoute(): React.ReactElement { label="Accueil" uri="accommodations" /> + } + label="Réservations" + uri="accommodations/reservations" + /> )} -- 2.45.2 From c8993f906dd272b7c01af3d8c7faa3d0320c0274 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 17 Jun 2024 21:23:51 +0200 Subject: [PATCH 38/65] WIP calendar integration --- .../AccommodationsReservationsApi.tsx | 75 ++++++++ .../AccommodationsReservationsRoute.tsx | 180 +++++++++++++++++- 2 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx diff --git a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx new file mode 100644 index 0000000..b5d469f --- /dev/null +++ b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx @@ -0,0 +1,75 @@ +import { APIClient } from "../ApiClient"; +import { Family } from "../FamilyApi"; +import moment from "moment"; + +export interface AccommodationReservation { + id: number; + family_id: number; + accommodation_id: number; + user_id: number; + time_create: number; + time_update: number; + reservation_start: number; + reservation_end: number; + validated?: boolean; +} + +export class AccommodationsReservationsList { + private list: AccommodationReservation[]; + private map: Map; + + constructor(list: AccommodationReservation[]) { + this.list = list; + this.map = new Map(); + + for (const m of list) { + this.map.set(m.id, m); + } + + this.list.sort((a, b) => a.reservation_start - b.reservation_start); + } + + public get isEmpty(): boolean { + return this.list.length === 0; + } + + public get size(): number { + return this.list.length; + } + + public get fullList(): AccommodationReservation[] { + return this.list; + } + + filter( + predicate: (m: AccommodationReservation) => boolean + ): AccommodationReservation[] { + return this.list.filter(predicate); + } + + forAccommodation(id: number): AccommodationReservation[] { + return this.filter((a) => a.accommodation_id === id); + } + + get(id: number): AccommodationReservation | undefined { + return this.map.get(id); + } +} + +export class AccommodationsReservationsApi { + /** + * Get the entire list of accommodations of a family + */ + static async FullListOfFamily( + family: Family + ): Promise { + const data = ( + await APIClient.exec({ + method: "GET", + uri: `/family/${family.family_id}/accommodations/reservations/full_list`, + }) + ).data; + + return new AccommodationsReservationsList(data); + } +} diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 310e67e..036fd2f 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -1,3 +1,181 @@ +import React from "react"; +import { FamilyApi, FamilyUser } from "../../../api/FamilyApi"; +import { + AccommodationsReservationsApi, + AccommodationsReservationsList, +} from "../../../api/accommodations/AccommodationsReservationsApi"; +import { AsyncWidget } from "../../../widgets/AsyncWidget"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; +import { + FormControl, + FormLabel, + FormGroup, + FormControlLabel, + Checkbox, + FormHelperText, +} from "@mui/material"; +import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; +import { + Calendar, + DateLocalizer, + Views, + momentLocalizer, +} from "react-big-calendar"; +import moment from "moment"; + +const localizer = momentLocalizer(moment); + export function AccommodationsReservationsRoute(): React.ReactElement { - return <>TODO; + const loadKey = React.useRef(1); + + const family = useFamily(); + const accommodations = useAccommodations(); + + const [reservations, setReservations] = React.useState< + AccommodationsReservationsList | undefined + >(); + const [users, setUsers] = React.useState(null); + + const [showValidated, setShowValidated] = React.useState(true); + const [showRejected, setShowRejected] = React.useState(true); + const [showPending, setShowPending] = React.useState(true); + + const [hiddenPeople, setHiddenPeople] = React.useState>( + new Set() + ); + const [hiddenAccommodations, setHiddenAccommodations] = React.useState< + Set + >(new Set()); + + const load = async () => { + setReservations( + await AccommodationsReservationsApi.FullListOfFamily(family.family) + ); + setUsers(await FamilyApi.GetUsersList(family.family.family_id)); + }; + + const reload = async () => { + loadKey.current += 1; + setUsers(null); + }; + + return ( + <> + + ( +
+
+ {/* Invitation status */} + + Status + + setShowValidated(v)} + /> + } + label="Validé" + /> + setShowRejected(v)} + /> + } + label="Rejetés" + /> + setShowPending(v)} + /> + } + label="En attente de validation" + /> + + + + {/* Accommodations */} + + Logements + + {accommodations.accommodations.fullList.map((a) => ( + { + if (v) hiddenAccommodations.delete(a.id); + else hiddenAccommodations.add(a.id); + setHiddenAccommodations( + new Set(hiddenAccommodations) + ); + }} + /> + } + label={a.name} + /> + ))} + + + + {/* People */} + + Personnes + + {users?.map((u) => ( + { + if (v) hiddenPeople.delete(u.user_id); + else hiddenPeople.add(u.user_id); + setHiddenPeople(new Set(hiddenPeople)); + }} + /> + } + label={u.user_name} + /> + ))} + + +
+ + {/* The calendar */} +
+ +
+
+ )} + /> + + ); } -- 2.45.2 From 5cac7c71418a2b674d149f021c7edfc455239219 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 17 Jun 2024 21:39:12 +0200 Subject: [PATCH 39/65] Use another planning --- geneit_app/package-lock.json | 221 ++++-------------- geneit_app/package.json | 6 +- .../AccommodationsReservationsRoute.tsx | 29 +-- 3 files changed, 66 insertions(+), 190 deletions(-) diff --git a/geneit_app/package-lock.json b/geneit_app/package-lock.json index d740e0a..f0db38d 100644 --- a/geneit_app/package-lock.json +++ b/geneit_app/package-lock.json @@ -12,6 +12,10 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^5.0.13", + "@fullcalendar/core": "^6.1.14", + "@fullcalendar/daygrid": "^6.1.14", + "@fullcalendar/list": "^6.1.14", + "@fullcalendar/react": "^6.1.14", "@mdi/js": "^7.2.96", "@mdi/react": "^1.6.1", "@mui/icons-material": "^5.15.17", @@ -24,7 +28,6 @@ "@testing-library/user-event": "^14.0.0", "@types/jest": "^29.0.0", "@types/react": "^18.3.2", - "@types/react-big-calendar": "^1.8.9", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.2.1", "date-and-time": "^3.2.0", @@ -32,7 +35,6 @@ "filesize": "^10.1.2", "jspdf": "^2.5.1", "react": "^18.3.1", - "react-big-calendar": "^1.13.0", "react-dom": "^18.3.1", "react-easy-crop": "^5.0.7", "react-qr-code": "^2.0.14", @@ -1059,6 +1061,40 @@ "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.13.tgz", "integrity": "sha512-j61DHjsdUCKMXSdNLTOxcG701FWnF0jcqNNQi2iPCDxU8seN/sMxeh62dC++UiagCWq9ghTypX+Pcy7kX+QOeQ==" }, + "node_modules/@fullcalendar/core": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.14.tgz", + "integrity": "sha512-hIPRBevm0aMc2aHy1hRIJgXmI1QTvQM1neQa9oxtuqUmF1+ApYC3oAdwcQMTuI7lHHw3pKJDyJFkKLPPnL6HXA==", + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.14.tgz", + "integrity": "sha512-DSyjiA1dEM8k3bOCrZpZOmAOZu71KGtH02ze+4QKuhxkmn/zQghmmLRdfzpOrcyJg6xGKkoB4pBcO+2lXar8XQ==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.14" + } + }, + "node_modules/@fullcalendar/list": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.14.tgz", + "integrity": "sha512-eV0/6iCumYfvlPzIUTAONWH17/JlQCyCChUz8m06L4E/sOiNjkHGz8vlVTmZKqXzx9oWOOyV/Nm3pCtHmVZh+Q==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.14" + } + }, + "node_modules/@fullcalendar/react": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.14.tgz", + "integrity": "sha512-sXLn2D8aPYLuDH3fy2ZhHTOz5WNSU1NhoECsGBzjUtz2IYHy6m5Y9TqlyqeAqVqFLDRSJAlKAr5LyrIvnD/IMA==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.14", + "react": "^16.7.0 || ^17 || ^18 || ^19", + "react-dom": "^16.7.0 || ^17 || ^18 || ^19" + } + }, "node_modules/@jest/expect-utils": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", @@ -1581,17 +1617,6 @@ "node": ">=14.0.0" } }, - "node_modules/@restart/hooks": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", - "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.17.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", @@ -2078,11 +2103,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/date-arithmetic": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz", - "integrity": "sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==" - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2175,16 +2195,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-big-calendar": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/@types/react-big-calendar/-/react-big-calendar-1.8.9.tgz", - "integrity": "sha512-HIHLUxR3PzWHrFdZ00VnCMvDjAh5uzlL0vMC2b7tL3bKaAJsqq9T8h+x0GVeDbZfMfHAd1cs5tZBhVvourNJXQ==", - "dependencies": { - "@types/date-arithmetic": "*", - "@types/prop-types": "*", - "@types/react": "*" - } - }, "node_modules/@types/react-dom": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", @@ -2206,11 +2216,6 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, - "node_modules/@types/warning": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", - "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" - }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -2538,16 +2543,6 @@ "integrity": "sha512-UguWfh9LkUecVrGSE0B7SpAnGRMPATmpwSoSij24/lDnwET3A641abfDBD/TdL0T+E04f8NWlbMkD9BscVvIZg==", "license": "MIT" }, - "node_modules/date-arithmetic": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", - "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==" - }, - "node_modules/dayjs": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", - "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2757,11 +2752,6 @@ "node": ">=6.9.0" } }, - "node_modules/globalize": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz", - "integrity": "sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==" - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2849,14 +2839,6 @@ "node": ">=8" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3339,11 +3321,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3363,14 +3340,6 @@ "yallist": "^3.0.2" } }, - "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", - "engines": { - "node": ">=12" - } - }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -3380,11 +3349,6 @@ "lz-string": "bin/bin.js" } }, - "node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" - }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -3405,25 +3369,6 @@ "node": ">=4" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.45", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", - "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3554,6 +3499,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -3626,41 +3580,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-big-calendar": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.13.0.tgz", - "integrity": "sha512-3ewolEKeBC5CjuxxDbo+IfQXjcd6jIBLSOoMzn1/lVMf+BYhPneifuOjMseXCIIaA4UlGZcy625BIdYgtAx+cA==", - "dependencies": { - "@babel/runtime": "^7.20.7", - "clsx": "^1.2.1", - "date-arithmetic": "^4.1.0", - "dayjs": "^1.11.7", - "dom-helpers": "^5.2.1", - "globalize": "^0.1.1", - "invariant": "^2.2.4", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "luxon": "^3.2.1", - "memoize-one": "^6.0.0", - "moment": "^2.29.4", - "moment-timezone": "^0.5.40", - "prop-types": "^15.8.1", - "react-overlays": "^5.2.1", - "uncontrollable": "^7.2.1" - }, - "peerDependencies": { - "react": "^16.14.0 || ^17 || ^18", - "react-dom": "^16.14.0 || ^17 || ^18" - } - }, - "node_modules/react-big-calendar/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "engines": { - "node": ">=6" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -3691,30 +3610,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" - }, - "node_modules/react-overlays": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", - "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", - "dependencies": { - "@babel/runtime": "^7.13.8", - "@popperjs/core": "^2.11.6", - "@restart/hooks": "^0.4.7", - "@types/warning": "^3.0.0", - "dom-helpers": "^5.2.0", - "prop-types": "^15.7.2", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, "node_modules/react-qr-code": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.14.tgz", @@ -4098,20 +3993,6 @@ "node": ">=14.17" } }, - "node_modules/uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "dependencies": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0" - } - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -4228,14 +4109,6 @@ } } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/web-vitals": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.2.tgz", diff --git a/geneit_app/package.json b/geneit_app/package.json index f3008b8..6505f4e 100644 --- a/geneit_app/package.json +++ b/geneit_app/package.json @@ -8,6 +8,10 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^5.0.13", + "@fullcalendar/core": "^6.1.14", + "@fullcalendar/daygrid": "^6.1.14", + "@fullcalendar/list": "^6.1.14", + "@fullcalendar/react": "^6.1.14", "@mdi/js": "^7.2.96", "@mdi/react": "^1.6.1", "@mui/icons-material": "^5.15.17", @@ -20,7 +24,6 @@ "@testing-library/user-event": "^14.0.0", "@types/jest": "^29.0.0", "@types/react": "^18.3.2", - "@types/react-big-calendar": "^1.8.9", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.2.1", "date-and-time": "^3.2.0", @@ -28,7 +31,6 @@ "filesize": "^10.1.2", "jspdf": "^2.5.1", "react": "^18.3.1", - "react-big-calendar": "^1.13.0", "react-dom": "^18.3.1", "react-easy-crop": "^5.0.7", "react-qr-code": "^2.0.14", diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 036fd2f..f03c005 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -16,15 +16,10 @@ import { FormHelperText, } from "@mui/material"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; -import { - Calendar, - DateLocalizer, - Views, - momentLocalizer, -} from "react-big-calendar"; -import moment from "moment"; - -const localizer = momentLocalizer(moment); +import FullCalendar from "@fullcalendar/react"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import frLocale from "@fullcalendar/core/locales/fr"; +import listPlugin from "@fullcalendar/list"; export function AccommodationsReservationsRoute(): React.ReactElement { const loadKey = React.useRef(1); @@ -69,7 +64,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { errMsg="Echec du chargement de la liste des réservations !" build={() => (
-
+
{/* Invitation status */} -
-- 2.45.2 From 6e6b45e0cc62fbb7c83ffda07798f5fcc6f87d68 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 17 Jun 2024 21:51:23 +0200 Subject: [PATCH 40/65] Add calendar interactions --- geneit_app/package-lock.json | 9 +++++ geneit_app/package.json | 1 + .../AccommodationsReservationsRoute.tsx | 36 +++++++++++-------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/geneit_app/package-lock.json b/geneit_app/package-lock.json index f0db38d..4a49568 100644 --- a/geneit_app/package-lock.json +++ b/geneit_app/package-lock.json @@ -14,6 +14,7 @@ "@fontsource/roboto": "^5.0.13", "@fullcalendar/core": "^6.1.14", "@fullcalendar/daygrid": "^6.1.14", + "@fullcalendar/interaction": "^6.1.14", "@fullcalendar/list": "^6.1.14", "@fullcalendar/react": "^6.1.14", "@mdi/js": "^7.2.96", @@ -1077,6 +1078,14 @@ "@fullcalendar/core": "~6.1.14" } }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.14.tgz", + "integrity": "sha512-rXum5XCjq+WEPNctFeYL/JKZGeU2rlxrElygocdMegcrIBJQW5hnWWVE+i4/1dOmUKF80CbGVlXUyYXoqK2eFg==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.14" + } + }, "node_modules/@fullcalendar/list": { "version": "6.1.14", "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.14.tgz", diff --git a/geneit_app/package.json b/geneit_app/package.json index 6505f4e..ce72f20 100644 --- a/geneit_app/package.json +++ b/geneit_app/package.json @@ -10,6 +10,7 @@ "@fontsource/roboto": "^5.0.13", "@fullcalendar/core": "^6.1.14", "@fullcalendar/daygrid": "^6.1.14", + "@fullcalendar/interaction": "^6.1.14", "@fullcalendar/list": "^6.1.14", "@fullcalendar/react": "^6.1.14", "@mdi/js": "^7.2.96", diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index f03c005..4bcb28f 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -1,3 +1,16 @@ +import { DateSelectArg } from "@fullcalendar/core"; +import frLocale from "@fullcalendar/core/locales/fr"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import interactionPlugin from "@fullcalendar/interaction"; +import listPlugin from "@fullcalendar/list"; +import FullCalendar from "@fullcalendar/react"; +import { + Checkbox, + FormControl, + FormControlLabel, + FormGroup, + FormLabel, +} from "@mui/material"; import React from "react"; import { FamilyApi, FamilyUser } from "../../../api/FamilyApi"; import { @@ -7,19 +20,7 @@ import { import { AsyncWidget } from "../../../widgets/AsyncWidget"; import { useFamily } from "../../../widgets/BaseFamilyRoute"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; -import { - FormControl, - FormLabel, - FormGroup, - FormControlLabel, - Checkbox, - FormHelperText, -} from "@mui/material"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; -import FullCalendar from "@fullcalendar/react"; -import dayGridPlugin from "@fullcalendar/daygrid"; -import frLocale from "@fullcalendar/core/locales/fr"; -import listPlugin from "@fullcalendar/list"; export function AccommodationsReservationsRoute(): React.ReactElement { const loadKey = React.useRef(1); @@ -55,6 +56,10 @@ export function AccommodationsReservationsRoute(): React.ReactElement { setUsers(null); }; + const onSelect = (d: DateSelectArg) => { + console.info(d); + }; + return ( <> @@ -64,7 +69,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { errMsg="Echec du chargement de la liste des réservations !" build={() => (
-
+
{/* Invitation status */}
-- 2.45.2 From b8a74013090f82bce612bb16b6cd53b701122385 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 18 Jun 2024 18:23:57 +0200 Subject: [PATCH 41/65] Start to build create reservation dialog --- .../accommodations/AccommodationListApi.tsx | 4 + .../AccommodationsReservationsApi.tsx | 7 +- .../UpdateReservationDialog.tsx | 86 +++++++++++++++++++ .../UpdateReservationDialogProvider.tsx | 64 ++++++++++++++ .../AccommodationsReservationsRoute.tsx | 12 +++ .../BaseAccommodationsRoute.tsx | 7 +- 6 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx create mode 100644 geneit_app/src/hooks/context_providers/accommodations/UpdateReservationDialogProvider.tsx diff --git a/geneit_app/src/api/accommodations/AccommodationListApi.tsx b/geneit_app/src/api/accommodations/AccommodationListApi.tsx index 9125d3f..e9a595d 100644 --- a/geneit_app/src/api/accommodations/AccommodationListApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationListApi.tsx @@ -45,6 +45,10 @@ export class AccommodationsList { return this.list.filter(predicate); } + get openToReservationList(): Accommodation[] { + return this.filter((a) => a.open_to_reservations); + } + get(id: number): Accommodation | undefined { return this.map.get(id); } diff --git a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx index b5d469f..845372a 100644 --- a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx @@ -1,6 +1,5 @@ import { APIClient } from "../ApiClient"; import { Family } from "../FamilyApi"; -import moment from "moment"; export interface AccommodationReservation { id: number; @@ -56,6 +55,12 @@ export class AccommodationsReservationsList { } } +export interface UpdateAccommodationReservation { + start: number; + end: number; + accommodation_id: number; +} + export class AccommodationsReservationsApi { /** * Get the entire list of accommodations of a family diff --git a/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx new file mode 100644 index 0000000..75fc5e4 --- /dev/null +++ b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx @@ -0,0 +1,86 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from "@mui/material"; +import React from "react"; +import { UpdateAccommodationReservation } from "../../api/accommodations/AccommodationsReservationsApi"; +import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute"; +import { PropSelect } from "../../widgets/forms/PropSelect"; + +export function UpdateReservationDialog(p: { + open: boolean; + create: boolean; + reservation?: UpdateAccommodationReservation; + onClose: () => void; + onSubmitted: (c: UpdateAccommodationReservation) => void; +}): React.ReactElement { + const accommodations = useAccommodations(); + + const [reservation, setReservation] = React.useState< + UpdateAccommodationReservation | undefined + >(); + + const clearForm = () => { + setReservation(undefined); + }; + + const cancel = () => { + clearForm(); + p.onClose(); + }; + + const submit = async () => { + clearForm(); + p.onSubmitted(reservation!); + }; + + React.useEffect(() => { + if (!reservation) setReservation(p.reservation); + }, [p.open, p.reservation]); + + return ( + + + {p.create ? "Création" : "Mise à jour"} d'une réservation + + + { + setReservation((a) => { + return { + ...a!, + accommodation_id: Number(v), + }; + }); + }} + options={accommodations.accommodations.openToReservationList.map( + (a) => { + return { label: a.name, value: a.id.toString() }; + } + )} + value={reservation?.accommodation_id?.toString()} + /> + + {/* TODO : la suite */} + + + + + + + ); +} diff --git a/geneit_app/src/hooks/context_providers/accommodations/UpdateReservationDialogProvider.tsx b/geneit_app/src/hooks/context_providers/accommodations/UpdateReservationDialogProvider.tsx new file mode 100644 index 0000000..e22b6ab --- /dev/null +++ b/geneit_app/src/hooks/context_providers/accommodations/UpdateReservationDialogProvider.tsx @@ -0,0 +1,64 @@ +import React, { PropsWithChildren } from "react"; +import { UpdateAccommodationReservation } from "../../../api/accommodations/AccommodationsReservationsApi"; +import { UpdateReservationDialog } from "../../../dialogs/accommodations/UpdateReservationDialog"; + +type DialogContext = ( + reservation: UpdateAccommodationReservation, + create: boolean +) => Promise; + +const DialogContextK = React.createContext(null); + +export function UpdateReservationDialogProvider( + p: PropsWithChildren +): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [reservation, setReservation] = React.useState< + UpdateAccommodationReservation | undefined + >(undefined); + const [create, setCreate] = React.useState(false); + + const cb = React.useRef< + null | ((a: UpdateAccommodationReservation | undefined) => void) + >(null); + + const handleClose = (res?: UpdateAccommodationReservation) => { + setOpen(false); + + if (cb.current !== null) cb.current(res); + cb.current = null; + }; + + const hook: DialogContext = (accommodation, create) => { + setReservation(accommodation); + setCreate(create); + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + return ( + <> + + {p.children} + + + {open && ( + + )} + + ); +} + +export function useUpdateAccommodationReservation(): DialogContext { + return React.useContext(DialogContextK)!; +} diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 4bcb28f..2fa5c6e 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -17,6 +17,7 @@ import { AccommodationsReservationsApi, AccommodationsReservationsList, } from "../../../api/accommodations/AccommodationsReservationsApi"; +import { useUpdateAccommodationReservation } from "../../../hooks/context_providers/accommodations/UpdateReservationDialogProvider"; import { AsyncWidget } from "../../../widgets/AsyncWidget"; import { useFamily } from "../../../widgets/BaseFamilyRoute"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; @@ -27,6 +28,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { const family = useFamily(); const accommodations = useAccommodations(); + const updateReservation = useUpdateAccommodationReservation(); const [reservations, setReservations] = React.useState< AccommodationsReservationsList | undefined @@ -57,7 +59,17 @@ export function AccommodationsReservationsRoute(): React.ReactElement { }; const onSelect = (d: DateSelectArg) => { + // TODO : render this functional + // TODO : handle busy case console.info(d); + updateReservation( + { + accommodation_id: -1, + start: Math.floor(d.start.getDate() / 1000), + end: Math.floor(d.end.getDate() / 1000), + }, + true + ); }; return ( diff --git a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx index 5e6b24b..b34ae3d 100644 --- a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx +++ b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx @@ -5,10 +5,11 @@ import { AccommodationsList, } from "../../api/accommodations/AccommodationListApi"; import { CreateAccommodationCalendarURLDialogProvider } from "../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider"; +import { InstallCalendarDialogProvider } from "../../hooks/context_providers/accommodations/InstallCalendarDialogProvider"; import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; +import { UpdateReservationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateReservationDialogProvider"; import { AsyncWidget } from "../AsyncWidget"; import { useFamily } from "../BaseFamilyRoute"; -import { InstallCalendarDialogProvider } from "../../hooks/context_providers/accommodations/InstallCalendarDialogProvider"; interface AccommodationsContext { accommodations: AccommodationsList; @@ -65,7 +66,9 @@ export function BaseAccommodationsRoute(): React.ReactElement { - + + + -- 2.45.2 From c8a01f11b26592761c08e6d5814bf1f8e9e2ef16 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 18 Jun 2024 22:59:03 +0200 Subject: [PATCH 42/65] Start to be able to select reservation dates --- geneit_app/package-lock.json | 72 +++++++++++++++++++ geneit_app/package.json | 2 + .../CreateAccommodationCalendarURLDialog.tsx | 2 +- .../UpdateReservationDialog.tsx | 28 ++++++++ .../AccommodationsReservationsRoute.tsx | 4 +- geneit_app/src/utils/time_utils.ts | 6 ++ .../src/widgets/forms/PropDateInput.tsx | 53 ++++++++++++++ 7 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 geneit_app/src/utils/time_utils.ts create mode 100644 geneit_app/src/widgets/forms/PropDateInput.tsx diff --git a/geneit_app/package-lock.json b/geneit_app/package-lock.json index 174d238..8622b02 100644 --- a/geneit_app/package-lock.json +++ b/geneit_app/package-lock.json @@ -23,6 +23,7 @@ "@mui/lab": "^5.0.0-alpha.140", "@mui/material": "^5.15.17", "@mui/x-data-grid": "^7.1.1", + "@mui/x-date-pickers": "^7.7.0", "@mui/x-tree-view": "^7.4.0", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^16.0.0", @@ -32,6 +33,7 @@ "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.2.1", "date-and-time": "^3.2.0", + "dayjs": "^1.11.11", "email-validator": "^2.0.4", "filesize": "^10.1.2", "jspdf": "^2.5.1", @@ -1579,6 +1581,71 @@ "react-dom": "^17.0.0 || ^18.0.0" } }, + "node_modules/@mui/x-date-pickers": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.7.0.tgz", + "integrity": "sha512-huyoA22Vi8iCkee6ro0sX7CcFIcPV/Fl7ZGWwaQC8PTAheXhz823DjMYAiwRU/imF+UFYfUInWQ4XZCIkM+2Dw==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@mui/base": "^5.0.0-beta.40", + "@mui/system": "^5.15.15", + "@mui/utils": "^5.15.14", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14", + "date-fns": "^2.25.0 || ^3.2.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, "node_modules/@mui/x-tree-view": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.7.0.tgz", @@ -2552,6 +2619,11 @@ "integrity": "sha512-UguWfh9LkUecVrGSE0B7SpAnGRMPATmpwSoSij24/lDnwET3A641abfDBD/TdL0T+E04f8NWlbMkD9BscVvIZg==", "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/geneit_app/package.json b/geneit_app/package.json index ce72f20..f7872ec 100644 --- a/geneit_app/package.json +++ b/geneit_app/package.json @@ -19,6 +19,7 @@ "@mui/lab": "^5.0.0-alpha.140", "@mui/material": "^5.15.17", "@mui/x-data-grid": "^7.1.1", + "@mui/x-date-pickers": "^7.7.0", "@mui/x-tree-view": "^7.4.0", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^16.0.0", @@ -28,6 +29,7 @@ "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.2.1", "date-and-time": "^3.2.0", + "dayjs": "^1.11.11", "email-validator": "^2.0.4", "filesize": "^10.1.2", "jspdf": "^2.5.1", diff --git a/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx b/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx index f1fe0b8..d95abf0 100644 --- a/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx @@ -9,9 +9,9 @@ import React from "react"; import { ServerApi } from "../../api/ServerApi"; import { NewCalendarURL } from "../../api/accommodations/AccommodationsCalendarURLApi"; import { checkConstraint } from "../../utils/from_utils"; +import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute"; import { PropEdit } from "../../widgets/forms/PropEdit"; import { PropSelect } from "../../widgets/forms/PropSelect"; -import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute"; export function CreateAccommodationCalendarURLDialog(p: { open: boolean; diff --git a/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx index 75fc5e4..841aae9 100644 --- a/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx @@ -8,6 +8,7 @@ import { import React from "react"; import { UpdateAccommodationReservation } from "../../api/accommodations/AccommodationsReservationsApi"; import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute"; +import { PropDateInput } from "../../widgets/forms/PropDateInput"; import { PropSelect } from "../../widgets/forms/PropSelect"; export function UpdateReservationDialog(p: { @@ -41,6 +42,8 @@ export function UpdateReservationDialog(p: { if (!reservation) setReservation(p.reservation); }, [p.open, p.reservation]); + // TODO : check availability + return ( @@ -66,6 +69,31 @@ export function UpdateReservationDialog(p: { value={reservation?.accommodation_id?.toString()} /> + { + setReservation((r) => { + return { ...r!, start: s ?? -1 }; + }); + }} + /> + + { + setReservation((r) => { + return { ...r!, end: s ?? -1 }; + }); + }} + /> + + {/* Constraint start and end */} + {/* TODO : la suite */} diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 2fa5c6e..5d979a8 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -65,8 +65,8 @@ export function AccommodationsReservationsRoute(): React.ReactElement { updateReservation( { accommodation_id: -1, - start: Math.floor(d.start.getDate() / 1000), - end: Math.floor(d.end.getDate() / 1000), + start: Math.floor(d.start.getTime() / 1000), + end: Math.floor(d.end.getTime() / 1000), }, true ); diff --git a/geneit_app/src/utils/time_utils.ts b/geneit_app/src/utils/time_utils.ts new file mode 100644 index 0000000..aea8b50 --- /dev/null +++ b/geneit_app/src/utils/time_utils.ts @@ -0,0 +1,6 @@ +/** + * Get formatted UNIX date + */ +export function fmtUnixDate(time: number): string { + return new Date(time * 1000).toLocaleString("fr-FR"); +} diff --git a/geneit_app/src/widgets/forms/PropDateInput.tsx b/geneit_app/src/widgets/forms/PropDateInput.tsx new file mode 100644 index 0000000..914de98 --- /dev/null +++ b/geneit_app/src/widgets/forms/PropDateInput.tsx @@ -0,0 +1,53 @@ +import { LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import dayjs from "dayjs"; +import { fmtUnixDate } from "../../utils/time_utils"; +import { PropEdit } from "./PropEdit"; + +export function PropDateInput(p: { + editable: boolean; + label: string; + value: number | undefined; + onChange: (v: number | undefined) => void; + lastSecOfDay?: boolean; +}): React.ReactElement { + let shiftV = p.value; + if (shiftV && p.lastSecOfDay) { + const d = new Date(shiftV * 1000); + if (d.getHours() === 0) { + shiftV -= 1; + } + } + + if (!p.editable) { + if (!shiftV) return <>; + + return ( + + ); + } + + const value = dayjs( + shiftV && p.value! > 0 ? new Date(shiftV * 1000) : undefined + ); + + return ( +
+ + { + if (v && p.lastSecOfDay) { + v.set("hours", 23); + v.set("minutes", 59); + v.set("seconds", 59); + } + p.onChange?.(v ? v.unix() : undefined); + }} + /> + +
+ ); +} -- 2.45.2 From bc4fb79f8cb9a7566d4bb112ffdd74e3ac4592c1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 18 Jun 2024 23:00:34 +0200 Subject: [PATCH 43/65] Fix bad conditions --- .../dialogs/accommodations/UpdateReservationDialog.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx index 841aae9..7a480e9 100644 --- a/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx @@ -101,9 +101,11 @@ export function UpdateReservationDialog(p: { @@ -104,7 +174,8 @@ export function UpdateReservationDialog(p: { !( (reservation?.accommodation_id ?? -1) > 0 && (reservation?.start ?? -1) > 0 && - (reservation?.end ?? -1) > (reservation?.start ?? 0) + (reservation?.end ?? -1) > (reservation?.start ?? 0) && + (conflicts?.length ?? 0) === 0 ) } > diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 5d979a8..324f9bd 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -74,7 +74,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { return ( <> - + - - { - if (v && p.lastSecOfDay) { - v.set("hours", 23); - v.set("minutes", 59); - v.set("seconds", 59); - } - p.onChange?.(v ? v.unix() : undefined); - }} - minDate={minDate} - maxDate={maxDate} - /> - -
+ + { + if (v && p.lastSecOfDay) { + v.set("hours", 23); + v.set("minutes", 59); + v.set("seconds", 59); + } + p.onChange?.(v ? v.unix() : undefined); + }} + minDate={minDate} + maxDate={maxDate} + /> +
+
); } diff --git a/geneit_backend/src/controllers/accommodations_reservations_controller.rs b/geneit_backend/src/controllers/accommodations_reservations_controller.rs index bda86ca..8c3fba5 100644 --- a/geneit_backend/src/controllers/accommodations_reservations_controller.rs +++ b/geneit_backend/src/controllers/accommodations_reservations_controller.rs @@ -93,6 +93,31 @@ pub async fn get_accommodation_reservations(a: FamilyAndAccommodationInPath) -> .json(accommodations_reservations_service::get_all_of_accommodation(a.id()).await?)) } +#[derive(serde::Deserialize)] +pub struct CheckAvailabilityQuery { + start: usize, + end: usize, +} + +/// Check reservation availability +pub async fn get_accommodation_reservations_for_interval( + a: FamilyAndAccommodationInPath, + req: web::Query, +) -> HttpResult { + if req.start > req.end { + return Ok(HttpResponse::BadRequest().json("start should be smaller than end!")); + } + + let res = accommodations_reservations_service::get_reservations_for_time_interval( + a.id(), + req.start, + req.end, + ) + .await?; + + Ok(HttpResponse::Ok().json(res)) +} + /// Get the full list of accommodations reservations for a family pub async fn full_list(m: FamilyInPath) -> HttpResult { Ok(HttpResponse::Ok() diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index ff815fc..b4b0f74 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -233,6 +233,11 @@ async fn main() -> std::io::Result<()> { web::get() .to(accommodations_reservations_controller::get_accommodation_reservations), ) + .route( + "/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/for_interval", + web::get() + .to(accommodations_reservations_controller::get_accommodation_reservations_for_interval), + ) .route( "/family/{id}/accommodations/reservations/full_list", web::get().to(accommodations_reservations_controller::full_list), -- 2.45.2 From e5f36a3d297790609b72e7a5feb4bb8b66ffaa04 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Jun 2024 22:25:39 +0200 Subject: [PATCH 47/65] Reservation creation is effective from web UI --- .../AccommodationsReservationsApi.tsx | 19 ++++++++ .../UpdateReservationDialog.tsx | 9 ++-- .../AccommodationsReservationsRoute.tsx | 44 ++++++++++++++----- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx index 7c426b9..232ea8b 100644 --- a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx @@ -64,6 +64,25 @@ export interface UpdateAccommodationReservation { } export class AccommodationsReservationsApi { + /** + * Create a new reservation + */ + static async Create( + family: Family, + reservation: UpdateAccommodationReservation + ): Promise { + return ( + await APIClient.exec({ + method: "POST", + uri: `/family/${family.family_id}/accommodations/reservations/accommodation/${reservation.accommodation_id}/create`, + jsonData: { + start: reservation.start, + end: reservation.end, + }, + }) + ).data; + } + /** * Get the entire list of accommodations of a family */ diff --git a/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx index 3ac396a..9485b05 100644 --- a/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx @@ -5,7 +5,6 @@ import { DialogActions, DialogContent, DialogTitle, - Typography, } from "@mui/material"; import React from "react"; import { @@ -14,11 +13,11 @@ import { UpdateAccommodationReservation, } from "../../api/accommodations/AccommodationsReservationsApi"; import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; +import { fmtUnixDate } from "../../utils/time_utils"; import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute"; import { PropDateInput } from "../../widgets/forms/PropDateInput"; import { PropSelect } from "../../widgets/forms/PropSelect"; -import { fmtUnixDate } from "../../utils/time_utils"; export function UpdateReservationDialog(p: { open: boolean; @@ -121,7 +120,11 @@ export function UpdateReservationDialog(p: { return { label: a.name, value: a.id.toString() }; } )} - value={reservation?.accommodation_id?.toString()} + value={ + reservation?.accommodation_id === -1 + ? "" + : reservation?.accommodation_id?.toString() + } /> { - // TODO : render this functional - // TODO : handle busy case - console.info(d); - updateReservation( - { - accommodation_id: -1, - start: Math.floor(d.start.getTime() / 1000), - end: Math.floor(d.end.getTime() / 1000), - }, - true - ); + const onSelect = async (d: DateSelectArg) => { + try { + const resa = await updateReservation( + { + accommodation_id: -1, + start: Math.floor(d.start.getTime() / 1000), + end: Math.floor(d.end.getTime() / 1000), + }, + true + ); + + if (!resa) return; + + loadingMessage.show("Création de la réservation en cours..."); + + await AccommodationsReservationsApi.Create(family.family, resa); + + reload(); + snackbar("La réservation a été créée avec succès !"); + } catch (e) { + console.error("Failed to create a reservation!", e); + alert("Échec de la création de la réservation!"); + } finally { + loadingMessage.hide(); + } }; return ( -- 2.45.2 From 337f6ced5d7ff2c698ec1a91bfaf157220aedf2b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Jun 2024 22:28:40 +0200 Subject: [PATCH 48/65] Add new notice --- .../accommodations/AccommodationsReservationsRoute.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 58f2f52..85c2056 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -5,11 +5,13 @@ import interactionPlugin from "@fullcalendar/interaction"; import listPlugin from "@fullcalendar/list"; import FullCalendar from "@fullcalendar/react"; import { + Alert, Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, + Typography, } from "@mui/material"; import React from "react"; import { FamilyApi, FamilyUser } from "../../../api/FamilyApi"; @@ -101,7 +103,11 @@ export function AccommodationsReservationsRoute(): React.ReactElement { errMsg="Echec du chargement de la liste des réservations !" build={() => (
-
+
+ + Cliquez sur le calendrier pour créer une réservation. + + {/* Invitation status */} Date: Thu, 20 Jun 2024 21:46:45 +0200 Subject: [PATCH 49/65] Add color to accommodations --- geneit_app/package-lock.json | 30 +++++++++++++++++++ geneit_app/package.json | 1 + .../accommodations/AccommodationListApi.tsx | 4 ++- .../UpdateAccommodationDialog.tsx | 15 ++++++++++ .../AccommodationsReservationsRoute.tsx | 6 ++++ .../AccommodationsSettingsRoute.tsx | 3 ++ .../src/widgets/forms/PropColorPicker.tsx | 24 +++++++++++++++ geneit_backend/Cargo.lock | 24 +++++++++++++++ geneit_backend/Cargo.toml | 1 + .../up.sql | 1 + .../accommodations_list_controller.rs | 22 ++++++++++---- geneit_backend/src/models.rs | 1 + geneit_backend/src/schema.rs | 2 ++ .../services/accommodations_list_service.rs | 1 + 14 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 geneit_app/src/widgets/forms/PropColorPicker.tsx diff --git a/geneit_app/package-lock.json b/geneit_app/package-lock.json index 8622b02..01a852e 100644 --- a/geneit_app/package-lock.json +++ b/geneit_app/package-lock.json @@ -37,6 +37,7 @@ "email-validator": "^2.0.4", "filesize": "^10.1.2", "jspdf": "^2.5.1", + "mui-color-input": "^2.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-easy-crop": "^5.0.7", @@ -519,6 +520,14 @@ "node": ">=6.9.0" } }, + "node_modules/@ctrl/tinycolor": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz", + "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==", + "engines": { + "node": ">=14" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -3455,6 +3464,27 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/mui-color-input": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-2.0.3.tgz", + "integrity": "sha512-rAd040qQ0Y+8dk4gE8kkCiJ/vCgA0j4vv1quJ43BfORTFE3uHarHj0xY1Vo9CPbojtx1f5vW+CjckYPRIZPIRg==", + "dependencies": { + "@ctrl/tinycolor": "^4.0.3" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": "^5.0.0", + "@types/react": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", diff --git a/geneit_app/package.json b/geneit_app/package.json index f7872ec..c9a3f05 100644 --- a/geneit_app/package.json +++ b/geneit_app/package.json @@ -33,6 +33,7 @@ "email-validator": "^2.0.4", "filesize": "^10.1.2", "jspdf": "^2.5.1", + "mui-color-input": "^2.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-easy-crop": "^5.0.7", diff --git a/geneit_app/src/api/accommodations/AccommodationListApi.tsx b/geneit_app/src/api/accommodations/AccommodationListApi.tsx index e9a595d..9a98fed 100644 --- a/geneit_app/src/api/accommodations/AccommodationListApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationListApi.tsx @@ -8,7 +8,8 @@ export interface Accommodation { time_update: number; name: string; need_validation: boolean; - description: string; + description?: string; + color?: string; open_to_reservations: boolean; } @@ -58,6 +59,7 @@ export interface UpdateAccommodation { name: string; need_validation: boolean; description?: string; + color?: string; open_to_reservations: boolean; } diff --git a/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx index 3ffaf1a..a6d720e 100644 --- a/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx @@ -12,6 +12,7 @@ import { UpdateAccommodation } from "../../api/accommodations/AccommodationListA import { checkConstraint } from "../../utils/from_utils"; import { PropCheckbox } from "../../widgets/forms/PropCheckbox"; import { PropEdit } from "../../widgets/forms/PropEdit"; +import { PropColorPicker } from "../../widgets/forms/PropColorPicker"; export function UpdateAccommodationDialog(p: { open: boolean; @@ -89,6 +90,20 @@ export function UpdateAccommodationDialog(p: { helperText={descriptionErr} /> + + setAccommodation((a) => { + return { + ...a!, + color: s!, + }; + }) + } + /> + { if (v) hiddenAccommodations.delete(a.id); diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index eb7f187..cb26ab1 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -1,6 +1,7 @@ import AddIcon from "@mui/icons-material/Add"; import CheckIcon from "@mui/icons-material/Check"; import CloseIcon from "@mui/icons-material/Close"; +import HouseIcon from "@mui/icons-material/House"; import { Button, Card, @@ -62,6 +63,7 @@ function AccommodationsListCard(): React.ReactElement { name: "", open_to_reservations: true, need_validation: false, + color: "2196f3", }, true ); @@ -178,6 +180,7 @@ function AccommodationCard(p: { Mis à jour il y a + {" "} {p.accommodation.name} diff --git a/geneit_app/src/widgets/forms/PropColorPicker.tsx b/geneit_app/src/widgets/forms/PropColorPicker.tsx new file mode 100644 index 0000000..11205b6 --- /dev/null +++ b/geneit_app/src/widgets/forms/PropColorPicker.tsx @@ -0,0 +1,24 @@ +import { MuiColorInput } from "mui-color-input"; +import { PropEdit } from "./PropEdit"; + +export function PropColorPicker(p: { + editable: boolean; + label: string; + value?: string; + onChange: (v: string | undefined) => void; +}): React.ReactElement { + if (!p.editable) { + if (!p.value) return <>; + + return ; + } + + return ( + p.onChange(c.hex.substring(1))} + /> + ); +} diff --git a/geneit_backend/Cargo.lock b/geneit_backend/Cargo.lock index b0851f1..ce9b2bb 100644 --- a/geneit_backend/Cargo.lock +++ b/geneit_backend/Cargo.lock @@ -1415,6 +1415,7 @@ dependencies = [ "httpdate", "ical", "image", + "lazy-regex", "lazy_static", "lettre", "light-openid", @@ -1952,6 +1953,29 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy-regex" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.63", +] + [[package]] name = "lazy_static" version = "1.4.0" diff --git a/geneit_backend/Cargo.toml b/geneit_backend/Cargo.toml index 13b4c0f..e7585ed 100644 --- a/geneit_backend/Cargo.toml +++ b/geneit_backend/Cargo.toml @@ -10,6 +10,7 @@ log = "0.4.21" env_logger = "0.11.3" clap = { version = "4.5.4", features = ["derive", "env"] } lazy_static = "1.4.0" +lazy-regex = "3.1.0" anyhow = "1.0.83" actix-web = "4.5.1" actix-cors = "0.7.0" diff --git a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql index 79d45ec..42d2dd8 100644 --- a/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql +++ b/geneit_backend/migrations/2024-05-23-163128_accommodation_module/up.sql @@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS accommodations_list name VARCHAR(50) NOT NULL, need_validation BOOLEAN NOT NULL DEFAULT true, description text NULL, + color VARCHAR(6) NULL, open_to_reservations BOOLEAN NOT NULL DEFAULT false ); diff --git a/geneit_backend/src/controllers/accommodations_list_controller.rs b/geneit_backend/src/controllers/accommodations_list_controller.rs index 6d0028f..cf640ce 100644 --- a/geneit_backend/src/controllers/accommodations_list_controller.rs +++ b/geneit_backend/src/controllers/accommodations_list_controller.rs @@ -8,10 +8,12 @@ use actix_web::{web, HttpResponse}; #[derive(thiserror::Error, Debug)] enum AccommodationListControllerErr { - #[error("Malformed name!")] - MalformedName, - #[error("Malformed description!")] - MalformedDescription, + #[error("Invalid name length!")] + InvalidNameLength, + #[error("Invalid description length!")] + InvalidDescriptionLength, + #[error("Malformed color!")] + MalformedColor, } #[derive(serde::Deserialize, Clone)] @@ -19,6 +21,7 @@ pub struct AccommodationRequest { pub name: String, pub need_validation: bool, pub description: Option, + pub color: Option, pub open_to_reservations: bool, } @@ -27,17 +30,24 @@ impl AccommodationRequest { let c = StaticConstraints::default(); if !c.accommodation_name_len.validate(&self.name) { - return Err(AccommodationListControllerErr::MalformedName.into()); + return Err(AccommodationListControllerErr::InvalidNameLength.into()); } accommodation.name = self.name; if let Some(d) = &self.description { if !c.accommodation_description_len.validate(d) { - return Err(AccommodationListControllerErr::MalformedDescription.into()); + return Err(AccommodationListControllerErr::InvalidDescriptionLength.into()); } } accommodation.description.clone_from(&self.description); + if let Some(c) = &self.color { + if !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c) { + return Err(AccommodationListControllerErr::MalformedColor.into()); + } + } + accommodation.color.clone_from(&self.color); + accommodation.need_validation = self.need_validation; accommodation.open_to_reservations = self.open_to_reservations; Ok(()) diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 720a39d..65389d2 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -459,6 +459,7 @@ pub struct Accommodation { pub name: String, pub need_validation: bool, pub description: Option, + pub color: Option, pub open_to_reservations: bool, } diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index 6f6200d..655f46f 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -10,6 +10,8 @@ diesel::table! { name -> Varchar, need_validation -> Bool, description -> Nullable, + #[max_length = 6] + color -> Nullable, open_to_reservations -> Bool, } } diff --git a/geneit_backend/src/services/accommodations_list_service.rs b/geneit_backend/src/services/accommodations_list_service.rs index 6d44bc0..ba0f7e8 100644 --- a/geneit_backend/src/services/accommodations_list_service.rs +++ b/geneit_backend/src/services/accommodations_list_service.rs @@ -71,6 +71,7 @@ pub async fn update(accommodation: &mut Accommodation) -> anyhow::Result<()> { accommodations_list::dsl::name.eq(accommodation.name.to_string()), accommodations_list::dsl::need_validation.eq(accommodation.need_validation), accommodations_list::dsl::description.eq(accommodation.description.clone()), + accommodations_list::dsl::color.eq(accommodation.color.clone()), accommodations_list::dsl::open_to_reservations.eq(accommodation.open_to_reservations), )) .execute(conn) -- 2.45.2 From a7016f47827a0985d0e42770c0ee33dee314d4f0 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Jun 2024 22:15:14 +0200 Subject: [PATCH 50/65] Display the reservations --- .../AccommodationsReservationsRoute.tsx | 43 +++++++++++++++++-- geneit_app/src/utils/time_utils.ts | 20 +++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index d91d46b..465875b 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -11,7 +11,6 @@ import { FormControlLabel, FormGroup, FormLabel, - Typography, } from "@mui/material"; import React from "react"; import { FamilyApi, FamilyUser } from "../../../api/FamilyApi"; @@ -23,6 +22,7 @@ import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider"; import { useUpdateAccommodationReservation } from "../../../hooks/context_providers/accommodations/UpdateReservationDialogProvider"; +import { fmtUnixDateFullCalendar } from "../../../utils/time_utils"; import { AsyncWidget } from "../../../widgets/AsyncWidget"; import { useFamily } from "../../../widgets/BaseFamilyRoute"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; @@ -67,6 +67,24 @@ export function AccommodationsReservationsRoute(): React.ReactElement { setUsers(null); }; + const visibleReservations = React.useMemo(() => { + return reservations?.filter((r) => { + if (!showValidated && r.validated === true) return false; + if (!showPending && r.validated === null) return false; + if (!showRejected && r.validated === false) return false; + if (hiddenPeople.has(r.user_id)) return false; + if (hiddenAccommodations.has(r.accommodation_id)) return false; + return true; + }); + }, [ + showValidated, + showRejected, + showPending, + hiddenPeople, + hiddenAccommodations, + reservations, + ]); + const onSelect = async (d: DateSelectArg) => { try { const resa = await updateReservation( @@ -123,7 +141,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { onChange={(_ev, v) => setShowValidated(v)} /> } - label="Validé" + label="Validées" /> setShowRejected(v)} /> } - label="Rejetés" + label="Rejetées" /> { + const a = accommodations.accommodations.get( + r.accommodation_id + )!; + const u = users?.find((u) => u.user_id === r.user_id); + return { + title: `${u?.user_name} - ${a.name}`, + start: fmtUnixDateFullCalendar(r.reservation_start), + end: fmtUnixDateFullCalendar(r.reservation_end), + allDay: true, + color: a.color ? "#" + a.color : undefined, + borderColor: + r.validated === true + ? "green" + : r.validated === false + ? "red dotted" + : "grey dotted", + }; + })} />
diff --git a/geneit_app/src/utils/time_utils.ts b/geneit_app/src/utils/time_utils.ts index aea8b50..26496da 100644 --- a/geneit_app/src/utils/time_utils.ts +++ b/geneit_app/src/utils/time_utils.ts @@ -4,3 +4,23 @@ export function fmtUnixDate(time: number): string { return new Date(time * 1000).toLocaleString("fr-FR"); } + +/** + * Get formatted UNIX date for Full Calendar + */ +export function fmtUnixDateFullCalendar(time: number): string { + const d = new Date(time * 1000); + + const s = `${d.getFullYear()}-${(d.getMonth() + 1) + .toString(10) + .padStart(2, "0")}-${d.getDate().toString(10).padStart(2, "0")}T${d + .getHours() + .toString(10) + .padStart(2, "0")}:${d.getMinutes().toString(10).padStart(2, "0")}:${d + .getSeconds() + .toString(10) + .padStart(2, "0")}`; + + console.info(d, s); + return s; +} -- 2.45.2 From b4a360c4922a08dec38bfa75c5d403fd3bc90165 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Jun 2024 22:45:34 +0200 Subject: [PATCH 51/65] Can middle day reservations --- .../UpdateReservationDialog.tsx | 2 + .../AccommodationsReservationsRoute.tsx | 6 +- geneit_app/src/utils/time_utils.ts | 1 - .../src/widgets/forms/PropDateInput.tsx | 76 +++++++++++++++---- 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx index 9485b05..b304157 100644 --- a/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/UpdateReservationDialog.tsx @@ -137,6 +137,7 @@ export function UpdateReservationDialog(p: { }); }} minDate={Math.floor(new Date().getTime() / 1000) - 3600 * 24 * 60} + canSetMiddleDay /> {conflicts && conflicts.length > 0 && ( diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 465875b..5f1099a 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -139,6 +139,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { setShowValidated(v)} + color="success" /> } label="Validées" @@ -148,6 +149,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { setShowRejected(v)} + color="error" /> } label="Rejetées" @@ -157,6 +159,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { setShowPending(v)} + color="info" /> } label="En attente de validation" @@ -239,7 +242,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { headerToolbar={{ left: "prev,next today", center: "title", - right: "dayGridMonth,dayGridWeek,listWeek", + right: "dayGridMonth,dayGridWeek,dayGridDay,listWeek", }} select={onSelect} events={visibleReservations?.map((r) => { @@ -251,7 +254,6 @@ export function AccommodationsReservationsRoute(): React.ReactElement { title: `${u?.user_name} - ${a.name}`, start: fmtUnixDateFullCalendar(r.reservation_start), end: fmtUnixDateFullCalendar(r.reservation_end), - allDay: true, color: a.color ? "#" + a.color : undefined, borderColor: r.validated === true diff --git a/geneit_app/src/utils/time_utils.ts b/geneit_app/src/utils/time_utils.ts index 26496da..56a3f1b 100644 --- a/geneit_app/src/utils/time_utils.ts +++ b/geneit_app/src/utils/time_utils.ts @@ -21,6 +21,5 @@ export function fmtUnixDateFullCalendar(time: number): string { .toString(10) .padStart(2, "0")}`; - console.info(d, s); return s; } diff --git a/geneit_app/src/widgets/forms/PropDateInput.tsx b/geneit_app/src/widgets/forms/PropDateInput.tsx index da8fefc..46c457a 100644 --- a/geneit_app/src/widgets/forms/PropDateInput.tsx +++ b/geneit_app/src/widgets/forms/PropDateInput.tsx @@ -5,6 +5,7 @@ import dayjs from "dayjs"; import "dayjs/locale/fr"; import { fmtUnixDate } from "../../utils/time_utils"; import { PropEdit } from "./PropEdit"; +import { Checkbox, FormControlLabel } from "@mui/material"; export function PropDateInput(p: { editable: boolean; @@ -14,7 +15,17 @@ export function PropDateInput(p: { lastSecOfDay?: boolean; minDate?: number; maxDate?: number; + canSetMiddleDay?: boolean; }): React.ReactElement { + // Check for mid-day value + let isMidDay = false; + if (p.value) { + const d = new Date(p.value * 1000); + isMidDay = + d.getHours() === 12 && d.getMinutes() === 0 && d.getSeconds() === 0; + } + + // Shift value let shiftV = p.value; if (shiftV && p.lastSecOfDay) { const d = new Date(shiftV * 1000); @@ -39,22 +50,55 @@ export function PropDateInput(p: { const maxDate = p.maxDate ? dayjs(new Date(p.maxDate * 1000)) : undefined; return ( - - { - if (v && p.lastSecOfDay) { - v.set("hours", 23); - v.set("minutes", 59); - v.set("seconds", 59); - } - p.onChange?.(v ? v.unix() : undefined); - }} - minDate={minDate} - maxDate={maxDate} - /> + <>
-
+ + { + if (v && p.lastSecOfDay) { + v = v.set("hours", 23); + v = v.set("minutes", 59); + v = v.set("seconds", 59); + } + p.onChange?.(v ? v.unix() : undefined); + }} + minDate={minDate} + maxDate={maxDate} + /> + + {p.canSetMiddleDay && ( + { + let v = value; + if (midDay) { + v = v.set("hours", 12); + v = v.set("minutes", 0); + v = v.set("seconds", 0); + } else if (p.lastSecOfDay) { + v = v.set("hours", 23); + v = v.set("minutes", 59); + v = v.set("seconds", 59); + } else { + v = v.set("hours", 0); + v = v.set("minutes", 0); + v = v.set("seconds", 0); + } + console.log(midDay, v, v.get("hours")); + + p.onChange(v.unix()); + }} + /> + } + label="Mi-journée" + /> + )} +
+ ); } -- 2.45.2 From 1305667f24e73d6996df115d64f0a54f297abae0 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Jun 2024 22:57:44 +0200 Subject: [PATCH 52/65] Fix date issues --- .../AccommodationsReservationsRoute.tsx | 5 +++-- geneit_app/src/utils/time_utils.ts | 15 +++++++++++---- geneit_app/src/widgets/forms/PropDateInput.tsx | 1 - 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 5f1099a..b47e03b 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -252,8 +252,9 @@ export function AccommodationsReservationsRoute(): React.ReactElement { const u = users?.find((u) => u.user_id === r.user_id); return { title: `${u?.user_name} - ${a.name}`, - start: fmtUnixDateFullCalendar(r.reservation_start), - end: fmtUnixDateFullCalendar(r.reservation_end), + start: fmtUnixDateFullCalendar(r.reservation_start, false), + end: fmtUnixDateFullCalendar(r.reservation_end, true), + allDay: true, color: a.color ? "#" + a.color : undefined, borderColor: r.validated === true diff --git a/geneit_app/src/utils/time_utils.ts b/geneit_app/src/utils/time_utils.ts index 56a3f1b..8b22197 100644 --- a/geneit_app/src/utils/time_utils.ts +++ b/geneit_app/src/utils/time_utils.ts @@ -8,18 +8,25 @@ export function fmtUnixDate(time: number): string { /** * Get formatted UNIX date for Full Calendar */ -export function fmtUnixDateFullCalendar(time: number): string { - const d = new Date(time * 1000); +export function fmtUnixDateFullCalendar( + time: number, + correctEnd: boolean +): string { + let d = new Date(time * 1000); + + if (d.getHours() > 0 && correctEnd) + d = new Date(time * 1000 + 3600 * 24 * 1000); const s = `${d.getFullYear()}-${(d.getMonth() + 1) .toString(10) - .padStart(2, "0")}-${d.getDate().toString(10).padStart(2, "0")}T${d + .padStart(2, "0")}-${d.getDate().toString(10).padStart(2, "0")}`; /*T${d .getHours() .toString(10) .padStart(2, "0")}:${d.getMinutes().toString(10).padStart(2, "0")}:${d .getSeconds() .toString(10) - .padStart(2, "0")}`; + .padStart(2, "0")}`*/ + console.log(s, d); return s; } diff --git a/geneit_app/src/widgets/forms/PropDateInput.tsx b/geneit_app/src/widgets/forms/PropDateInput.tsx index 46c457a..faafa8f 100644 --- a/geneit_app/src/widgets/forms/PropDateInput.tsx +++ b/geneit_app/src/widgets/forms/PropDateInput.tsx @@ -89,7 +89,6 @@ export function PropDateInput(p: { v = v.set("minutes", 0); v = v.set("seconds", 0); } - console.log(midDay, v, v.get("hours")); p.onChange(v.unix()); }} -- 2.45.2 From 30cca548c245022a8d4dc5a13fb417a7209e058d Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Jun 2024 23:06:10 +0200 Subject: [PATCH 53/65] Prepare next session --- .../accommodations/AccommodationsReservationsRoute.tsx | 8 +++++++- geneit_app/src/utils/time_utils.ts | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index b47e03b..2d7bfc2 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -1,4 +1,4 @@ -import { DateSelectArg } from "@fullcalendar/core"; +import { DateSelectArg, EventClickArg } from "@fullcalendar/core"; import frLocale from "@fullcalendar/core/locales/fr"; import dayGridPlugin from "@fullcalendar/daygrid"; import interactionPlugin from "@fullcalendar/interaction"; @@ -27,6 +27,7 @@ import { AsyncWidget } from "../../../widgets/AsyncWidget"; import { useFamily } from "../../../widgets/BaseFamilyRoute"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; +import { EventImpl } from "@fullcalendar/core/internal"; export function AccommodationsReservationsRoute(): React.ReactElement { const snackbar = useSnackbar(); @@ -112,6 +113,10 @@ export function AccommodationsReservationsRoute(): React.ReactElement { } }; + const onEventClick = (ev: EventClickArg) => { + console.log(ev); + }; + return ( <> @@ -245,6 +250,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { right: "dayGridMonth,dayGridWeek,dayGridDay,listWeek", }} select={onSelect} + eventClick={onEventClick} events={visibleReservations?.map((r) => { const a = accommodations.accommodations.get( r.accommodation_id diff --git a/geneit_app/src/utils/time_utils.ts b/geneit_app/src/utils/time_utils.ts index 8b22197..aacde37 100644 --- a/geneit_app/src/utils/time_utils.ts +++ b/geneit_app/src/utils/time_utils.ts @@ -27,6 +27,5 @@ export function fmtUnixDateFullCalendar( .toString(10) .padStart(2, "0")}`*/ - console.log(s, d); return s; } -- 2.45.2 From ac9bf00fa4986167d801ae483d6e4f283f1549b1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Jun 2024 23:07:36 +0200 Subject: [PATCH 54/65] Update --- .../family/accommodations/AccommodationsReservationsRoute.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 2d7bfc2..669d5c7 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -27,7 +27,6 @@ import { AsyncWidget } from "../../../widgets/AsyncWidget"; import { useFamily } from "../../../widgets/BaseFamilyRoute"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; -import { EventImpl } from "@fullcalendar/core/internal"; export function AccommodationsReservationsRoute(): React.ReactElement { const snackbar = useSnackbar(); -- 2.45.2 From 71abecae4ee2113da8f108aecfcfde93b70b10ff Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Jun 2024 18:30:33 +0200 Subject: [PATCH 55/65] Prepare calendar popover --- .../AccommodationsReservationsRoute.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 669d5c7..20b3e4f 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -11,6 +11,9 @@ import { FormControlLabel, FormGroup, FormLabel, + Popover, + Typography, + fabClasses, } from "@mui/material"; import React from "react"; import { FamilyApi, FamilyUser } from "../../../api/FamilyApi"; @@ -55,6 +58,17 @@ export function AccommodationsReservationsRoute(): React.ReactElement { Set >(new Set()); + const eventPopupAnchor = React.useRef(null); + const [activeEvent, setActiveEvent] = React.useState< + | undefined + | { + x: number; + y: number; + w: number; + h: number; + } + >(); + const load = async () => { setReservations( await AccommodationsReservationsApi.FullListOfFamily(family.family) @@ -113,6 +127,8 @@ export function AccommodationsReservationsRoute(): React.ReactElement { }; const onEventClick = (ev: EventClickArg) => { + const loc = ev.el.getBoundingClientRect(); + setActiveEvent({ x: loc.left, y: loc.top, w: loc.width, h: loc.height }); console.log(ev); }; @@ -271,6 +287,33 @@ export function AccommodationsReservationsRoute(): React.ReactElement { })} />
+ + {/* Calendar event popover */} +
+ { + setActiveEvent(undefined); + }} + anchorOrigin={{ + vertical: "bottom", + horizontal: "left", + }} + > + The content of the Popover. +
)} /> -- 2.45.2 From ac8ff918b1d46afa2caf164b4c636d6c1526e583 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Jun 2024 19:13:01 +0200 Subject: [PATCH 56/65] Can delete a reservation --- .../AccommodationsReservationsApi.tsx | 10 ++ .../AccommodationsReservationsRoute.tsx | 138 +++++++++++++++++- 2 files changed, 143 insertions(+), 5 deletions(-) diff --git a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx index 232ea8b..3cb5a1a 100644 --- a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx @@ -117,4 +117,14 @@ export class AccommodationsReservationsApi { return new AccommodationsReservationsList(data); } + + /** + * Delete a reservation + */ + static async Delete(r: AccommodationReservation): Promise { + await APIClient.exec({ + method: "DELETE", + uri: `/family/${r.family_id}/accommodations/reservation/${r.id}`, + }); + } } diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 20b3e4f..9d99e99 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -4,29 +4,44 @@ import dayGridPlugin from "@fullcalendar/daygrid"; import interactionPlugin from "@fullcalendar/interaction"; import listPlugin from "@fullcalendar/list"; import FullCalendar from "@fullcalendar/react"; +import DeleteIcon from "@mui/icons-material/Delete"; import { Alert, + Avatar, + Card, + CardActions, + CardContent, + CardHeader, Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, + IconButton, Popover, + Tooltip, Typography, - fabClasses, } from "@mui/material"; +import { red } from "@mui/material/colors"; import React from "react"; import { FamilyApi, FamilyUser } from "../../../api/FamilyApi"; +import { Accommodation } from "../../../api/accommodations/AccommodationListApi"; import { + AccommodationReservation, AccommodationsReservationsApi, AccommodationsReservationsList, } from "../../../api/accommodations/AccommodationsReservationsApi"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; +import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider"; import { useUpdateAccommodationReservation } from "../../../hooks/context_providers/accommodations/UpdateReservationDialogProvider"; -import { fmtUnixDateFullCalendar } from "../../../utils/time_utils"; +import { + fmtUnixDate, + fmtUnixDateFullCalendar, +} from "../../../utils/time_utils"; import { AsyncWidget } from "../../../widgets/AsyncWidget"; +import { useUser } from "../../../widgets/BaseAuthenticatedPage"; import { useFamily } from "../../../widgets/BaseFamilyRoute"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; @@ -34,10 +49,12 @@ import { useAccommodations } from "../../../widgets/accommodations/BaseAccommoda export function AccommodationsReservationsRoute(): React.ReactElement { const snackbar = useSnackbar(); const alert = useAlert(); + const confirm = useConfirm(); const loadingMessage = useLoadingMessage(); const loadKey = React.useRef(1); + const user = useUser(); const family = useFamily(); const accommodations = useAccommodations(); const updateReservation = useUpdateAccommodationReservation(); @@ -62,6 +79,10 @@ export function AccommodationsReservationsRoute(): React.ReactElement { const [activeEvent, setActiveEvent] = React.useState< | undefined | { + user: FamilyUser; + accommodation: Accommodation; + reservation: AccommodationReservation; + x: number; y: number; w: number; @@ -127,9 +148,52 @@ export function AccommodationsReservationsRoute(): React.ReactElement { }; const onEventClick = (ev: EventClickArg) => { + const id: number = ev.event.extendedProps.id; + const resa = reservations?.get(id)!; + const acc = accommodations.accommodations.get(resa.accommodation_id)!; + + const user = users?.find((u) => u.user_id === resa.user_id); + + if (!user) { + console.error(`User ${resa.user_id} not found!`); + return; + } + const loc = ev.el.getBoundingClientRect(); - setActiveEvent({ x: loc.left, y: loc.top, w: loc.width, h: loc.height }); - console.log(ev); + setActiveEvent({ + reservation: resa, + accommodation: acc, + user: user, + + x: loc.left, + y: loc.top, + w: loc.width, + h: loc.height, + }); + }; + + const deleteReservation = async (r: AccommodationReservation) => { + try { + if ( + !(await confirm( + "Voulez-vous vraiment supprimer cette réservation ? L'opération n'est pas réversible !" + )) + ) + return; + + setActiveEvent(undefined); + loadingMessage.show("Suppression de la réservation en cours..."); + + await AccommodationsReservationsApi.Delete(r); + + reload(); + snackbar("La réservation a été supprimée avec succès !"); + } catch (e) { + console.error("Failed to delete a reservation!", e); + alert("Échec de la suppression de la réservation!"); + } finally { + loadingMessage.hide(); + } }; return ( @@ -283,6 +347,9 @@ export function AccommodationsReservationsRoute(): React.ReactElement { : r.validated === false ? "red dotted" : "grey dotted", + extendedProps: { + id: r.id, + }, }; })} /> @@ -299,6 +366,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement { width: activeEvent?.w + "px", height: activeEvent?.h + "px", backgroundColor: "pink", + zIndex: 0, }} >
- The content of the Popover. + + + {activeEvent?.user.user_name + .substring(0, 1) + .toLocaleUpperCase()} + + } + title={activeEvent?.user.user_name} + subheader={activeEvent?.user.user_mail} + /> + + + +

+ Réservation de {activeEvent?.accommodation.name} +
+ {activeEvent?.accommodation.description} +

+

+ Du{" "} + {fmtUnixDate( + activeEvent?.reservation.reservation_start ?? 0 + )}{" "} +
+ Au{" "} + {fmtUnixDate( + activeEvent?.reservation.reservation_end ?? 0 + )} +

+

+ + {activeEvent?.reservation.validated === false ? ( + Refusée + ) : activeEvent?.reservation.validated === true ? ( + Validée + ) : ( + + En attente de validation + + )} + +

+
+
+ + {user.user.id === activeEvent?.reservation.user_id && ( + + + deleteReservation(activeEvent?.reservation) + } + > + + + + )} + +
)} -- 2.45.2 From bd3e8df4aa980e8b80aa88ea24cc434ce7bbee38 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Jun 2024 20:01:43 +0200 Subject: [PATCH 57/65] Can change a reservation's dates --- .../AccommodationsReservationsApi.tsx | 17 +++++ .../AccommodationsReservationsRoute.tsx | 70 ++++++++++++++++--- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx index 3cb5a1a..20801f9 100644 --- a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx @@ -118,6 +118,23 @@ export class AccommodationsReservationsApi { return new AccommodationsReservationsList(data); } + /** + * Update a reservation + */ + static async Update( + family: Family, + r: UpdateAccommodationReservation + ): Promise { + await APIClient.exec({ + method: "PATCH", + uri: `/family/${family.family_id}/accommodations/reservation/${r.reservation_id}`, + jsonData: { + start: r.start, + end: r.end, + }, + }); + } + /** * Delete a reservation */ diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 9d99e99..f36211c 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -5,6 +5,7 @@ import interactionPlugin from "@fullcalendar/interaction"; import listPlugin from "@fullcalendar/list"; import FullCalendar from "@fullcalendar/react"; import DeleteIcon from "@mui/icons-material/Delete"; +import EditIcon from "@mui/icons-material/Edit"; import { Alert, Avatar, @@ -172,6 +173,44 @@ export function AccommodationsReservationsRoute(): React.ReactElement { }); }; + const changeReservation = async (r: AccommodationReservation) => { + try { + const ac = accommodations.accommodations.get(r.accommodation_id); + if ( + ac?.need_validation && + !(await confirm( + "Voulez-vous vraiment changer cette réservation ? Celle-ci devra être de nouveau validée !" + )) + ) + return; + + const newResa = await updateReservation( + { + reservation_id: r.id, + accommodation_id: r.accommodation_id, + start: r.reservation_start, + end: r.reservation_end, + }, + false + ); + + if (!newResa) return; + + setActiveEvent(undefined); + loadingMessage.show("Mise à jour de la réservation en cours..."); + + await AccommodationsReservationsApi.Update(family.family, newResa); + + reload(); + snackbar("La réservation a été mise à jour avec succès !"); + } catch (e) { + console.error("Failed to update a reservation!", e); + alert("Échec de la mise à jour de la réservation!"); + } finally { + loadingMessage.hide(); + } + }; + const deleteReservation = async (r: AccommodationReservation) => { try { if ( @@ -428,16 +467,27 @@ export function AccommodationsReservationsRoute(): React.ReactElement { {user.user.id === activeEvent?.reservation.user_id && ( - - - deleteReservation(activeEvent?.reservation) - } - > - - - + <> + + + changeReservation(activeEvent?.reservation) + } + > + + + + + + deleteReservation(activeEvent?.reservation) + } + > + + + + )}
-- 2.45.2 From 24e1229baf16eb9aaca8cb796b19834e885346af Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Jun 2024 20:09:57 +0200 Subject: [PATCH 58/65] Add a button to validate or reject the reservation --- .../AccommodationsReservationsRoute.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index f36211c..9410839 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -6,6 +6,7 @@ import listPlugin from "@fullcalendar/list"; import FullCalendar from "@fullcalendar/react"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; +import RuleIcon from "@mui/icons-material/Rule"; import { Alert, Avatar, @@ -173,6 +174,10 @@ export function AccommodationsReservationsRoute(): React.ReactElement { }); }; + const validateReservation = async (r: AccommodationReservation) => { + // TODO + }; + const changeReservation = async (r: AccommodationReservation) => { try { const ac = accommodations.accommodations.get(r.accommodation_id); @@ -466,9 +471,24 @@ export function AccommodationsReservationsRoute(): React.ReactElement { + {activeEvent?.accommodation.need_validation && + family.family.is_admin && ( + + + validateReservation(activeEvent?.reservation) + } + > + + + + )} {user.user.id === activeEvent?.reservation.user_id && ( <> - + changeReservation(activeEvent?.reservation) -- 2.45.2 From f6d8d6b3d18ffea6f90d92178a59ba4bc2c439cb Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 22 Jun 2024 08:57:33 +0200 Subject: [PATCH 59/65] Can respond to reservation requests --- .../AccommodationsReservationsApi.tsx | 16 ++++ .../AccommodationsReservationsRoute.tsx | 88 ++++++++++++++++--- 2 files changed, 92 insertions(+), 12 deletions(-) diff --git a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx index 20801f9..8b3c15f 100644 --- a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx @@ -144,4 +144,20 @@ export class AccommodationsReservationsApi { uri: `/family/${r.family_id}/accommodations/reservation/${r.id}`, }); } + + /** + * Validate or reject a reservation request + */ + static async Validate( + r: AccommodationReservation, + accept: boolean + ): Promise { + await APIClient.exec({ + method: "POST", + uri: `/family/${r.family_id}/accommodations/reservation/${r.id}/validate`, + jsonData: { + validate: accept, + }, + }); + } } diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 9410839..531464a 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -20,6 +20,8 @@ import { FormGroup, FormLabel, IconButton, + Menu, + MenuItem, Popover, Tooltip, Typography, @@ -92,6 +94,9 @@ export function AccommodationsReservationsRoute(): React.ReactElement { } >(); + const [validateResaAnchorEl, setValidateResaAnchorEl] = + React.useState(null); + const load = async () => { setReservations( await AccommodationsReservationsApi.FullListOfFamily(family.family) @@ -174,8 +179,39 @@ export function AccommodationsReservationsRoute(): React.ReactElement { }); }; + const respondToResaRequest = async ( + r: AccommodationReservation, + validate: boolean + ) => { + try { + loadingMessage.show("Validation de la réservation en cours..."); + + setActiveEvent(undefined); + + await AccommodationsReservationsApi.Validate(r, validate); + + reload(); + snackbar("La réservation a été mise à jour avec succès !"); + } catch (e) { + console.error("Failed to respond to reservation request!", e); + alert(`Echec de l'enregistrement de la réponse à la réservation ! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + const validateReservation = async (r: AccommodationReservation) => { - // TODO + respondToResaRequest(r, true); + }; + + const rejectReservation = async (r: AccommodationReservation) => { + if ( + !(await confirm( + "Voulez-vous vraiment rejeter cette demande de réservation ?" + )) + ) + return; + respondToResaRequest(r, false); }; const changeReservation = async (r: AccommodationReservation) => { @@ -473,18 +509,46 @@ export function AccommodationsReservationsRoute(): React.ReactElement { {activeEvent?.accommodation.need_validation && family.family.is_admin && ( - - - validateReservation(activeEvent?.reservation) - } + <> + - - - + + setValidateResaAnchorEl(e.currentTarget) + } + > + + + + setValidateResaAnchorEl(null)} + > + + validateReservation(activeEvent.reservation) + } + > + Valider + + + rejectReservation(activeEvent.reservation) + } + > + Rejeter + + + )} {user.user.id === activeEvent?.reservation.user_id && ( <> -- 2.45.2 From 136ed8121eb2b014ed36f44707747399211380a6 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 22 Jun 2024 09:03:00 +0200 Subject: [PATCH 60/65] Proper handling of reservation validation errors --- .../AccommodationsReservationsApi.tsx | 16 ++++++++++++++-- .../AccommodationsReservationsRoute.tsx | 12 +++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx index 8b3c15f..23ea118 100644 --- a/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationsReservationsApi.tsx @@ -2,6 +2,12 @@ import { APIClient } from "../ApiClient"; import { Family } from "../FamilyApi"; import { Accommodation } from "./AccommodationListApi"; +export enum ValidateResaResult { + Success, + Error, + Conflict, +} + export interface AccommodationReservation { id: number; family_id: number; @@ -151,13 +157,19 @@ export class AccommodationsReservationsApi { static async Validate( r: AccommodationReservation, accept: boolean - ): Promise { - await APIClient.exec({ + ): Promise { + const res = await APIClient.exec({ method: "POST", uri: `/family/${r.family_id}/accommodations/reservation/${r.id}/validate`, jsonData: { validate: accept, }, + allowFail: true, }); + + if (res.status >= 200 && res.status <= 299) + return ValidateResaResult.Success; + if (res.status === 409) return ValidateResaResult.Conflict; + return ValidateResaResult.Error; } } diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 531464a..4461655 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -34,6 +34,7 @@ import { AccommodationReservation, AccommodationsReservationsApi, AccommodationsReservationsList, + ValidateResaResult, } from "../../../api/accommodations/AccommodationsReservationsApi"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; @@ -186,9 +187,18 @@ export function AccommodationsReservationsRoute(): React.ReactElement { try { loadingMessage.show("Validation de la réservation en cours..."); + setValidateResaAnchorEl(null); setActiveEvent(undefined); - await AccommodationsReservationsApi.Validate(r, validate); + const res = await AccommodationsReservationsApi.Validate(r, validate); + + if (res === ValidateResaResult.Conflict) { + throw new Error( + "The reservation is in conflict with other reservations!" + ); + } else if (res === ValidateResaResult.Error) { + throw new Error("Failed to validate the reservation!"); + } reload(); snackbar("La réservation a été mise à jour avec succès !"); -- 2.45.2 From 22eeffce244fb366f68cc17eff99c5d55ff9c66e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 22 Jun 2024 09:06:51 +0200 Subject: [PATCH 61/65] Fix accommodations colors --- .../family/accommodations/AccommodationsReservationsRoute.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 4461655..2f06fba 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -435,8 +435,8 @@ export function AccommodationsReservationsRoute(): React.ReactElement { r.validated === true ? "green" : r.validated === false - ? "red dotted" - : "grey dotted", + ? "red" + : "grey ", extendedProps: { id: r.id, }, -- 2.45.2 From be9a278c1171d82bb8ebac1aa830fb4ca9162934 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 22 Jun 2024 22:31:11 +0200 Subject: [PATCH 62/65] Fix code issues --- .../CreateAccommodationCalendarURLDialog.tsx | 2 +- .../dialogs/accommodations/UpdateAccommodationDialog.tsx | 2 +- .../accommodations/AccommodationsReservationsRoute.tsx | 4 ++++ .../accommodations/AccommodationsSettingsRoute.tsx | 9 ++++++++- geneit_app/src/utils/{from_utils.ts => form_utils.ts} | 2 +- geneit_app/src/widgets/CopyToClipboard.tsx | 2 +- .../accommodations_reservations_calendars_controller.rs | 2 ++ 7 files changed, 18 insertions(+), 5 deletions(-) rename geneit_app/src/utils/{from_utils.ts => form_utils.ts} (89%) diff --git a/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx b/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx index d95abf0..c5846f9 100644 --- a/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/CreateAccommodationCalendarURLDialog.tsx @@ -8,7 +8,7 @@ import { import React from "react"; import { ServerApi } from "../../api/ServerApi"; import { NewCalendarURL } from "../../api/accommodations/AccommodationsCalendarURLApi"; -import { checkConstraint } from "../../utils/from_utils"; +import { checkConstraint } from "../../utils/form_utils"; import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute"; import { PropEdit } from "../../widgets/forms/PropEdit"; import { PropSelect } from "../../widgets/forms/PropSelect"; diff --git a/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx index a6d720e..8f179e6 100644 --- a/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx +++ b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx @@ -9,7 +9,7 @@ import { import React from "react"; import { ServerApi } from "../../api/ServerApi"; import { UpdateAccommodation } from "../../api/accommodations/AccommodationListApi"; -import { checkConstraint } from "../../utils/from_utils"; +import { checkConstraint } from "../../utils/form_utils"; import { PropCheckbox } from "../../widgets/forms/PropCheckbox"; import { PropEdit } from "../../widgets/forms/PropEdit"; import { PropColorPicker } from "../../widgets/forms/PropColorPicker"; diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx index 2f06fba..97d30b5 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsReservationsRoute.tsx @@ -560,10 +560,14 @@ export function AccommodationsReservationsRoute(): React.ReactElement { )} + {user.user.id === activeEvent?.reservation.user_id && ( <> changeReservation(activeEvent?.reservation) } diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index cb26ab1..9022ef5 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -3,6 +3,7 @@ import CheckIcon from "@mui/icons-material/Check"; import CloseIcon from "@mui/icons-material/Close"; import HouseIcon from "@mui/icons-material/House"; import { + Alert, Button, Card, CardActions, @@ -310,10 +311,16 @@ function AccommodationsCalURLsCard(): React.ReactElement { Vous pouvez, si vous le souhaitez, importer dans votre application de - calendrier le planning de réservation des logement. Pour ce faire, il + calendrier le planning de réservation des logements. Pour ce faire, il vous suffit de créer une URL de calendrier. + + Les calendriers créés ici ne sont visible que par vous. Vous ne pouvez + pas manipuler les calendriers créés par les autres membres de la + famille. + +