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",
|
||||
"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"
|
||||
|
@ -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"] }
|
||||
|
@ -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
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 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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user