diff --git a/Cargo.lock b/Cargo.lock index e221ae56..e2017a11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -758,6 +758,7 @@ dependencies = [ "futures-util", "lettre", "pico-args", + "quick-xml", "rand", "serde", "serde_json", @@ -2088,6 +2089,16 @@ dependencies = [ "cc", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.35" diff --git a/Cargo.toml b/Cargo.toml index e70d56a3..907d11b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ anyhow = "1.0.71" dotenv = "0.15.0" actix-session = { version = "0.7.2", features = ["cookie-session"] } actix-identity = "0.5.2" -chrono = { version = "0.4.33", features = ["serde"] } +chrono = { version = "0.4.33", features = ["serde", "now"] } actix-files = "0.6.5" askama_actix = "0.14.0" futures-util = "0.3.30" @@ -24,3 +24,4 @@ pico-args = "0.5.0" rand = { version = "0.8.5", features = ["getrandom"] } async-trait = "0.1.79" lettre = "0.11.7" +quick-xml = { version = "0.31.0", features = ["serde", "serialize"] } diff --git a/src/calendar/routes.rs b/src/calendar/routes.rs index fcfb988e..b4756def 100644 --- a/src/calendar/routes.rs +++ b/src/calendar/routes.rs @@ -33,7 +33,7 @@ pub struct CalendarQuery { struct CalendarTemplate { user: User, date: NaiveDate, - area: Area, + areas: Vec, events: Vec, availabillities: Vec, } @@ -51,9 +51,10 @@ async fn get_index( Some(given_date) => given_date, None => Utc::now().date_naive(), }; - let area = Area::read_by_id(pool.get_ref(), current_user.area_id) + let areas = Area::read_all(pool.get_ref()) .await .unwrap(); + let events = Event::read_by_date_including_location(pool.get_ref(), date).await.unwrap(); let availabillities = Availabillity::read_by_date_including_user(pool.get_ref(), date) .await @@ -62,7 +63,7 @@ async fn get_index( let template = CalendarTemplate { user: current_user, date, - area, + areas, events, availabillities, }; diff --git a/src/endpoints/get_export.rs b/src/endpoints/get_export.rs new file mode 100644 index 00000000..aff91eaf --- /dev/null +++ b/src/endpoints/get_export.rs @@ -0,0 +1,102 @@ +use actix_identity::Identity; +use actix_web::{http::header::{ContentDisposition, ContentType, Header, CONTENT_DISPOSITION}, web, HttpResponse, Responder}; +use chrono::{Months, NaiveDate, NaiveTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; + +use crate::models::{Availabillity, Function, Role, User}; + +#[derive(Deserialize)] +struct ExportQuery { + year: u16, + month: u8, + area_id: Option, +} + +#[derive(Serialize)] +struct ExportXml { + year: u16, + month: u8, + area: String, + availabillities: Vec, +} + +#[derive(Serialize)] +struct ExportAvailabillityXml { + name: String, + area: String, + function: Function, + date: NaiveDate, + whole_day: bool, + start_time: NaiveTime, + end_time: NaiveTime, + assigned: bool, +} + +#[actix_web::get("/export-availabillities")] +pub async fn get( + pool: web::Data, + user: Identity, + query: web::Query, +) -> impl Responder { + let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()) + .await + .unwrap(); + + if current_user.role != Role::Admin && current_user.role != Role::AreaManager { + return HttpResponse::Unauthorized().finish(); + } + + let start_date = NaiveDate::from_ymd_opt(query.year as i32, query.month as u32, 1) + .unwrap_or(Utc::now().date_naive()); + let end_date = start_date + .checked_add_months(Months::new(1)) + .unwrap() + .pred_opt() + .unwrap(); + + let area_id = if current_user.role == Role::Admin && query.area_id.is_some() { + query.area_id.unwrap() + } else { + current_user.area_id + }; + + let availabillities = + Availabillity::read_for_export(pool.get_ref(), (start_date, end_date), area_id) + .await + .unwrap(); + + let export_availabillities = availabillities + .into_iter() + .map(|a| ExportAvailabillityXml { + name: a.user.as_ref().unwrap().name.clone(), + area: a.user.as_ref().unwrap().area.as_ref().unwrap().name.clone(), + function: a.user.unwrap().function, + date: a.date, + whole_day: a.start_time.is_none() && a.end_time.is_none(), + start_time: a + .start_time + .unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap()), + end_time: a + .end_time + .unwrap_or(NaiveTime::from_hms_opt(23, 59, 59).unwrap()), + assigned: false, + }) + .collect(); + + let out = ExportXml { + year: query.year, + month: query.month, + area: area_id.to_string(), + availabillities: export_availabillities, + }; + + if let Ok(xml) = quick_xml::se::to_string(&out) { + return HttpResponse::Ok() + .content_type(ContentType::xml()) + .insert_header((CONTENT_DISPOSITION, ContentDisposition::attachment("export.xml"))) + .body(xml); + } + + HttpResponse::BadRequest().finish() +} diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index b5b5e1b1..f6655948 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -6,6 +6,7 @@ mod events; mod location; mod user; mod assignment; +mod get_export; #[derive(Deserialize)] pub struct IdPath { @@ -40,4 +41,6 @@ pub fn init(cfg: &mut ServiceConfig) { cfg.service(assignment::get_new::get); cfg.service(assignment::post_new::post); + + cfg.service(get_export::get); } diff --git a/src/endpoints/user/post_reset.rs b/src/endpoints/user/post_reset.rs index eca6d8ea..cdb2552c 100644 --- a/src/endpoints/user/post_reset.rs +++ b/src/endpoints/user/post_reset.rs @@ -1,5 +1,5 @@ use actix_web::{web, HttpResponse, Responder}; -use lettre::{message::header::ContentType, transport::smtp::{authentication::Credentials, client::TlsParameters}, Message, SmtpTransport, Transport}; +use lettre::{message::header::ContentType, Message, SmtpTransport, Transport}; use serde::Deserialize; use sqlx::PgPool; diff --git a/src/models/availabillity.rs b/src/models/availabillity.rs index 67e56c7a..049381ff 100644 --- a/src/models/availabillity.rs +++ b/src/models/availabillity.rs @@ -1,7 +1,7 @@ use chrono::{NaiveDate, NaiveTime}; use sqlx::{query, PgPool}; -use super::{function::Function, role::Role, user::User}; +use super::{function::Function, role::Role, user::User, Area}; #[derive(Clone)] pub struct Availabillity { @@ -118,7 +118,10 @@ impl Availabillity { Ok(availabillities) } - pub async fn read_not_assigned_by_date_including_user(pool: &PgPool, date: NaiveDate) -> anyhow::Result> { + pub async fn read_not_assigned_by_date_including_user( + pool: &PgPool, + date: NaiveDate, + ) -> anyhow::Result> { let records = query!( r##" SELECT @@ -195,6 +198,77 @@ impl Availabillity { Ok(availabillity) } + pub async fn read_for_export( + pool: &PgPool, + date_range: (NaiveDate, NaiveDate), + area_id: i32, + ) -> anyhow::Result> { + let records = query!( + r##" + SELECT + availabillity.id, + availabillity.userId, + availabillity.date, + availabillity.startTime, + availabillity.endTime, + availabillity.comment, + user_.name, + user_.email, + user_.password, + user_.salt, + user_.role AS "role: Role", + user_.function AS "function: Function", + user_.areaId, + user_.locked, + user_.lastLogin, + user_.receiveNotifications, + area.name AS areaName + FROM availabillity + JOIN user_ ON availabillity.userId = user_.id + JOIN area ON user_.areaId = area.id + WHERE user_.areaId = $1 AND + availabillity.date >= $2 AND + availabillity.date <= $3; + "##, + area_id, + date_range.0, + date_range.1 + ) + .fetch_all(pool) + .await?; + + let availabillities = records + .iter() + .map(|r| Availabillity { + id: r.id, + user_id: r.userid, + user: Some(User { + id: r.userid, + name: r.name.clone(), + email: r.email.clone(), + password: r.password.clone(), + salt: r.salt.clone(), + role: r.role.clone(), + function: r.function.clone(), + area_id: r.areaid, + area: Some(Area { + id: r.areaid, + name: r.areaname.clone(), + }), + locked: r.locked, + last_login: r.lastlogin, + receive_notifications: r.receivenotifications, + }), + date: r.date, + start_time: r.starttime, + end_time: r.endtime, + comment: r.comment.clone(), + }) + .collect(); + + Ok(availabillities) + } + pub async fn update( pool: &PgPool, id: i32, diff --git a/src/models/function.rs b/src/models/function.rs index 7ea16ad7..81b09a23 100644 --- a/src/models/function.rs +++ b/src/models/function.rs @@ -1,6 +1,8 @@ use std::fmt::Display; -#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq, Eq)] +use serde::Serialize; + +#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[sqlx(type_name = "function", rename_all = "lowercase")] pub enum Function { Posten = 1, diff --git a/templates/index.html b/templates/index.html index 350e74da..b50aa4df 100644 --- a/templates/index.html +++ b/templates/index.html @@ -130,6 +130,45 @@ + + + + Verfügbarkeiten nach XML exportieren + + + + Jahr + + + + + + + Monat + + + + + + {% if user.role == Role::Admin %} + + Bereich + + + + {% for area in areas %} + {{ area.name }} + {% endfor %} + + + + + {% endif %} + + + + +