Improve UI

- Add metrics table  to sidebar after schedule generation
- Add scheduler status indicator to sidebar
- Refactor report() to consume `ResidentMetrics`
- Delete unused preview component
- Beautify css across wizard steps
This commit is contained in:
2026-03-14 19:44:29 +02:00
parent 3ecdc91802
commit 756c1cdc47
14 changed files with 363 additions and 269 deletions

View File

@@ -37,6 +37,8 @@ impl From<anyhow::Error> 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",

View File

@@ -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(())

View File

@@ -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<MonthlySchedule>,
tracker: Mutex<WorkloadTracker>,
config: Mutex<UserConfig>,
config: Mutex<Option<UserConfig>>,
}
/// 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<ResidentMetrics> {
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");
}

View File

@@ -15,6 +15,16 @@ use serde::Serializer;
#[derive(Deserialize, Debug, Clone, Default)]
pub struct MonthlySchedule(pub HashMap<Slot, ResidentId>);
#[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<ResidentMetrics> {
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
}
}

View File

@@ -196,7 +196,7 @@ impl Scheduler {
fn sort_residents(
&self,
resident_ids: &mut Vec<ResidentId>,
resident_ids: &mut [ResidentId],
tracker: &WorkloadTracker,
slot: Slot,
) {

View File

@@ -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);