diff --git a/Cargo.lock b/Cargo.lock index c3b0894f..a8fe760f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -416,6 +416,15 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "argon2" version = "0.5.3" @@ -814,6 +823,7 @@ dependencies = [ "quick-xml", "rand 0.9.1", "regex", + "rust_xlsxwriter", "serde", "serde_json", "sqlx", @@ -1191,6 +1201,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -1463,6 +1484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -2153,6 +2175,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2828,6 +2859,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust_xlsxwriter" +version = "0.87.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8079587c37b35a067846a853a524cfde7012754650de7274beecc35e43acd44b" +dependencies = [ + "zip", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -3048,6 +3088,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "similar" version = "2.7.0" @@ -4228,6 +4274,38 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "zip" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/web/Cargo.toml b/web/Cargo.toml index 845defa4..db10e1d7 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -36,6 +36,7 @@ tracing-actix-web = "0.7.18" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-panic = "0.1.2" +rust_xlsxwriter = "0.87.0" [build-dependencies] built = "0.7.4" diff --git a/web/src/endpoints/export/get_events_data.rs b/web/src/endpoints/export/get_events_data.rs index 9f486d54..78fcf600 100644 --- a/web/src/endpoints/export/get_events_data.rs +++ b/web/src/endpoints/export/get_events_data.rs @@ -1,11 +1,13 @@ -use actix_web::{web, HttpResponse, Responder}; -use chrono::NaiveDate; +use actix_http::header::CONTENT_DISPOSITION; +use actix_web::{http::header::ContentDisposition, web, HttpResponse, Responder}; +use chrono::{Datelike, NaiveDate}; +use rust_xlsxwriter::{workbook::Workbook, Format}; use serde::Deserialize; -use sqlx::PgPool; +use sqlx::{query, PgPool}; use crate::{ models::{Role, User}, - utils::ApplicationError, + utils::{ApplicationError, DateTimeFormat}, }; #[derive(Deserialize)] @@ -15,6 +17,135 @@ struct ExportQuery { area: Option, } +struct EventExportEntry { + date: String, + weekday: String, + start_time: String, + end_time: String, + hours: f32, + location: String, + name: String, + assigned_name: Option, + assigned_function: Option, +} + +async fn read(pool: &PgPool) -> Result, ApplicationError> { + let results = query!("select + event.starttimestamp, + event.endtimestamp, + (event.amountofposten + event.voluntaryfuehrungsassistent::int + event.voluntarywachhabender::int) as expectedPeople, + location.name as locationName, + event.name as eventName, + array ( + select + user_.name || ' --- ' || assignment.function + from + assignment + join availability on + assignment.availabilityid = availability.id + join user_ on + availability.userid = user_.id + where + assignment.eventId = event.id) as assignments, + array ( + select + vehicle.station || ' ' || vehicle.radiocallname + from + vehicleassignment + join vehicle on + vehicleassignment.vehicleId = vehicle.id + where + vehicleassignment.eventId = event.id) as vehicles +from + event +join location on + event.locationId = location.id").fetch_all(pool).await?; + + let mut entries = Vec::new(); + + for r in results { + let date = r + .starttimestamp + .date_naive() + .format(DateTimeFormat::DayMonthYear.into()); + let weekday = r.starttimestamp.date_naive().weekday(); + let start_time = r + .starttimestamp + .time() + .format(DateTimeFormat::HourMinute.into()); + let end_time = r + .endtimestamp + .time() + .format(DateTimeFormat::HourMinute.into()); + let hours = (r.endtimestamp - r.starttimestamp).as_seconds_f32() / 3600.0 + 1.0; + let location = r.locationname; + let name = r.eventname; + + let mut event_new: Vec = Vec::new(); + + if let Some(assignments) = r.assignments { + for a in assignments { + let (assigned_name, assigned_function) = a.split_once("---").unwrap(); + let assigned_function = match assigned_function.trim() { + "posten" => "PO", + "wachhabender" => "WH", + "fuehrungsassistent" => "FüAss", + _ => assigned_function.trim(), + }; + + event_new.push(EventExportEntry { + date: date.to_string(), + weekday: weekday.to_string(), + start_time: start_time.to_string(), + end_time: end_time.to_string(), + hours, + location: location.to_string(), + name: name.to_string(), + assigned_name: Some(assigned_name.trim().to_string()), + assigned_function: Some(assigned_function.to_string()), + }); + } + } + + let expected = r.expectedpeople.unwrap_or(0) as usize; + if event_new.len() < expected { + for _ in 0..(expected - event_new.len()) { + event_new.push(EventExportEntry { + date: date.to_string(), + weekday: weekday.to_string(), + start_time: start_time.to_string(), + end_time: end_time.to_string(), + hours, + location: location.to_string(), + name: name.to_string(), + assigned_name: None, + assigned_function: None, + }); + } + } + + if let Some(vehicles) = r.vehicles { + for v in vehicles { + event_new.push(EventExportEntry { + date: date.to_string(), + weekday: weekday.to_string(), + start_time: start_time.to_string(), + end_time: end_time.to_string(), + hours, + location: location.to_string(), + name: name.to_string(), + assigned_name: Some(v.to_string()), + assigned_function: Some(String::from("Fzg")), + }); + } + } + + entries.append(&mut event_new); + } + + Ok(entries) +} + #[actix_web::get("/export/eventsdata")] pub async fn get( pool: web::Data, @@ -25,5 +156,38 @@ pub async fn get( return Err(ApplicationError::Unauthorized); } - Ok(HttpResponse::BadRequest().finish()) + let entries = read(pool.get_ref()).await?; + + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + // let time_format = Format::new(); + // time_format.set_num_format(num_format) + + for (i, entry) in entries.iter().enumerate() { + let i = (i + 3) as u32; + + worksheet.write(i, 0, &entry.date).unwrap(); + worksheet.write(i, 1, &entry.weekday).unwrap(); + worksheet.write(i, 2, &entry.start_time).unwrap(); + worksheet.write(i, 3, &entry.end_time).unwrap(); + worksheet.write(i, 4, entry.hours).unwrap(); + worksheet.write(i, 5, &entry.location).unwrap(); + worksheet.write(i, 6, &entry.name).unwrap(); + worksheet.write(i, 7, entry.assigned_name.as_ref()).unwrap(); + worksheet + .write(i, 8, entry.assigned_function.as_ref()) + .unwrap(); + } + + worksheet.autofit(); + + let buffer = workbook.save_to_buffer().unwrap(); + + return Ok(HttpResponse::Ok() + .content_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + .insert_header(( + CONTENT_DISPOSITION, + ContentDisposition::attachment("export.xlsx"), + )) + .body(buffer)); }