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:
@@ -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",
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ impl Scheduler {
|
||||
|
||||
fn sort_residents(
|
||||
&self,
|
||||
resident_ids: &mut Vec<ResidentId>,
|
||||
resident_ids: &mut [ResidentId],
|
||||
tracker: &WorkloadTracker,
|
||||
slot: Slot,
|
||||
) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user