diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index cb64e94..b870eab 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -37,6 +37,8 @@ impl From for SearchError { #[derive(Error, Debug)] pub enum ExportError { + #[error("no schedule has been generated yet")] + NotGenerated, #[error("path not found: {0}")] InvalidPath(#[from] io::Error), #[error("docx packaging error: {0}")] @@ -57,6 +59,7 @@ impl Serialize for ExportError { s.serialize_field( "kind", match self { + ExportError::NotGenerated => "NotGenerated", ExportError::InvalidPath(_) => "InvalidPath", ExportError::Packaging(_) => "Packaging", ExportError::OpenFailed(_) => "OpenFailed", diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs index 92f5b7a..a6f1895 100644 --- a/src-tauri/src/export.rs +++ b/src-tauri/src/export.rs @@ -5,9 +5,8 @@ use docx_rs::{Docx, Paragraph, Run, RunFonts, Table, TableCell, TableRow}; use crate::{ config::UserConfig, errors::ExportError, - schedule::MonthlySchedule, + schedule::{MonthlySchedule, ResidentMetrics}, slot::{Day, ShiftPosition, Slot}, - workload::WorkloadTracker, }; #[derive(Debug)] @@ -21,7 +20,7 @@ pub trait Export { &self, file_type: FileType, config: &UserConfig, - tracker: &WorkloadTracker, + metrics: &[ResidentMetrics], ) -> Result<(), ExportError>; } @@ -30,10 +29,10 @@ impl Export for MonthlySchedule { &self, file_type: FileType, config: &UserConfig, - tracker: &WorkloadTracker, + metrics: &[ResidentMetrics], ) -> Result<(), ExportError> { match file_type { - FileType::Txt => self.export_as_txt(config, tracker)?, + FileType::Txt => self.export_as_txt(metrics)?, FileType::Docx => self.export_as_docx(config)?, }; @@ -42,16 +41,11 @@ impl Export for MonthlySchedule { } impl MonthlySchedule { - pub fn export_as_txt( - &self, - config: &UserConfig, - tracker: &WorkloadTracker, - ) -> Result<(), ExportError> { + pub fn export_as_txt(&self, metrics: &[ResidentMetrics]) -> Result<(), ExportError> { let file = File::create("rota.txt")?; let mut writer = std::io::BufWriter::new(file); - writer.write_all(self.pretty_print(config).as_bytes())?; - writer.write_all(self.report(config, tracker).as_bytes())?; + writer.write_all(self.report(metrics).as_bytes())?; writer.flush()?; Ok(()) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f735125..f35d167 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,7 +6,7 @@ use crate::{ config::{UserConfig, UserConfigDTO}, errors::{ExportError, SearchError}, export::{Export, FileType}, - schedule::MonthlySchedule, + schedule::{MonthlySchedule, ResidentMetrics}, scheduler::Scheduler, workload::WorkloadTracker, }; @@ -25,7 +25,7 @@ pub mod workload; struct AppState { schedule: Mutex, tracker: Mutex, - config: Mutex, + config: Mutex>, } /// argument to this must be the rota state including all @@ -58,19 +58,33 @@ fn generate( *internal_schedule = schedule.clone(); *internal_tracker = tracker.clone(); - *internal_config = config.clone(); + *internal_config = Some(config.clone()); Ok(schedule) } +#[tauri::command] +fn get_metrics(state: tauri::State<'_, AppState>) -> Vec { + let schedule = state.schedule.lock().unwrap(); + let tracker = state.tracker.lock().unwrap(); + let config = state.config.lock().unwrap(); + match config.as_ref() { + Some(c) => schedule.metrics(c, &tracker), + None => vec![], + } +} + #[tauri::command] fn export(state: tauri::State<'_, AppState>) -> Result<(), ExportError> { let schedule = state.schedule.lock().unwrap(); let tracker = state.tracker.lock().unwrap(); let config = state.config.lock().unwrap(); + let config = config.as_ref().ok_or(ExportError::NotGenerated)?; - schedule.export(FileType::Docx, &config, &tracker)?; - schedule.export(FileType::Txt, &config, &tracker)?; + let metrics = schedule.metrics(config, &tracker); + + schedule.export(FileType::Docx, config, &metrics)?; + schedule.export(FileType::Txt, config, &metrics)?; let log_dir = std::env::current_dir().unwrap_or(std::path::PathBuf::from(".")); info!("Files exported at {}", log_dir.display()); @@ -86,7 +100,7 @@ pub fn run() { .manage(AppState { schedule: Mutex::new(MonthlySchedule::new()), tracker: Mutex::new(WorkloadTracker::default()), - config: Mutex::new(UserConfig::default()), + config: Mutex::new(None), }) .plugin( tauri_plugin_log::Builder::new() @@ -100,7 +114,7 @@ pub fn run() { .build(), ) .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![generate, export]) + .invoke_handler(tauri::generate_handler![generate, export, get_metrics]) .run(tauri::generate_context!()) .expect("Error while running tauri application"); } diff --git a/src-tauri/src/schedule.rs b/src-tauri/src/schedule.rs index 2717f8c..864bc0c 100644 --- a/src-tauri/src/schedule.rs +++ b/src-tauri/src/schedule.rs @@ -15,6 +15,16 @@ use serde::Serializer; #[derive(Deserialize, Debug, Clone, Default)] pub struct MonthlySchedule(pub HashMap); +#[derive(Serialize)] +pub struct ResidentMetrics { + pub name: String, + pub total: u8, + pub open_first: u8, + pub open_second: u8, + pub closed: u8, + pub holiday: u8, +} + impl MonthlySchedule { pub fn new() -> Self { Self::default() @@ -88,7 +98,23 @@ impl MonthlySchedule { output } - pub fn report(&self, config: &UserConfig, tracker: &WorkloadTracker) -> String { + pub fn metrics(&self, config: &UserConfig, tracker: &WorkloadTracker) -> Vec { + let mut residents: Vec<_> = config.residents.iter().collect(); + residents.sort_by_key(|r| &r.name); + residents + .iter() + .map(|r| ResidentMetrics { + name: r.name.clone(), + total: tracker.current_workload(&r.id), + open_first: tracker.current_shift_type_workload(&r.id, ShiftType::OpenFirst), + open_second: tracker.current_shift_type_workload(&r.id, ShiftType::OpenSecond), + closed: tracker.current_shift_type_workload(&r.id, ShiftType::Closed), + holiday: tracker.current_holiday_workload(&r.id), + }) + .collect() + } + + pub fn report(&self, metrics: &[ResidentMetrics]) -> String { let mut output = String::new(); output.push('\n'); output.push_str(&format!( @@ -98,23 +124,26 @@ impl MonthlySchedule { output.push_str("-".repeat(85).as_str()); output.push('\n'); - let mut residents: Vec<_> = config.residents.iter().collect(); - residents.sort_by_key(|r| &r.name); - - for r in residents { - let total = tracker.current_workload(&r.id); - let o1 = tracker.current_shift_type_workload(&r.id, ShiftType::OpenFirst); - let o2 = tracker.current_shift_type_workload(&r.id, ShiftType::OpenSecond); - let cl = tracker.current_shift_type_workload(&r.id, ShiftType::Closed); - let holiday = tracker.current_holiday_workload(&r.id); - + for m in metrics { output.push_str(&format!( "{:<15} | {:<6} | {:<10} | {:<10} | {:<7} | {:<10}\n", - r.name, total, o1, o2, cl, holiday + m.name, m.total, m.open_first, m.open_second, m.closed, m.holiday )); } + output.push_str("-".repeat(85).as_str()); output.push('\n'); + + let total: u8 = metrics.iter().map(|m| m.total).sum(); + let o1: u8 = metrics.iter().map(|m| m.open_first).sum(); + let o2: u8 = metrics.iter().map(|m| m.open_second).sum(); + let cl: u8 = metrics.iter().map(|m| m.closed).sum(); + let holiday: u8 = metrics.iter().map(|m| m.holiday).sum(); + output.push_str(&format!( + "{:<15} | {:<6} | {:<10} | {:<10} | {:<7} | {:<10}\n", + "Σύνολο", total, o1, o2, cl, holiday + )); + output } } diff --git a/src-tauri/src/scheduler.rs b/src-tauri/src/scheduler.rs index 2591237..b16281b 100644 --- a/src-tauri/src/scheduler.rs +++ b/src-tauri/src/scheduler.rs @@ -196,7 +196,7 @@ impl Scheduler { fn sort_residents( &self, - resident_ids: &mut Vec, + resident_ids: &mut [ResidentId], tracker: &WorkloadTracker, slot: Slot, ) { diff --git a/src-tauri/tests/integration.rs b/src-tauri/tests/integration.rs index 50dba3d..ef175ff 100644 --- a/src-tauri/tests/integration.rs +++ b/src-tauri/tests/integration.rs @@ -32,7 +32,10 @@ mod integration_tests { let mut tracker = WorkloadTracker::default(); let solved = scheduler.run(&mut schedule, &mut tracker)?; - println!("{}", schedule.report(&minimal_config, &tracker)); + println!( + "{}", + schedule.report(&schedule.metrics(&minimal_config, &tracker)) + ); assert!(solved); validate_all_constraints(&schedule, &tracker, &minimal_config); @@ -50,7 +53,10 @@ mod integration_tests { let mut tracker = WorkloadTracker::default(); let solved = scheduler.run(&mut schedule, &mut tracker)?; - println!("{}", schedule.report(&maximal_config, &tracker)); + println!( + "{}", + schedule.report(&schedule.metrics(&maximal_config, &tracker)) + ); assert!(solved); validate_all_constraints(&schedule, &tracker, &maximal_config); @@ -68,7 +74,10 @@ mod integration_tests { let mut tracker = WorkloadTracker::default(); let solved = scheduler.run(&mut schedule, &mut tracker)?; - println!("{}", schedule.report(&manual_shifts_heavy_config, &tracker)); + println!( + "{}", + schedule.report(&schedule.metrics(&manual_shifts_heavy_config, &tracker)) + ); assert!(solved); validate_all_constraints(&schedule, &tracker, &manual_shifts_heavy_config); @@ -86,7 +95,10 @@ mod integration_tests { let mut tracker = WorkloadTracker::default(); let solved = scheduler.run(&mut schedule, &mut tracker)?; - println!("{}", schedule.report(&complex_config, &tracker)); + println!( + "{}", + schedule.report(&schedule.metrics(&complex_config, &tracker)) + ); assert!(solved); validate_all_constraints(&schedule, &tracker, &complex_config); @@ -104,7 +116,10 @@ mod integration_tests { let mut tracker = WorkloadTracker::default(); let solved = scheduler.run(&mut schedule, &mut tracker)?; - println!("{}", schedule.report(&hard_config, &tracker)); + println!( + "{}", + schedule.report(&schedule.metrics(&hard_config, &tracker)) + ); assert!(solved); validate_all_constraints(&schedule, &tracker, &hard_config); diff --git a/src/app.css b/src/app.css index a2cc6b5..89a8e7a 100644 --- a/src/app.css +++ b/src/app.css @@ -118,4 +118,9 @@ body { @apply bg-background text-foreground; } + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } } \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ace29e2..c16cdc2 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,38 +2,38 @@ import Basic from "./components/configurations/basic.svelte"; import Residents from "./components/configurations/residents.svelte"; import Advanced from "./components/configurations/advanced.svelte"; - import Preview from "./components/schedule/preview.svelte"; - - import { rota, steps } from "./state.svelte.js"; + import { EngineStatus, rota, steps } from "./state.svelte.js"; import Generate from "./components/schedule/generate.svelte";