feat: basic index and new availabilly page
This commit is contained in:
parent
066cc62262
commit
497e6fbf6c
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -372,6 +372,16 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"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]]
|
[[package]]
|
||||||
name = "askama_derive"
|
name = "askama_derive"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@ -635,6 +645,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
"askama",
|
"askama",
|
||||||
|
"askama_actix",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"serde",
|
"serde",
|
||||||
@ -714,6 +725,7 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets 0.52.0",
|
"windows-targets 0.52.0",
|
||||||
]
|
]
|
||||||
|
@ -8,12 +8,13 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
sqlx = { version = "0.6", features = ["runtime-async-std-rustls", "postgres", "chrono"] }
|
sqlx = { version = "0.6", features = ["runtime-async-std-rustls", "postgres", "chrono"] }
|
||||||
actix-web = { version = "4" }
|
actix-web = { version = "4" }
|
||||||
askama = "0.12.0"
|
askama = { version = "0.12.0", features = ["with-actix-web"] }
|
||||||
serde = { version = "1.0.164", features = [ "derive"]}
|
serde = { version = "1.0.164", features = ["derive"] }
|
||||||
argon2 = { version = "0.5.0", features = [ "std"]}
|
argon2 = { version = "0.5.0", features = [ "std"]}
|
||||||
anyhow = "1.0.71"
|
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 = "0.4.33"
|
chrono = { version = "0.4.33", features = ["serde"] }
|
||||||
actix-files = "0.6.5"
|
actix-files = "0.6.5"
|
||||||
|
askama_actix = "0.14.0"
|
||||||
|
41
src/calendar/get_availabillity_new.rs
Normal file
41
src/calendar/get_availabillity_new.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
mod get_availabillity_new;
|
||||||
|
|
||||||
pub use routes::init;
|
pub use routes::init;
|
||||||
|
@ -1,26 +1,66 @@
|
|||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use chrono::{NaiveDate, Utc};
|
||||||
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
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) {
|
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get_index);
|
cfg.service(get_index);
|
||||||
|
cfg.service(get_availabillity_new);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CalendarQuery {
|
||||||
|
date: Option<NaiveDate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
struct CalendarTemplate {
|
struct CalendarTemplate {
|
||||||
user_role: Role,
|
user_role: Role,
|
||||||
|
date: NaiveDate,
|
||||||
|
area: Area,
|
||||||
|
events: Vec<Event>,
|
||||||
|
availabillities: Vec<Availabillity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/")]
|
#[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 {
|
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())
|
HttpResponse::Ok().body(template.render().unwrap())
|
||||||
} else {
|
} else {
|
||||||
|
@ -12,8 +12,8 @@ impl Area {
|
|||||||
Ok(result.id)
|
Ok(result.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read_by_id(pool: &PgPool, id: i32) -> anyhow::Result<Option<Area>> {
|
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_optional(pool).await?;
|
let record = query_as!(Area, "SELECT * FROM area WHERE id = $1", id).fetch_one(pool).await?;
|
||||||
|
|
||||||
Ok(record)
|
Ok(record)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,38 @@
|
|||||||
|
use sqlx::{query, PgPool};
|
||||||
|
|
||||||
pub struct Location {
|
pub struct Location {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub areaId: i32
|
pub area_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Location {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
110
templates/availabillity_new.html
Normal file
110
templates/availabillity_new.html
Normal 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 %}
|
@ -5,7 +5,7 @@
|
|||||||
<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="bulma.css">
|
<link rel="stylesheet" href="/bulma.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -1,12 +1,74 @@
|
|||||||
{% extends "nav.html" %}
|
{% extends "nav.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="title">
|
<div class="level">
|
||||||
Vefügbarkeit für |datum|
|
<div class="level-left">
|
||||||
</h1>
|
<a class="button is-link level-item" href="/?date={{ date.pred() }}">←</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() }}">→</a>
|
||||||
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user