feat: basic index and new availabilly page

This commit is contained in:
Max Hohlfeld 2024-02-05 23:22:14 +01:00
parent 066cc62262
commit 497e6fbf6c
11 changed files with 354 additions and 18 deletions

12
Cargo.lock generated
View File

@ -372,6 +372,16 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "askama_actix"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4b0dd17cfe203b00ba3853a89fba459ecf24c759b738b244133330607c78e55"
dependencies = [
"actix-web",
"askama",
]
[[package]]
name = "askama_derive"
version = "0.12.1"
@ -635,6 +645,7 @@ dependencies = [
"anyhow",
"argon2",
"askama",
"askama_actix",
"chrono",
"dotenv",
"serde",
@ -714,6 +725,7 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.0",
]

View File

@ -8,12 +8,13 @@ edition = "2021"
[dependencies]
sqlx = { version = "0.6", features = ["runtime-async-std-rustls", "postgres", "chrono"] }
actix-web = { version = "4" }
askama = "0.12.0"
serde = { version = "1.0.164", features = [ "derive"]}
askama = { version = "0.12.0", features = ["with-actix-web"] }
serde = { version = "1.0.164", features = ["derive"] }
argon2 = { version = "0.5.0", features = [ "std"]}
anyhow = "1.0.71"
dotenv = "0.15.0"
actix-session = { version = "0.7.2", features = ["cookie-session"] }
actix-identity = "0.5.2"
chrono = "0.4.33"
chrono = { version = "0.4.33", features = ["serde"] }
actix-files = "0.6.5"
askama_actix = "0.14.0"

View File

@ -0,0 +1,41 @@
use actix_identity::Identity;
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use chrono::NaiveDate;
use serde::Deserialize;
use sqlx::PgPool;
use crate::models::{role::Role, user::User};
#[derive(Template)]
#[template(path = "availabillity_new.html")]
struct AvailabillityNewTemplate {
user_role: Role,
date: NaiveDate
}
#[derive(Deserialize)]
pub struct AvailabillityNewQuery {
date: NaiveDate,
}
#[actix_web::get("/availabillity/new")]
pub async fn get_availabillity_new(
user: Option<Identity>,
pool: web::Data<PgPool>,
query: web::Query<AvailabillityNewQuery>,
) -> impl Responder {
if let Some(user) = user {
let current_user = User::read_by_id(pool.as_ref(), user.id().unwrap().parse().unwrap()).await.unwrap();
let template = AvailabillityNewTemplate { user_role: current_user.role, date: query.date };
template.to_response()
} else {
HttpResponse::PermanentRedirect()
.insert_header((LOCATION, "/login"))
.finish()
}
}

View File

@ -1,3 +1,4 @@
pub mod routes;
mod get_availabillity_new;
pub use routes::init;

View File

@ -1,26 +1,66 @@
use actix_identity::Identity;
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use askama::Template;
use chrono::{NaiveDate, Utc};
use serde::Deserialize;
use sqlx::PgPool;
use crate::models::{role::Role, user::User};
use crate::models::{
area::Area, availabillity::Availabillity, event::Event, role::Role, user::User,
};
use super::get_availabillity_new::get_availabillity_new;
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(get_index);
cfg.service(get_availabillity_new);
}
#[derive(Deserialize)]
pub struct CalendarQuery {
date: Option<NaiveDate>,
}
#[derive(Template)]
#[template(path = "index.html")]
struct CalendarTemplate {
user_role: Role,
date: NaiveDate,
area: Area,
events: Vec<Event>,
availabillities: Vec<Availabillity>,
}
#[actix_web::get("/")]
async fn get_index(user: Option<Identity>, pool: web::Data<PgPool>) -> impl Responder {
async fn get_index(
user: Option<Identity>,
pool: web::Data<PgPool>,
query: web::Query<CalendarQuery>,
) -> impl Responder {
if let Some(user) = user {
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()).await.unwrap();
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
let date = match query.date {
Some(given_date) => given_date,
None => Utc::now().date_naive(),
};
let area = Area::read_by_id(pool.get_ref(), current_user.area_id)
.await
.unwrap();
let events = Event::read_by_date(pool.get_ref(), date).await.unwrap();
let availabillities = Availabillity::read_by_date(pool.get_ref(), date)
.await
.unwrap();
let template = CalendarTemplate{ user_role: current_user.role };
let template = CalendarTemplate {
user_role: current_user.role,
date,
area,
events,
availabillities,
};
HttpResponse::Ok().body(template.render().unwrap())
} else {

View File

@ -12,8 +12,8 @@ impl Area {
Ok(result.id)
}
pub async fn read_by_id(pool: &PgPool, id: i32) -> anyhow::Result<Option<Area>> {
let record = query_as!(Area, "SELECT * FROM area WHERE id = $1", id).fetch_optional(pool).await?;
pub async fn read_by_id(pool: &PgPool, id: i32) -> anyhow::Result<Area> {
let record = query_as!(Area, "SELECT * FROM area WHERE id = $1", id).fetch_one(pool).await?;
Ok(record)
}

View File

@ -0,0 +1,40 @@
use chrono::{NaiveDate, NaiveTime};
use sqlx::{query, PgPool};
pub struct Availabillity {
pub id: i32,
pub user_id: i32,
pub date: NaiveDate,
pub start_time: Option<NaiveTime>,
pub end_time: Option<NaiveTime>
}
impl Availabillity {
pub async fn create(pool: &PgPool, user_id: i32, date: NaiveDate, start_time: Option<NaiveTime>, end_time: Option<NaiveTime>) -> anyhow::Result<i32> {
let mut result = match (start_time, end_time) {
(Some(start_time), Some(end_time)) => query!("INSERT INTO availabillity (userId, date, startTime, endTime) VALUES ($1, $2, $3, $4) RETURNING id;", user_id, date, start_time, end_time).fetch_one(pool).await?.id,
(_, _) => query!("INSERT INTO availabillity (userId, date) VALUES ($1, $2) RETURNING id;", user_id, date).fetch_one(pool).await?.id
};
Ok(result)
}
pub async fn read_by_date(pool: &PgPool, date: NaiveDate) -> anyhow::Result<Vec<Availabillity>> {
let records = query!("SELECT * FROM availabillity WHERE date = $1", date)
.fetch_all(pool)
.await?;
let availabillities = records
.iter()
.map(|a| Availabillity {
id: a.id,
user_id: a.userid,
date: a.date,
start_time: a.starttime,
end_time: a.endtime
})
.collect();
Ok(availabillities)
}
}

View File

@ -1,9 +1,38 @@
use sqlx::{query, PgPool};
pub struct Location {
pub id: i32,
pub name: String,
pub areaId: i32
pub area_id: i32,
}
impl Location {
pub async fn create(pool: &PgPool, name: &str, area_id: i32) -> anyhow::Result<i32> {
let result = query!(
"INSERT INTO location (name, areaId) VALUES ($1, $2) RETURNING id;",
name,
area_id
)
.fetch_one(pool)
.await?;
Ok(result.id)
}
pub async fn read_by_area(pool: &PgPool, area_id: i32) -> anyhow::Result<Vec<Location>> {
let records = query!("SELECT * FROM location WHERE areaId = $1;", area_id)
.fetch_all(pool)
.await?;
let locations = records
.iter()
.map(|lr| Location {
id: lr.id,
name: lr.name.to_string(),
area_id: lr.areaid,
})
.collect();
Ok(locations)
}
}

View File

@ -0,0 +1,110 @@
{% extends "nav.html" %}
{% block content %}
<section class="section">
<div class="container">
<form>
<h1 class="title">Neue Vefügbarkeit für den {{ date.format("%d.%m.%Y") }}</h1>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Funktion</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select">
<select name="function">
<option value="1">Posten</option>
<option value="10">Wachhabender</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Zeitangabe</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<label class="radio">
<input type="radio" id="wholeDay" name="hasTime" checked>
ganztägig
<label class="radio">
<input type="radio" id="partDay" name="hasTime">
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" value="00:00" disabled>
</div>
<div class="field">
<input class="input" type="time" id="till" value="23:59" disabled>
</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="Erstellen">
</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

@ -5,7 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brass - Brasiwa Leipzig</title>
<link rel="stylesheet" href="bulma.css">
<link rel="stylesheet" href="/bulma.css">
</head>
<body>

View File

@ -1,12 +1,74 @@
{% extends "nav.html" %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title">
Vefügbarkeit für |datum|
</h1>
<div class="container">
<div class="level">
<div class="level-left">
<a class="button is-link level-item" href="/?date={{ date.pred() }}">&larr;</a>
</div>
<div class="control level-item is-flex-grow-0">
<input class="input" type="date" value="{{ date }}">
</div>
<div class="level-right">
<a class="button is-link level-item" href="/?date={{ date.succ() }}">&rarr;</a>
</div>
</div>
</section>
</div>
</section>
<section class="section">
<div class="container">
<div class="level">
<div class="level-left">
<h3 class="title is-3">
Events am {{ date.format("%d.%m.%Y") }}
</h3>
</div>
<div class="level-right">
<a class="button" href="/event/new">Neues Event für diesen Tag</a>
</div>
</div>
{% if events.len() == 0 %}
<div class="box">
<h5 class="title is-5">keine Events geplant</h5>
</div>
{% else %}
{% for event in events %}
<div class="box">
<h5 class="title is-5">{{ event.name }}</h5>
</div>
{% endfor %}
{% endif %}
</div>
</section>
<section class="section">
<div class="container">
<div class="level">
<div class="level-left">
<h3 class="title is-3">
Verfügbarkeiten am {{ date.format("%d.%m.%Y") }}
</h3>
</div>
<div class="level-right">
<a class="button" href="/availabillity/new?date={{ date }}">Neue Verfügbarkeit für diesen Tag</a>
</div>
</div>
{% if events.len() == 0 %}
<div class="box">
<h5 class="title is-5">keine Verfügbarkeiten eingetragen</h5>
</div>
{% else %}
{% for availabillity in availabillities %}
<div class="box">
<h5 class="title is-5">{{ availabillity.user_id }}</h5>
</div>
{% endfor %}
{% endif %}
</div>
</section>
{% endblock %}