From 0726b971821351f0d1c731d7ca3dbd4c79630574 Mon Sep 17 00:00:00 2001 From: marcantoinem <marc-antoine.m@outlook.com> Date: Sat, 27 Jul 2024 19:35:44 -0400 Subject: [PATCH] (feat) dynamic stepper --- .../src/algorithm/generation.rs | 5 ++++ .../src/algorithm/schedule.rs | 6 ---- .../src/algorithm/schedules.rs | 10 +++---- aep-schedule-generator/src/data/course.rs | 21 ++++++++++++++ aep-schedule-generator/src/data/groups.rs | 7 +++++ aep-schedule-generator/src/data/time/week.rs | 9 ++++++ .../src/frontend/components/options/form.rs | 12 ++++++-- .../src/frontend/components/options/todo.rs | 28 ++++++++++++++---- .../src/frontend/components/schedules.rs | 3 +- .../src/frontend/pages/generator.rs | 29 ++++++++++++++++--- aep-schedule-website/style/main.scss | 4 +-- aep-schedule-website/style/options.scss | 2 +- 12 files changed, 108 insertions(+), 28 deletions(-) diff --git a/aep-schedule-generator/src/algorithm/generation.rs b/aep-schedule-generator/src/algorithm/generation.rs index 88a0bf5..cf57da9 100644 --- a/aep-schedule-generator/src/algorithm/generation.rs +++ b/aep-schedule-generator/src/algorithm/generation.rs @@ -21,6 +21,11 @@ impl SchedulesOptions { // pub fn get_simple_conflict<'a>(&'a self) -> Option<Conflicts> { // } + pub fn apply_personal_schedule<'a>(&'a mut self) { + self.courses_to_take + .iter_mut() + .for_each(|c| c.apply_week_mask(&self.user_conflicts)); + } pub fn get_impossible_course<'a>(&'a self) -> Vec<CompactString> { self.courses_to_take .iter() diff --git a/aep-schedule-generator/src/algorithm/schedule.rs b/aep-schedule-generator/src/algorithm/schedule.rs index c5367f9..339516f 100644 --- a/aep-schedule-generator/src/algorithm/schedule.rs +++ b/aep-schedule-generator/src/algorithm/schedule.rs @@ -10,7 +10,6 @@ use crate::data::{ day::Day, hours::NO_HOUR, period::Period, - week::Week, weeks::Weeks, }, }; @@ -74,7 +73,6 @@ impl<'a> ScheduleBuilder<'a> { &self, n: u8, min: f64, - user_conflicts: &Week<5>, options: EvaluationOption, new_course: TakenCourseBuilder, ) -> Option<Self> { @@ -82,10 +80,6 @@ impl<'a> ScheduleBuilder<'a> { let mut is_cancelled = false; new_course.for_each_group(self.courses, |group| { for period in &group.periods { - if user_conflicts.user_conflict_in_day(period) { - is_cancelled = true; - return; - } if new_schedule.week.conflict_in_day(period) { new_schedule.conflicts += 1; if new_schedule.conflicts > n { diff --git a/aep-schedule-generator/src/algorithm/schedules.rs b/aep-schedule-generator/src/algorithm/schedules.rs index 80cdc6a..3dbf244 100644 --- a/aep-schedule-generator/src/algorithm/schedules.rs +++ b/aep-schedule-generator/src/algorithm/schedules.rs @@ -46,12 +46,11 @@ impl<'a> Schedules<'a> { }; let min = self.get_min(); let e = self.options.evaluation; - let c = self.options.user_conflicts; match &course.course_type { CourseType::LabOnly { lab_groups } => { for lab_group in lab_groups.iter().filter(|g| g.open) { let course = TakenCourseBuilder::new(i, GroupIndex::none(), lab_group.into()); - if let Some(schedule) = schedule.add_check_conflicts(n, min, &c, e, course) { + if let Some(schedule) = schedule.add_check_conflicts(n, min, e, course) { self.get_schedules_rec(schedule, n, i + 1); } } @@ -59,7 +58,7 @@ impl<'a> Schedules<'a> { CourseType::TheoOnly { theo_groups } => { for theo_group in theo_groups.iter().filter(|g| g.open) { let course = TakenCourseBuilder::new(i, theo_group.into(), GroupIndex::none()); - if let Some(schedule) = schedule.add_check_conflicts(n, min, &c, e, course) { + if let Some(schedule) = schedule.add_check_conflicts(n, min, e, course) { self.get_schedules_rec(schedule, n, i + 1); } } @@ -72,8 +71,7 @@ impl<'a> Schedules<'a> { for lab_group in lab_groups.iter().filter(|g| g.open) { let course = TakenCourseBuilder::new(i, theo_group.into(), lab_group.into()); - if let Some(schedule) = schedule.add_check_conflicts(n, min, &c, e, course) - { + if let Some(schedule) = schedule.add_check_conflicts(n, min, e, course) { self.get_schedules_rec(schedule, n, i + 1); } } @@ -89,7 +87,7 @@ impl<'a> Schedules<'a> { .zip(lab_groups.iter().filter(|g| g.open)) { let course = TakenCourseBuilder::new(i, theo_group.into(), lab_group.into()); - if let Some(schedule) = schedule.add_check_conflicts(n, min, &c, e, course) { + if let Some(schedule) = schedule.add_check_conflicts(n, min, e, course) { self.get_schedules_rec(schedule, n, i + 1); } } diff --git a/aep-schedule-generator/src/data/course.rs b/aep-schedule-generator/src/data/course.rs index c523b62..070ab8f 100644 --- a/aep-schedule-generator/src/data/course.rs +++ b/aep-schedule-generator/src/data/course.rs @@ -3,6 +3,7 @@ use super::{ group::Group, group_index::GroupIndex, group_sigle::{GroupType, SigleGroup}, + time::week::Week, }; use compact_str::CompactString; use serde::{Deserialize, Serialize}; @@ -166,4 +167,24 @@ impl Course { CourseType::Linked { theo_groups, .. } => theo_groups.is_impossible(), } } + pub fn apply_week_mask(&mut self, week: &Week<5>) { + match &mut self.course_type { + CourseType::TheoOnly { theo_groups } => theo_groups.apply_week_mask(week), + CourseType::LabOnly { lab_groups } => lab_groups.apply_week_mask(week), + CourseType::Both { + theo_groups, + lab_groups, + } => { + theo_groups.apply_week_mask(week); + lab_groups.apply_week_mask(week); + } + CourseType::Linked { + theo_groups, + lab_groups, + } => { + theo_groups.apply_week_mask(week); + lab_groups.apply_week_mask(week); + } + } + } } diff --git a/aep-schedule-generator/src/data/groups.rs b/aep-schedule-generator/src/data/groups.rs index ebb8b46..f6fdc31 100644 --- a/aep-schedule-generator/src/data/groups.rs +++ b/aep-schedule-generator/src/data/groups.rs @@ -2,6 +2,7 @@ use super::{ group::Group, group_index::GroupIndex, group_sigle::{GroupType, SigleGroup}, + time::week::Week, }; use compact_str::CompactString; use serde::{Deserialize, Serialize}; @@ -71,6 +72,12 @@ impl Groups { .map(|g| SigleGroup::new(sigle.clone(), group_type, g.number)) .collect() } + + pub fn apply_week_mask(&mut self, week: &Week<5>) { + self.iter_mut() + .filter(|g| g.periods.iter().any(|p| week.user_conflict_in_day(p))) + .for_each(|g| g.open = false); + } } impl Index<GroupIndex> for Groups { diff --git a/aep-schedule-generator/src/data/time/week.rs b/aep-schedule-generator/src/data/time/week.rs index ec95e75..6e741d3 100644 --- a/aep-schedule-generator/src/data/time/week.rs +++ b/aep-schedule-generator/src/data/time/week.rs @@ -8,6 +8,7 @@ use std::ops::Deref; pub struct Week<const N: usize>([Hours; N]); impl<const N: usize> Default for Week<N> { + #[inline] fn default() -> Self { Self([NO_HOUR; N]) } @@ -15,21 +16,29 @@ impl<const N: usize> Default for Week<N> { impl<const N: usize> Deref for Week<N> { type Target = [Hours; N]; + #[inline] fn deref(&self) -> &Self::Target { &self.0 } } impl<const N: usize> Week<N> { + #[inline] pub fn new(week: [u64; N]) -> Self { Self(week.map(|d| Hours::from(d))) } + + #[inline] pub fn add_period(&mut self, period: &Period) { self.0[period.day as usize] |= period.hours; } + + #[inline] pub fn conflict_in_day(&self, period: &Period) -> bool { self.0[period.day as usize] & period.hours != NO_HOUR } + + #[inline] pub fn user_conflict_in_day(&self, period: &Period) -> bool { if period.day as usize >= N { return false; diff --git a/aep-schedule-website/src/frontend/components/options/form.rs b/aep-schedule-website/src/frontend/components/options/form.rs index 7b63932..4eee55c 100644 --- a/aep-schedule-website/src/frontend/components/options/form.rs +++ b/aep-schedule-website/src/frontend/components/options/form.rs @@ -12,12 +12,20 @@ use aep_schedule_generator::algorithm::{generation::SchedulesOptions, schedule:: use leptos::*; #[component] -pub fn OptionsForms(action: Action<SchedulesOptions, Vec<Schedule>>) -> impl IntoView { +pub fn OptionsForms<F>( + action: Action<SchedulesOptions, Vec<Schedule>>, + step: ReadSignal<u8>, + validate: F, +) -> impl IntoView +where + F: Fn(OptionState) + Copy + 'static, +{ let state: OptionState = use_context().unwrap(); let first_generation_done: FirstGenerationDone = use_context().unwrap(); let submit = move || { - if !first_generation_done.0.get() { + validate(state); + if !first_generation_done.0.get() || step.get() != 5 { return; } action.dispatch((&state).into()); diff --git a/aep-schedule-website/src/frontend/components/options/todo.rs b/aep-schedule-website/src/frontend/components/options/todo.rs index be3cd94..88d9f10 100644 --- a/aep-schedule-website/src/frontend/components/options/todo.rs +++ b/aep-schedule-website/src/frontend/components/options/todo.rs @@ -46,7 +46,10 @@ pub fn Step( } #[component] -pub fn Todo(action: Action<SchedulesOptions, Vec<Schedule>>) -> impl IntoView { +pub fn Todo( + action: Action<SchedulesOptions, Vec<Schedule>>, + step: ReadSignal<u8>, +) -> impl IntoView { let state: OptionState = use_context().unwrap(); let first_generation_done: FirstGenerationDone = use_context().unwrap(); @@ -55,27 +58,40 @@ pub fn Todo(action: Action<SchedulesOptions, Vec<Schedule>>) -> impl IntoView { action.dispatch((&state).into()) }; - let (step, set_step) = create_signal(3); + let disab = move || { + let step = step.get(); + match step { + 0..=4 => "disabled", + _ => "", + } + }; + + let bg_color = move || { + match step.get().cmp(&5) { + Ordering::Less => "flex transition-colors items-center justify-center w-10 h-10 border rounded-full", + Ordering::Greater | Ordering::Equal => "flex transition-colors items-center justify-center w-10 h-10 border rounded-full bg-green-400", + } + }; view! { <div class="px-4 py-4 mx-auto"> <div class="grid gap-6 row-gap-10"> <div class="lg:py-6 lg:pr-16"> <Step n=1 step title="Ajoutez vos cours" description="Utilisez la barre de recherche à gauche pour trouver et sélectionner vos cours. Une fois les cours sélectionnés, ils apparaîtront comme un onglet."/> - <Step n=2 step title="Forcer des heures libres" description="Sélectionnez une plage de temps à avoir absolument libre en pressant et relâchant sur votre horaire personnel."/> - <Step n=3 step title="Ouvrez des sections" description="Assurez d'avoir au moins une section d'ouverte pour la théorie et la pratique. En sélectionnant l'onglet du cours et en appuyant sur les sections."/> + <Step n=2 step title="Ouvrez des sections" description="Assurez d'avoir au moins une section d'ouverte pour la théorie et la pratique. En sélectionnant l'onglet du cours et en appuyant sur les sections."/> + <Step n=3 step title="Forcer des heures libres" description="Sélectionnez une plage de temps à avoir absolument libre en pressant et relâchant sur votre horaire personnel."/> <Step n=4 step title="Ajustez les paramètres" description="Bougez les curseurs en bas pour ajuster vos préférences. Vous pouvez choisir d'avoir plus de congés, de commencer en moyenne les cours plus tôt ou plus tard, ou de finir en moyenne plus tôt."/> <div class="flex items-center"> <div class="flex flex-col items-center mr-4"> <div> - <div class="flex items-center justify-center w-10 h-10 border rounded-full"> + <div class=bg_color> <svg class="w-6 text-gray-600" stroke="currentColor" viewBox="0 0 24 24"> <polyline fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="6,12 10,16 18,8"></polyline> </svg> </div> </div> </div> - <button on:click=submit class="select-none rounded-lg bg-amber-500 py-1 text-lg font-sans font-semibold px-2 w-64 self-center text-center align-middle text-black shadow-md shadow-amber-500/20 transition-all hover:shadow-lg hover:shadow-amber-500/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none">"Générer les horaires"</button> + <button on:click=submit class="select-none rounded-lg bg-amber-500 py-1 text-lg font-sans font-semibold px-2 w-64 self-center text-center align-middle text-black shadow-md shadow-amber-500/20 transition-all hover:shadow-lg hover:shadow-amber-500/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none" prop:disabled=disab disabled>"Générer les horaires"</button> </div> </div> </div> diff --git a/aep-schedule-website/src/frontend/components/schedules.rs b/aep-schedule-website/src/frontend/components/schedules.rs index d7ac67e..caf96ee 100644 --- a/aep-schedule-website/src/frontend/components/schedules.rs +++ b/aep-schedule-website/src/frontend/components/schedules.rs @@ -8,6 +8,7 @@ use leptos::*; pub fn SchedulesComponent( read_signal: RwSignal<Option<Vec<Schedule>>>, action: Action<SchedulesOptions, Vec<Schedule>>, + step: ReadSignal<u8>, ) -> impl IntoView { view! { <Await @@ -22,7 +23,7 @@ pub fn SchedulesComponent( } }).collect_view(), None => view ! { - <Todo action/> + <Todo action step/> } } } diff --git a/aep-schedule-website/src/frontend/pages/generator.rs b/aep-schedule-website/src/frontend/pages/generator.rs index d03cd86..783631e 100644 --- a/aep-schedule-website/src/frontend/pages/generator.rs +++ b/aep-schedule-website/src/frontend/pages/generator.rs @@ -17,10 +17,13 @@ pub fn GeneratorPage() -> impl IntoView { let (hide, set_hide) = create_signal(false); let first_generation_done = create_rw_signal(false); + let (step, set_step) = create_signal(1); + // Creates a reactive value to update the button let action = create_action(move |s: &SchedulesOptions| { - let s = s.clone(); + let mut s = s.clone(); set_hide(true); + s.apply_personal_schedule(); async move { s.get_schedules().into_sorted_vec() } }); @@ -28,16 +31,34 @@ pub fn GeneratorPage() -> impl IntoView { let state = OptionState::default(); + let validate = move |state: OptionState| { + let mut options: SchedulesOptions = (&state).into(); + if options.courses_to_take.is_empty() { + set_step.set(1); + return; + } + if !options.get_impossible_course().is_empty() { + set_step.set(2); + return; + } + options.apply_personal_schedule(); + if !options.get_impossible_course().is_empty() { + set_step.set(3); + return; + } + set_step.set(5); + }; + provide_context(state); provide_context(SetModal(set_modal)); provide_context(FirstGenerationDone(first_generation_done)); view! { <aside class="left-panel" class=("hide-left-panel", hide)> - <OptionsForms action=action/> + <OptionsForms action=action validate step/> </aside> - <section class="right-panel "> - <SchedulesComponent action=action read_signal=action.value()/> + <section class="right-panel"> + <SchedulesComponent action=action read_signal=action.value() step/> </section> <Notifications modal set_modal/> <button on:click=move |_| {set_hide(false)} id="go-back"><CaretDoubleRight weight=IconWeight::Regular size="3vh"/></button> diff --git a/aep-schedule-website/style/main.scss b/aep-schedule-website/style/main.scss index 680a920..93b18b3 100644 --- a/aep-schedule-website/style/main.scss +++ b/aep-schedule-website/style/main.scss @@ -125,8 +125,8 @@ main { display: flex; flex-direction: column; align-items: center; - gap: 2rem; - padding: 0.25rem; + gap: 3rem; + padding: 1rem; background-color: $light-background; } diff --git a/aep-schedule-website/style/options.scss b/aep-schedule-website/style/options.scss index 3c170e1..d92cd29 100644 --- a/aep-schedule-website/style/options.scss +++ b/aep-schedule-website/style/options.scss @@ -175,7 +175,7 @@ } .selected-hour { - background-color: $background-color; + background-color: rgb(87, 104, 175); } .unselected-hour { -- GitLab