feat: export events into xlsx

This commit is contained in:
Max Hohlfeld 2025-05-25 22:00:15 +02:00
parent 4af004456f
commit 01cf373b98
3 changed files with 248 additions and 5 deletions

78
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<i32>,
}
struct EventExportEntry {
date: String,
weekday: String,
start_time: String,
end_time: String,
hours: f32,
location: String,
name: String,
assigned_name: Option<String>,
assigned_function: Option<String>,
}
async fn read(pool: &PgPool) -> Result<Vec<EventExportEntry>, 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<EventExportEntry> = 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<PgPool>,
@ -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));
}