feat: crud for availabillity

This commit is contained in:
Max Hohlfeld 2024-02-12 23:20:54 +01:00
parent 8840f0ab48
commit b736b04ced
9 changed files with 440 additions and 29 deletions

View File

@ -0,0 +1,28 @@
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use serde::Deserialize;
use sqlx::PgPool;
use crate::models::availabillity::Availabillity;
#[derive(Deserialize)]
pub struct AvailabillityPath {
pub id: i32
}
#[actix_web::delete("/availabillity/delete/{id}")]
pub async fn delete_availabillity(user: Identity, pool: web::Data<PgPool>, path: web::Path<AvailabillityPath>) -> impl Responder {
if let Ok(current_user_id) = user.id() {
let current_user_id: i32 = current_user_id.parse().unwrap();
if let Ok(availabillity_in_db) = Availabillity::read_by_id(pool.get_ref(), path.id).await {
if availabillity_in_db.user_id == current_user_id {
if let Ok(_) = Availabillity::delete(pool.get_ref(), availabillity_in_db.id).await {
return HttpResponse::NoContent().finish();
}
}
}
}
return HttpResponse::BadRequest().finish();
}

View File

@ -1,5 +1,7 @@
pub mod routes;
mod get_availabillity_new;
mod post_availabillity;
mod delete_availabillity;
mod update_availabillity;
pub use routes::init;

View File

@ -8,10 +8,10 @@ use crate::models::{availabillity::Availabillity, user::User};
#[derive(Deserialize)]
pub struct AvailabillityForm {
date: NaiveDate,
start_time: Option<NaiveTime>,
end_time: Option<NaiveTime>,
comment: Option<String>
pub date: NaiveDate,
pub from: Option<NaiveTime>,
pub till: Option<NaiveTime>,
pub comment: Option<String>
}
#[actix_web::post("/availabillity/new")]
@ -24,7 +24,7 @@ pub async fn post_availabillity(
.await
.unwrap();
if let Ok(_) = Availabillity::create(pool.get_ref(), current_user.id, form.date, form.start_time, form.end_time, form.comment.clone()).await {
if let Ok(_) = Availabillity::create(pool.get_ref(), current_user.id, form.date, form.from, form.till, form.comment.clone()).await {
HttpResponse::Found()
.insert_header((LOCATION, "/"))
.finish()

View File

@ -1,20 +1,23 @@
use actix_identity::Identity;
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use chrono::{NaiveDate, Utc};
use serde::Deserialize;
use sqlx::PgPool;
use crate::models::{
area::Area, availabillity::Availabillity, event::Event, role::Role, user::User,
area::Area, availabillity::Availabillity, event::Event, function, role::Role, user::User
};
use super::{get_availabillity_new::get_availabillity_new, post_availabillity::post_availabillity};
use super::{delete_availabillity::delete_availabillity, get_availabillity_new::get_availabillity_new, post_availabillity::post_availabillity, update_availabillity::{get_update_availabillity, post_update_availabillity}};
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(get_index);
cfg.service(get_availabillity_new);
cfg.service(post_availabillity);
cfg.service(delete_availabillity);
cfg.service(get_update_availabillity);
cfg.service(post_update_availabillity);
}
#[derive(Deserialize)]
@ -26,12 +29,15 @@ pub struct CalendarQuery {
#[template(path = "index.html")]
struct CalendarTemplate {
user_role: Role,
current_user_id: i32,
date: NaiveDate,
area: Area,
events: Vec<Event>,
availabillities: Vec<Availabillity>,
}
type Function = function::Function;
#[actix_web::get("/")]
async fn get_index(
user: Identity,
@ -49,16 +55,13 @@ async fn get_index(
.await
.unwrap();
let events = Event::read_by_date(pool.get_ref(), date).await.unwrap();
let mut availabillities = Availabillity::read_by_date(pool.get_ref(), date)
let availabillities = Availabillity::read_by_date_including_user(pool.get_ref(), date)
.await
.unwrap();
for avl in availabillities.iter_mut() {
avl.load_user(pool.get_ref()).await.unwrap()
}
let template = CalendarTemplate {
user_role: current_user.role,
current_user_id: current_user.id,
date,
area,
events,

View File

@ -0,0 +1,110 @@
use actix_identity::Identity;
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use chrono::{NaiveDate, NaiveTime};
use sqlx::PgPool;
use crate::{calendar::post_availabillity::AvailabillityForm, models::{availabillity::Availabillity, role::Role, user::User}};
use super::delete_availabillity::AvailabillityPath;
#[derive(Template)]
#[template(path = "availabillity_edit.html")]
pub struct AvailabillityEditTemplate {
user_role: Role,
date: NaiveDate,
id: i32,
start_time: String,
end_time: String,
has_time: bool,
comment: String,
}
#[actix_web::get("/availabillity/edit/{id}")]
pub async fn get_update_availabillity(
user: Identity,
pool: web::Data<PgPool>,
path: web::Path<AvailabillityPath>,
) -> impl Responder {
let current_user = User::read_by_id(pool.as_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
if let Ok(availabillity) = Availabillity::read_by_id(pool.get_ref(), path.id).await {
if availabillity.user_id == user.id().unwrap().parse::<i32>().unwrap() {
let start_time = availabillity
.start_time
.unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
.format("%R")
.to_string();
let end_time = availabillity
.end_time
.unwrap_or(NaiveTime::from_hms_opt(23, 59, 0).unwrap())
.format("%R")
.to_string();
let has_time = availabillity.start_time.is_some() && availabillity.end_time.is_some();
let comment = availabillity.comment.unwrap_or(String::new());
let template = AvailabillityEditTemplate {
user_role: current_user.role,
date: availabillity.date,
id: path.id,
start_time,
end_time,
has_time,
comment
};
return template.to_response();
}
}
HttpResponse::BadRequest().body("Availabillity with this id doesn't exist.")
}
#[actix_web::post("/availabillity/edit/{id}")]
pub async fn post_update_availabillity(
user: Identity,
pool: web::Data<PgPool>,
path: web::Path<AvailabillityPath>,
form: web::Form<AvailabillityForm>
) -> impl Responder {
let current_user = User::read_by_id(pool.as_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
if let Ok(mut availabillity) = Availabillity::read_by_id(pool.get_ref(), path.id).await {
if availabillity.user_id == user.id().unwrap().parse::<i32>().unwrap() {
let mut has_changed = false;
if availabillity.start_time != form.from {
availabillity.start_time = form.from;
has_changed = true;
}
if availabillity.end_time != form.till {
availabillity.end_time = form.till;
has_changed = true;
}
if availabillity.comment != form.comment {
availabillity.comment = form.comment.clone();
has_changed = true;
}
if has_changed {
if let Ok(_) = Availabillity::update(pool.get_ref(), path.id, &availabillity).await {
return HttpResponse::Found()
.insert_header((LOCATION, "/"))
.finish();
}
}
}
}
HttpResponse::BadRequest().body("Fehler beim erstellen")
}

View File

@ -1,7 +1,7 @@
use chrono::{NaiveDate, NaiveTime};
use sqlx::{query, PgPool};
use super::user::User;
use super::{function::Function, role::Role, user::User};
pub struct Availabillity {
pub id: i32,
@ -56,10 +56,106 @@ impl Availabillity {
Ok(availabillities)
}
pub async fn load_user(&mut self, pool: &PgPool) -> anyhow::Result<()> {
let user = User::read_by_id(pool, self.user_id).await?;
self.user = Some(user);
pub async fn read_by_date_including_user(
pool: &PgPool,
date: NaiveDate,
) -> 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
FROM availabillity
JOIN user_ ON availabillity.userId = user_.id
WHERE availabillity.date = $1;
"##,
date
)
.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,
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 read_by_id(pool: &PgPool, id: i32) -> anyhow::Result<Availabillity> {
let record = query!("SELECT * FROM availabillity WHERE id = $1", id)
.fetch_one(pool)
.await?;
let availabillity = Availabillity {
id: record.id,
user_id: record.userid,
user: None,
date: record.date,
start_time: record.starttime,
end_time: record.endtime,
comment: record.comment.clone(),
};
Ok(availabillity)
}
pub async fn update(
pool: &PgPool,
id: i32,
updated_availabillity: &Availabillity,
) -> anyhow::Result<()> {
query!(
"UPDATE availabillity SET startTime = $1, endTime = $2, comment = $3 WHERE id = $4",
updated_availabillity.start_time,
updated_availabillity.end_time,
updated_availabillity.comment,
id
)
.execute(pool)
.await?;
Ok(())
}
pub async fn delete(pool: &PgPool, id: i32) -> anyhow::Result<()> {
query!("DELETE FROM availabillity WHERE id = $1", id)
.execute(pool)
.await?;
Ok(())
}
}

View File

@ -0,0 +1,117 @@
{% extends "nav.html" %}
{% block content %}
<section class="section">
<div class="container">
<form method="post" action="/availabillity/edit/{{ id }}">
<h1 class="title">Bearbeite Vefügbarkeit für den {{ date.format("%d.%m.%Y") }}</h1>
<input type="hidden" name="date" value="{{ date }}">
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Dauer</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<label class="radio">
{% if has_time %}
<input type="radio" id="wholeDay" name="hasTime">
{% else %}
<input type="radio" id="wholeDay" name="hasTime" checked>
{% endif %}
ganztägig
<label class="radio">
{% if has_time %}
<input type="radio" id="partDay" name="hasTime" checked>
{% else %}
<input type="radio" id="partDay" name="hasTime">
{% endif %}
zeitweise
</label>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Von - Bis</label>
</div>
<div class="field-body">
<div class="field">
<input class="input" type="time" id="from" name="from" value="{{ start_time }}" disabled>
</div>
<div class="field">
<input class="input" type="time" id="till" name="till" value="{{ end_time }}" disabled>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Kommentar</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<textarea class="textarea" name="comment" placeholder="nur Posten, nur Wachhabender, etc..">{{ comment }}</textarea>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label"></div>
<div class="field-body">
<div class="field is-grouped">
<div class="control">
<input class="button is-link" type="submit" value="Speichern">
</div>
<div class="control">
<a class="button is-link is-light" href="/?date={{ date }}">Zurück</a>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
<script>
const wholeDay = document.getElementById("wholeDay");
const partDay = document.getElementById("partDay");
const from = document.getElementById("from");
const till = document.getElementById("till");
let lastFrom = null;
let lastTill = null;
wholeDay.addEventListener("click", (event) => {
from.disabled = true
till.disabled = true
lastFrom = from.value;
lastTill = till.value;
from.value = null;
till.value = null;
});
partDay.addEventListener("click", (event) => {
from.disabled = false
till.disabled = false
if (lastFrom != null) {
from.value = lastFrom;
}
if (lastTill != null) {
till.value = lastTill;
}
});
</script>
{% endblock %}

View File

@ -33,10 +33,10 @@
</div>
<div class="field-body">
<div class="field">
<input class="input" type="time" id="from" value="00:00" disabled>
<input class="input" type="time" id="from" name="from" value="00:00" disabled>
</div>
<div class="field">
<input class="input" type="time" id="till" value="23:59" disabled>
<input class="input" type="time" id="till" name="till" value="23:59" disabled>
</div>
</div>
</div>
@ -48,7 +48,7 @@
<div class="field-body">
<div class="field">
<div class="control">
<textarea class="textarea" placeholder="nur Posten, nur Wachhabender, etc.."></textarea>
<textarea class="textarea" name="comment" placeholder="nur Posten, nur Wachhabender, etc.."></textarea>
</div>
</div>
</div>

View File

@ -62,19 +62,74 @@
<h5 class="title is-5">keine Verfügbarkeiten eingetragen</h5>
</div>
{% else %}
{% for availabillity in availabillities %}
{% let user = availabillity.user.as_ref().unwrap() %}
<div class="box">
<p>{{ user.name }}</p>
<p>{{ user.function }}</p>
{% if availabillity.start_time.is_some() && availabillity.end_time.is_some() %}
<p>{{ availabillity.start_time.as_ref().unwrap() }}</p>
{% endif %}
<table class="table is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Funktion</th>
<th>Zeitraum</th>
<th>Kommentar</th>
<th></th>
</tr>
</thead>
<tbody>
{% for availabillity in availabillities %}
{% let user = availabillity.user.as_ref().unwrap() %}
<tr id="availabillity-{{ availabillity.id }}">
<td>{{ user.name }}</td>
<td>
{% match user.function %}
{% when Function::Posten %}
<span class="tag is-info is-light">Posten</span>
{% when Function::Wachhabender %}
<span class="tag is-info">Wachhabender</span>
{% else %}
{% endmatch %}
</td>
<td>
{% 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") }}
{% else %}
ganztägig
{% endif %}
</td>
<td>
{{ availabillity.comment.as_deref().unwrap_or("") }}
</td>
<td>
{% if availabillity.user_id == current_user_id %}
<div class="buttons is-right">
<a class="button is-link" href="/availabillity/edit/{{ availabillity.id }}">Bearbeiten</a>
<button class="button is-danger" name="delete-availabillity">Löschen</button>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
{% endif %}
</div>
</section>
<script>
document.getElementsByName("delete-availabillity")
.forEach(ele => ele.addEventListener("click", (event) => {
const id = event.target.closest("tr").id.split('-')[1];
event.target.classList.add("is-loading");
fetch(`/availabillity/delete/${id}`, { method: "DELETE"})
.then(response => {
if (response.status == 204) {
document.getElementById(`availabillity-${id}`).remove()
} else {
event.target.classList.remove("is-loading");
console.log("Fehler beim Löschen.")
}
});
}));
</script>
{% endblock %}