feat: export availabillities
This commit is contained in:
parent
772cf9e8b3
commit
9c75c10b11
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -758,6 +758,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"lettre",
|
"lettre",
|
||||||
"pico-args",
|
"pico-args",
|
||||||
|
"quick-xml",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -2088,6 +2089,16 @@ dependencies = [
|
|||||||
"cc",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.35"
|
version = "1.0.35"
|
||||||
|
@ -15,7 +15,7 @@ anyhow = "1.0.71"
|
|||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
actix-session = { version = "0.7.2", features = ["cookie-session"] }
|
actix-session = { version = "0.7.2", features = ["cookie-session"] }
|
||||||
actix-identity = "0.5.2"
|
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"
|
actix-files = "0.6.5"
|
||||||
askama_actix = "0.14.0"
|
askama_actix = "0.14.0"
|
||||||
futures-util = "0.3.30"
|
futures-util = "0.3.30"
|
||||||
@ -24,3 +24,4 @@ pico-args = "0.5.0"
|
|||||||
rand = { version = "0.8.5", features = ["getrandom"] }
|
rand = { version = "0.8.5", features = ["getrandom"] }
|
||||||
async-trait = "0.1.79"
|
async-trait = "0.1.79"
|
||||||
lettre = "0.11.7"
|
lettre = "0.11.7"
|
||||||
|
quick-xml = { version = "0.31.0", features = ["serde", "serialize"] }
|
||||||
|
@ -33,7 +33,7 @@ pub struct CalendarQuery {
|
|||||||
struct CalendarTemplate {
|
struct CalendarTemplate {
|
||||||
user: User,
|
user: User,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
area: Area,
|
areas: Vec<Area>,
|
||||||
events: Vec<Event>,
|
events: Vec<Event>,
|
||||||
availabillities: Vec<Availabillity>,
|
availabillities: Vec<Availabillity>,
|
||||||
}
|
}
|
||||||
@ -51,9 +51,10 @@ async fn get_index(
|
|||||||
Some(given_date) => given_date,
|
Some(given_date) => given_date,
|
||||||
None => Utc::now().date_naive(),
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let events = Event::read_by_date_including_location(pool.get_ref(), date).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)
|
let availabillities = Availabillity::read_by_date_including_user(pool.get_ref(), date)
|
||||||
.await
|
.await
|
||||||
@ -62,7 +63,7 @@ async fn get_index(
|
|||||||
let template = CalendarTemplate {
|
let template = CalendarTemplate {
|
||||||
user: current_user,
|
user: current_user,
|
||||||
date,
|
date,
|
||||||
area,
|
areas,
|
||||||
events,
|
events,
|
||||||
availabillities,
|
availabillities,
|
||||||
};
|
};
|
||||||
|
102
src/endpoints/get_export.rs
Normal file
102
src/endpoints/get_export.rs
Normal 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()
|
||||||
|
}
|
@ -6,6 +6,7 @@ mod events;
|
|||||||
mod location;
|
mod location;
|
||||||
mod user;
|
mod user;
|
||||||
mod assignment;
|
mod assignment;
|
||||||
|
mod get_export;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct IdPath {
|
pub struct IdPath {
|
||||||
@ -40,4 +41,6 @@ pub fn init(cfg: &mut ServiceConfig) {
|
|||||||
|
|
||||||
cfg.service(assignment::get_new::get);
|
cfg.service(assignment::get_new::get);
|
||||||
cfg.service(assignment::post_new::post);
|
cfg.service(assignment::post_new::post);
|
||||||
|
|
||||||
|
cfg.service(get_export::get);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use actix_web::{web, HttpResponse, Responder};
|
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 serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use chrono::{NaiveDate, NaiveTime};
|
use chrono::{NaiveDate, NaiveTime};
|
||||||
use sqlx::{query, PgPool};
|
use sqlx::{query, PgPool};
|
||||||
|
|
||||||
use super::{function::Function, role::Role, user::User};
|
use super::{function::Function, role::Role, user::User, Area};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Availabillity {
|
pub struct Availabillity {
|
||||||
@ -118,7 +118,10 @@ impl Availabillity {
|
|||||||
Ok(availabillities)
|
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!(
|
let records = query!(
|
||||||
r##"
|
r##"
|
||||||
SELECT
|
SELECT
|
||||||
@ -195,6 +198,77 @@ impl Availabillity {
|
|||||||
Ok(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(
|
pub async fn update(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
id: i32,
|
id: i32,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use std::fmt::Display;
|
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")]
|
#[sqlx(type_name = "function", rename_all = "lowercase")]
|
||||||
pub enum Function {
|
pub enum Function {
|
||||||
Posten = 1,
|
Posten = 1,
|
||||||
|
@ -130,6 +130,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<script>
|
||||||
document.getElementsByName("delete-availabillity")
|
document.getElementsByName("delete-availabillity")
|
||||||
.forEach(ele => ele.addEventListener("click", (event) => {
|
.forEach(ele => ele.addEventListener("click", (event) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user