From 4b49b4b54eb84055c222f9245fd3f6867d2faacb Mon Sep 17 00:00:00 2001 From: stefiosif Date: Tue, 13 Jan 2026 21:08:24 +0200 Subject: [PATCH] Export result to txt/docx --- src-tauri/.gitignore | 3 + src-tauri/Cargo.lock | 89 ++++++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/export.rs | 165 ++++++++++++++++++++++++++++++++++------ src-tauri/src/lib.rs | 2 +- src-tauri/src/slot.rs | 30 ++++++++ 6 files changed, 265 insertions(+), 25 deletions(-) diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index b21bd68..3a41134 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -5,3 +5,6 @@ # Generated by Tauri # will have schema files for capabilities auto-completion /gen/schemas + + +schedule.* \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4d18acf..6d132e7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -552,6 +552,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -846,6 +852,21 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "docx-rs" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f21be13b97bd2924f30323d674f5a8db382964972825abd93f30d08f21dad98" +dependencies = [ + "base64 0.22.1", + "image", + "serde", + "serde_json", + "thiserror 1.0.69", + "xml-rs", + "zip", +] + [[package]] name = "dpi" version = "0.1.2" @@ -1344,6 +1365,16 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gio" version = "0.18.4" @@ -1769,6 +1800,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1896,6 +1943,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" + [[package]] name = "js-sys" version = "0.3.83" @@ -3170,6 +3223,7 @@ name = "rota" version = "0.1.0" dependencies = [ "chrono", + "docx-rs", "itertools", "log", "rand 0.9.2", @@ -4163,6 +4217,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.44" @@ -4817,6 +4882,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -5357,6 +5428,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + [[package]] name = "yoke" version = "0.8.1" @@ -5515,6 +5592,18 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zvariant" version = "5.8.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5276c7c..3bf81fb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,3 +28,4 @@ rstest = "0.26.1" tauri-plugin-log = "2" log = "0.4.29" rand = "0.9.2" +docx-rs = "0.4.18" diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs index 2fd2c80..191162b 100644 --- a/src-tauri/src/export.rs +++ b/src-tauri/src/export.rs @@ -1,21 +1,18 @@ -// here lies the logic for the export of the final schedule into docx/pdf formats - use std::{fs::File, io::Write}; +use docx_rs::{Docx, Paragraph, Run, RunFonts, Table, TableCell, TableRow}; use log::info; use crate::{ config::UserConfig, schedule::MonthlySchedule, + slot::{month_to_greek, weekday_to_greek, Day, ShiftPosition, Slot}, }; #[derive(Debug)] pub enum FileType { Txt, - Json, - Csv, - Doc, - Pdf, + Docx, } pub trait Export { @@ -26,10 +23,7 @@ impl Export for MonthlySchedule { fn export(&self, file_type: FileType, config: &UserConfig) { match file_type { FileType::Txt => self.export_as_txt(config), - FileType::Csv => self.export_as_csv(config), - FileType::Json => self.export_as_json(config), - FileType::Doc => self.export_as_doc(config), - FileType::Pdf => self.export_as_pdf(config), + FileType::Docx => self.export_as_doc(config), }; // TODO: make this env var from a config file? Option to change this in-app @@ -49,32 +43,155 @@ impl MonthlySchedule { .write_all(self.pretty_print(config).as_bytes()) .expect("Failed to write to buffer"); + writer + .write_all(self.report(config).as_bytes()) + .expect("Failed to write to buffer"); + writer.flush().expect("Failed to flush buffer"); info!("im here"); "ok".to_string() } - pub fn export_as_csv(&self, config: &UserConfig) -> String { - todo!() - } - - pub fn export_as_json(&self, config: &UserConfig) -> String { - todo!() - } - pub fn export_as_doc(&self, config: &UserConfig) -> String { - todo!() - } + let path = std::path::Path::new("./schedule.docx"); + let file = std::fs::File::create(path).unwrap(); - pub fn export_as_pdf(&self, config: &UserConfig) -> String { - todo!() + let header = Table::new(vec![ + TableRow::new(vec![TableCell::new().add_paragraph( + Paragraph::new() + .add_run(Run::new().bold().add_text("Εφημερίες Ακτινολογικό")) + .fonts(RunFonts::new().ascii("Arial")), + )]), + TableRow::new(vec![TableCell::new().add_paragraph( + Paragraph::new() + .add_run(Run::new().bold().add_text(format!( + "Μήνας {} {}", + month_to_greek(config.month.number_from_month()), + config.year + ))) + .fonts(RunFonts::new().ascii("Arial")), + )]), + TableRow::new(vec![TableCell::new().add_paragraph( + Paragraph::new() + .add_run(Run::new().bold().add_text("")) + .fonts(RunFonts::new().ascii("Arial")), + )]), + TableRow::new(vec![TableCell::new().add_paragraph( + Paragraph::new() + .add_run( + Run::new() + .bold() + .add_text("ΠΑΝΕΠΙΣΤΗΜΙΑΚΟ ΓΕΝΙΚΟ ΝΟΣΟΚΟΜΕΙΟ ΙΩΑΝΝΙΝΩΝ"), + ) + .fonts(RunFonts::new().ascii("Arial")), + )]), + ]); + + let mut doc = Docx::new().add_table(header); + + let mut residents_table = Table::new(vec![]); + + for d in 1..=config.total_days() { + let day = Day(d); + let is_weekend = day.is_weekend(config.month.number_from_month(), config.year); + let slot_first = Slot::new(Day(d), ShiftPosition::First); + let slot_first_res_id = self.get_resident_id(&slot_first); + let res_name_1 = config + .residents + .iter() + .find(|r| Some(&r.id) == slot_first_res_id) + .map(|r| r.name.as_str()) + .unwrap(); + + let res_name_2 = if day.is_open_shift() { + let slot_second = Slot::new(Day(d), ShiftPosition::Second); + let slot_second_res_id = self.get_resident_id(&slot_second); + config + .residents + .iter() + .find(|r| Some(&r.id) == slot_second_res_id) + .map(|r| r.name.as_str()) + } else { + None + }; + + let make_run = |text: &str| { + let mut run = Run::new().add_text(text); + if is_weekend { + run = run.bold(); + } + run + }; + + let residents_table_row = TableRow::new(vec![ + TableCell::new().add_paragraph( + Paragraph::new() + .add_run(make_run(&format!("{d}"))) + .fonts(RunFonts::new().ascii("Arial")), + ), + TableCell::new().add_paragraph( + Paragraph::new() + .add_run(make_run(weekday_to_greek( + Day(d).weekday(config.month.number_from_month(), config.year), + ))) + .fonts(RunFonts::new().ascii("Arial")), + ), + TableCell::new().add_paragraph( + Paragraph::new() + .add_run(make_run(res_name_1)) + .fonts(RunFonts::new().ascii("Arial")), + ), + TableCell::new().add_paragraph( + Paragraph::new() + .add_run(make_run(res_name_2.unwrap_or(""))) + .fonts(RunFonts::new().ascii("Arial")), + ), + ]); + + residents_table = residents_table.add_row(residents_table_row); + } + + doc = doc.add_table(residents_table); + + doc.build().pack(file).unwrap(); + + "just a string".to_string() } } #[cfg(test)] mod tests { - use rstest::rstest; + use rstest::{fixture, rstest}; + + use crate::{ + config::UserConfig, generator::backtracking, resident::Resident, schedule::MonthlySchedule, + slot::Slot, + }; + + #[fixture] + fn schedule() -> MonthlySchedule { + MonthlySchedule::new() + } + + #[fixture] + fn config() -> UserConfig { + let mut config = UserConfig::default().with_residents(vec![ + Resident::new("1", "Στέφανος"), + Resident::new("2", "Ιορδάνης"), + Resident::new("3", "Μαρία"), + Resident::new("4", "Βεατρίκη"), + Resident::new("5", "Τάκης"), + Resident::new("6", "Μάκης"), + ]); + config.calculate_workload_limits(); + config.calculate_holiday_limits(); + config.calculate_shift_type_fairness(); + config + } #[rstest] - pub fn xxxy() {} + pub fn test_export_as_doc(mut schedule: MonthlySchedule, config: UserConfig) { + backtracking(&mut schedule, Slot::default(), &config); + schedule.export_as_doc(&config); + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3c9d12f..b12839a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -50,7 +50,7 @@ fn generate(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> Monthly fn export(config: UserConfigDTO, state: tauri::State<'_, AppState>) { let config = UserConfig::from_dto(config); let schedule = state.schedule.lock().unwrap(); - schedule.export(FileType::Txt, &config); + schedule.export(FileType::Docx, &config); } #[cfg_attr(mobile, tauri::mobile_entry_point)] diff --git a/src-tauri/src/slot.rs b/src-tauri/src/slot.rs index afb4e53..20d1495 100644 --- a/src-tauri/src/slot.rs +++ b/src-tauri/src/slot.rs @@ -128,6 +128,36 @@ pub enum ShiftPosition { Second, } +pub fn weekday_to_greek(weekday: Weekday) -> &'static str { + match weekday { + Weekday::Mon => "Δευτέρα", + Weekday::Tue => "Τρίτη", + Weekday::Wed => "Τετάρτη", + Weekday::Thu => "Πέμπτη", + Weekday::Fri => "Παρασκευή", + Weekday::Sat => "Σάββατο", + Weekday::Sun => "Κυριακή", + } +} + +pub fn month_to_greek(month: u32) -> &'static str { + match month { + 1 => "Ιανουάριος", + 2 => "Φεβρουάριος", + 3 => "Μάρτιος", + 4 => "Απρίλιος", + 5 => "Μάιος", + 6 => "Ιούνιος", + 7 => "Ιούλιος", + 8 => "Αύγουστος", + 9 => "Σεπτέμβριος", + 10 => "Οκτώβριος", + 11 => "Νοέμβριος", + 12 => "Δεκέμβριος", + _ => panic!("Unable to find translation for month {}", month), + } +} + #[cfg(test)] mod tests { use rstest::rstest;