feat: move export to own view

This commit is contained in:
Max Hohlfeld 2024-06-23 11:12:53 +02:00
parent 4c58ff867d
commit 33baf479b5
13 changed files with 316 additions and 230 deletions

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
max_line_length = off
[*.html]
indent_style = space
indent_size = 2

View File

@ -0,0 +1,36 @@
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use sqlx::PgPool;
use crate::models::{Area, User, Role};
#[derive(Template)]
#[template(path = "export/availabilityxml.html")]
struct AvailabilityExportTemplate {
user: User,
areas: Option<Vec<Area>>
}
#[actix_web::get("/export/availability")]
pub async fn get(
user: web::ReqData<User>,
pool: web::Data<PgPool>
) -> impl Responder {
if user.role != Role::Admin && user.role != Role::AreaManager {
return HttpResponse::Unauthorized().finish();
}
let areas = if user.role == Role::Admin {
Some(Area::read_all(pool.get_ref()).await.unwrap())
} else {
None
};
let template = AvailabilityExportTemplate {
user: user.into_inner(),
areas
};
return template.to_response();
}

View File

@ -1,5 +1,5 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{http::header::{ContentDisposition, ContentType, Header, CONTENT_DISPOSITION}, web, HttpResponse, Responder}; use actix_web::{http::header::{ContentDisposition, ContentType, CONTENT_DISPOSITION}, web, HttpResponse, Responder};
use chrono::{Months, NaiveDate, NaiveTime, Utc}; use chrono::{Months, NaiveDate, NaiveTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
@ -33,7 +33,7 @@ struct ExportAvailabillityXml {
assigned: bool, assigned: bool,
} }
#[actix_web::get("/export-availabillities")] #[actix_web::get("/export/availabilitydata")]
pub async fn get( pub async fn get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
user: Identity, user: Identity,

View File

@ -0,0 +1,2 @@
pub mod get_availability;
pub mod get_availability_data;

View File

@ -2,13 +2,13 @@ use actix_web::web::ServiceConfig;
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::Deserialize; use serde::Deserialize;
mod area;
mod assignment;
mod availability; mod availability;
mod events; mod events;
mod export;
mod location; mod location;
mod user; mod user;
mod assignment;
mod get_export;
mod area;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct IdPath { pub struct IdPath {
@ -54,5 +54,6 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(area::get_new::get); cfg.service(area::get_new::get);
cfg.service(area::post_new::post); cfg.service(area::post_new::post);
cfg.service(get_export::get); cfg.service(export::get_availability::get);
cfg.service(export::get_availability_data::get);
} }

View File

@ -7,44 +7,44 @@
<h1 class="title">Neuen Bereich anlegen</h1> <h1 class="title">Neuen Bereich anlegen</h1>
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label"> <div class="field-label">
<label class="label">Name</label> <label class="label">Name</label>
</div> </div>
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input" type="text" name="name" required placeholder="Leipzig Ost"/> <input class="input" type="text" name="name" required placeholder="Leipzig Ost" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label"></div> <div class="field-label"></div>
<div class="field-body"> <div class="field-body">
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<button class="button is-success"> <button class="button is-success">
<span class="icon"> <span class="icon">
<svg class="feather"> <svg class="feather">
<use href="/static/feather-sprite.svg#check-circle" /> <use href="/static/feather-sprite.svg#check-circle" />
</svg> </svg>
</span> </span>
<span>Speichern</span> <span>Speichern</span>
</button> </button>
</div> </div>
<div class="control"> <div class="control">
<a class="button is-link is-light" hx-boost="true" href="/locations"> <a class="button is-link is-light" hx-boost="true" href="/locations">
<span class="icon"> <span class="icon">
<svg class="feather"> <svg class="feather">
<use href="/static/feather-sprite.svg#arrow-left" /> <use href="/static/feather-sprite.svg#arrow-left" />
</svg> </svg>
</span> </span>
<span>Zurück</span> <span>Zurück</span>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>

View File

@ -2,43 +2,43 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brass - Brasiwa Leipzig</title> <title>Brass - Brasiwa Leipzig</title>
<link rel="stylesheet" href="/static/bulma.min.css"> <link rel="stylesheet" href="/static/bulma.min.css">
<script src="/static/htmx.min.js"></script> <script src="/static/htmx.min.js"></script>
<script src="/static/response-targets.js"></script> <script src="/static/response-targets.js"></script>
<style> <style>
.feather { .feather {
width: 24px; width: 24px;
height: 24px; height: 24px;
stroke: currentColor; stroke: currentColor;
stroke-width: 2; stroke-width: 2;
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;
fill: none; fill: none;
} }
[class*=" icon"], [class*=" icon"],
[class^=icon] { [class^=icon] {
display: inline-block; display: inline-block;
width: 1em; width: 1em;
height: 1em; height: 1em;
stroke-width: 0; stroke-width: 0;
stroke: currentColor; stroke: currentColor;
fill: currentColor; fill: currentColor;
line-height: 1; line-height: 1;
position: relative; position: relative;
top: -.05em; top: -.05em;
vertical-align: middle; vertical-align: middle;
} }
</style> </style>
</head> </head>
<body hx-ext="response-targets"> <body hx-ext="response-targets">
{% block body %} {% block body %}
{% endblock %} {% endblock %}
</body> </body>
</html> </html>

View File

@ -0,0 +1,42 @@
{% extends "nav.html" %}
{% block content %}
<section class="section">
<div class="container">
<h3 class="title is-3">
Verfügbarkeiten nach XML exportieren
</h3>
<form action="/export/availabilitydata" 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.as_ref().unwrap() %}
<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>
{% endblock %}

View File

@ -40,7 +40,7 @@
{% if event.canceled %}<b>Veranstaltung abgesagt!</b>{% endif %} {% if event.canceled %}<b>Veranstaltung abgesagt!</b>{% endif %}
<div class="level"> <div class="level">
<h5 class="title is-5 level-left">{{ event.name }}</h5> <h5 class="title is-5 level-left">{{ event.name }}</h5>
<span class ="level-right"> <span class="level-right">
{% if user.role == Role::AreaManager || user.role == Role::Admin %} {% if user.role == Role::AreaManager || user.role == Role::Admin %}
<a href="/assignments/new?event={{ event.id }}" class="button is-primary level-item">Planen</a> <a href="/assignments/new?event={{ event.id }}" class="button is-primary level-item">Planen</a>
<a href="" class="button is-primary-light level-item">bearbeiten</a> <a href="" class="button is-primary-light level-item">bearbeiten</a>
@ -89,24 +89,25 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for availabillity in availabillities %} {% for availabillity in availabillities %}
{% let u = availabillity.user.as_ref().unwrap() %} {% let u = availabillity.user.as_ref().unwrap() %}
<tr id="availabillity-{{ availabillity.id }}"> <tr id="availabillity-{{ availabillity.id }}">
<td>{{ u.name }}</td> <td>{{ u.name }}</td>
<td> <td>
{% match u.function %} {% match u.function %}
{% when Function::Posten %} {% when Function::Posten %}
<span class="tag is-info is-light">Posten</span> <span class="tag is-info is-light">Posten</span>
{% when Function::Wachhabender %} {% when Function::Wachhabender %}
<span class="tag is-info">Wachhabender</span> <span class="tag is-info">Wachhabender</span>
{% else %} {% else %}
{% endmatch %} {% endmatch %}
</td> </td>
<td> <td>
{% if availabillity.start_time.is_some() && availabillity.end_time.is_some() %} {% if availabillity.start_time.is_some() && availabillity.end_time.is_some() %}
{{ availabillity.start_time.as_ref().unwrap().format("%R") }} bis {{ availabillity.end_time.as_ref().unwrap().format("%R") }} {{ availabillity.start_time.as_ref().unwrap().format("%R") }} bis {{
availabillity.end_time.as_ref().unwrap().format("%R") }}
{% else %} {% else %}
ganztägig ganztägig
{% endif %} {% endif %}
</td> </td>
<td> <td>
@ -121,7 +122,7 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -130,52 +131,13 @@
</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) => {
const id = event.target.closest("tr").id.split('-')[1]; const id = event.target.closest("tr").id.split('-')[1];
event.target.classList.add("is-loading"); event.target.classList.add("is-loading");
fetch(`/availabillity/delete/${id}`, { method: "DELETE"}) fetch(`/availabillity/delete/${id}`, {method: "DELETE"})
.then(response => { .then(response => {
if (response.status == 204) { if (response.status == 204) {
document.getElementById(`availabillity-${id}`).remove() document.getElementById(`availabillity-${id}`).remove()

View File

@ -7,65 +7,65 @@
<h1 class="title">Neuen Ort anlegen</h1> <h1 class="title">Neuen Ort anlegen</h1>
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label"> <div class="field-label">
<label class="label">Name</label> <label class="label">Name</label>
</div> </div>
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input" type="text" name="name" required placeholder="Zentralstadion"/> <input class="input" type="text" name="name" required placeholder="Zentralstadion" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% if user.role == Role::Admin %} {% if user.role == Role::Admin %}
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label"> <div class="field-label">
<label class="label">Bereich</label> <label class="label">Bereich</label>
</div> </div>
<div class="field-body"> <div class="field-body">
<div class="field is-narrow"> <div class="field is-narrow">
<div class="control"> <div class="control">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select required name="area"> <select required name="area">
{% for area in areas.as_ref().unwrap() %} {% for area in areas.as_ref().unwrap() %}
<option value="{{ area.id }}">{{ area.name }}</option> <option value="{{ area.id }}">{{ area.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label"></div> <div class="field-label"></div>
<div class="field-body"> <div class="field-body">
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<button class="button is-success"> <button class="button is-success">
<span class="icon"> <span class="icon">
<svg class="feather"> <svg class="feather">
<use href="/static/feather-sprite.svg#check-circle" /> <use href="/static/feather-sprite.svg#check-circle" />
</svg> </svg>
</span> </span>
<span>Speichern</span> <span>Speichern</span>
</button> </button>
</div> </div>
<div class="control"> <div class="control">
<a class="button is-link is-light" hx-boost="true" href="/locations"> <a class="button is-link is-light" hx-boost="true" href="/locations">
<span class="icon"> <span class="icon">
<svg class="feather"> <svg class="feather">
<use href="/static/feather-sprite.svg#arrow-left" /> <use href="/static/feather-sprite.svg#arrow-left" />
</svg> </svg>
</span> </span>
<span>Zurück</span> <span>Zurück</span>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>

View File

@ -69,8 +69,5 @@
{% endif %} {% endif %}
</div> </div>
</section> </section>
</section>
<script>
</script>
{% endblock %} {% endblock %}

View File

@ -4,7 +4,7 @@
<nav class="navbar" role="navigation" aria-label="main navigation"> <nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">
<img src="/static/brass.jpeg" height="240" /> <img src="/static/brass.jpeg" height="240" />
</a> </a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample"> <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
@ -15,43 +15,64 @@
</div> </div>
<div class="navbar-menu"> <div class="navbar-menu">
<div class="navbar-start"> <div hx-boost="true" class="navbar-start">
<a href="/" class="navbar-item"> <a href="/" class="navbar-item">
Kalender Kalender
</a> </a>
{% match user.role %} {% match user.role %}
{% when Role::Staff %} {% when Role::Staff %}
{% when Role::AreaManager %} {% when Role::AreaManager %}
<a class="navbar-item"> <div class="navbar-item has-dropdown is-hoverable">
Planung <div class="navbar-item">
</a> Export
<a href="/locations" class="navbar-item"> </div>
Veranstaltungsorte
</a> <div class="navbar-dropdown">
<a href="/users" class="navbar-item"> <a href="/export/availability" class="navbar-item">
Nutzerverwaltung Verfügbarkeiten
</a> </a>
{% when Role::Admin %} </div>
<a class="navbar-item"> </div>
Planung <a href="/locations" class="navbar-item">
</a> Veranstaltungsorte
<a href="/locations" class="navbar-item"> </a>
Veranstaltungsorte <a href="/users" class="navbar-item">
</a> Nutzerverwaltung
<a href="/users" class="navbar-item"> </a>
Nutzerverwaltung {% when Role::Admin %}
</a> <div class="navbar-item has-dropdown is-hoverable">
<div class="navbar-item">
Export
</div>
<div class="navbar-dropdown">
<a href="/export/availability" class="navbar-item">
Verfügbarkeiten
</a>
</div>
</div>
<a href="/locations" class="navbar-item">
Veranstaltungsorte
</a>
<a href="/users" class="navbar-item">
Nutzerverwaltung
</a>
{% endmatch %} {% endmatch %}
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item"> <div class="navbar-item">
angemeldet als {{ user.name }} angemeldet als {{ user.name }}
<div class="buttons ml-3"> <div class="buttons ml-3">
<a class="button is-success"> <a class="button is-success">
Profil <span class="icon">
<svg class="feather">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
</span>
<span>Profil</span>
</a> </a>
<a href="/logout" class="button is-light"> <a href="/logout" class="button is-light">
Abmelden Abmelden
@ -64,12 +85,13 @@
<noscript> <noscript>
<section class="section"> <section class="section">
<div class="notification is-danger is-light"> <div class="notification is-danger is-light">
Dein Browser unterstützt kein JavaScript oder du hast es deaktiviert. Die Funktionalität ist daher auf nur-Lesen beschränkt. Dein Browser unterstützt kein JavaScript oder du hast es deaktiviert. Die Funktionalität ist daher auf
nur-Lesen beschränkt.
</div> </div>
</section> </section>
</noscript> </noscript>
{% block content %} {% block content %}
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}

View File

@ -2,30 +2,38 @@
{% block body %} {% block body %}
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<h1 class="title">Brass - Anmeldung</h1> <h1 class="title">Brass - Anmeldung</h1>
<form class="box" action="/login" method="post" hx-boost="true" hx-target-400="#error-message" hx-on:change="document.getElementById('error-message').innerHTML = ''"> <form class="box" action="/login" method="post" hx-boost="true" hx-target-400="#error-message"
<div class="field"> hx-on:change="document.getElementById('error-message').innerHTML = ''">
<label class="label" for="email">E-Mail:</label> <div class="field">
<div class="control"> <label class="label" for="email">E-Mail:</label>
<input class="input" placeholder="max.mustermann@example.com" name="email" type="text" required> <div class="control">
</div> <input class="input" placeholder="max.mustermann@example.com" name="email" type="text" required>
</div> </div>
</div>
<div class="field"> <div class="field">
<label class="label" for="password">Passwort:</label> <label class="label" for="password">Passwort:</label>
<div class="control"> <div class="control">
<input class="input" placeholder="**********" name="password" type="password" required> <input class="input" placeholder="**********" name="password" type="password" required>
</div> </div>
</div> </div>
<div id="error-message" class="mb-3 help is-danger"></div> <div id="error-message" class="mb-3 help is-danger"></div>
<div class="level"> <div class="level">
<input class="button is-primary level-left" type="submit" value="Anmelden" /> <button class="button is-primary level-left">
<a class="button is-info is-light level-right" hx-boost="true" href="/reset-password" >Passwort vergessen</a> <span class="icon">
</div> <svg class="feather">
</form> <use href="/static/feather-sprite.svg#log-in" />
</div> </svg>
</span>
<span>Anmelden</span>
</button>
<a class="button is-info is-light level-right" hx-boost="true" href="/reset-password">Passwort vergessen</a>
</div>
</form>
</div>
</section> </section>
{% endblock %} {% endblock %}