feat: export availabillities

This commit is contained in:
Max Hohlfeld 2024-05-27 23:53:06 +02:00
parent 772cf9e8b3
commit 9c75c10b11
9 changed files with 241 additions and 8 deletions

11
Cargo.lock generated
View File

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

View File

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

View File

@ -33,7 +33,7 @@ pub struct CalendarQuery {
struct CalendarTemplate {
user: User,
date: NaiveDate,
area: Area,
areas: Vec<Area>,
events: Vec<Event>,
availabillities: Vec<Availabillity>,
}
@ -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,
};

102
src/endpoints/get_export.rs Normal file
View File

@ -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<i32>,
}
#[derive(Serialize)]
struct ExportXml {
year: u16,
month: u8,
area: String,
availabillities: Vec<ExportAvailabillityXml>,
}
#[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<PgPool>,
user: Identity,
query: web::Query<ExportQuery>,
) -> 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()
}

View File

@ -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);
}

View File

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

View File

@ -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<Vec<Availabillity>> {
pub async fn read_not_assigned_by_date_including_user(
pool: &PgPool,
date: NaiveDate,
) -> anyhow::Result<Vec<Availabillity>> {
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<Vec<Availabillity>> {
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,

View File

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

View File

@ -130,6 +130,45 @@
</div>
</section>
<section class="section">
<div class="container">
<h3 class="title is-3">
Verfügbarkeiten nach XML exportieren
</h3>
<form action="/export-availabillities" target="_blank">
<div class="field">
<label class="label">Jahr</label>
<div class="control">
<input class="input" type="number" name="year" min="2024" max="9999" value="2024" />
</div>
</div>
<div class="field">
<label class="label">Monat</label>
<div class="control">
<input class="input" type="number" name="month" min="1" max="12" value="1" />
</div>
</div>
{% if user.role == Role::Admin %}
<div class="field">
<label class="label">Bereich</label>
<div class="control">
<div class="select is-fullwidth">
<select name="area">
{% for area in areas %}
<option value="{{ area.id }}">{{ area.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
{% endif %}
<input class="button is-primary" type="submit" value="Exportieren" />
</form>
</div>
</section>
<script>
document.getElementsByName("delete-availabillity")
.forEach(ele => ele.addEventListener("click", (event) => {