feat: rework location view, add icons

This commit is contained in:
Max Hohlfeld 2024-06-22 18:57:44 +02:00
parent 94c67a8b73
commit 4c58ff867d
14 changed files with 211 additions and 54 deletions

View File

@ -1,4 +1,4 @@
use actix_web::{web, Responder};
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
@ -12,6 +12,10 @@ struct NewAreaTemplate {
#[actix_web::get("/area/new")]
async fn get(user: web::ReqData<User>) -> impl Responder {
if user.role != Role::Admin {
return HttpResponse::Unauthorized().finish();
}
let template = NewAreaTemplate {
user: user.into_inner(),
};

View File

@ -1 +1,2 @@
pub mod get_new;
pub mod post_new;

View File

@ -0,0 +1,34 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use serde::Deserialize;
use sqlx::PgPool;
use crate::models::{Area, Role, User};
#[derive(Deserialize)]
struct NewAreaForm {
name: String,
}
#[actix_web::post("/area/new")]
pub async fn post(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
form: web::Form<NewAreaForm>,
) -> impl Responder {
if user.role != Role::Admin {
return HttpResponse::Unauthorized().finish();
}
match Area::create(pool.get_ref(), &form.name).await {
Ok(_) => {
return HttpResponse::Found()
.insert_header((LOCATION, "/locations"))
.insert_header(("HX-LOCATION", "/locations"))
.finish()
}
Err(err) => {
println!("{}", err);
return HttpResponse::InternalServerError().finish();
}
}
}

View File

@ -1,4 +1,3 @@
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
@ -14,20 +13,16 @@ pub struct NewLocationTemplate {
}
#[actix_web::get("/locations/new")]
pub async fn get(user: Identity, pool: web::Data<PgPool>) -> impl Responder {
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
if current_user.role == Role::AreaManager || current_user.role == Role::Admin {
pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl Responder {
if user.role == Role::AreaManager || user.role == Role::Admin {
let mut areas = None;
if current_user.role == Role::Admin {
if user.role == Role::Admin {
areas = Some(Area::read_all(pool.get_ref()).await.unwrap());
}
let template = NewLocationTemplate {
user: current_user,
user: user.into_inner(),
areas,
};

View File

@ -1,4 +1,3 @@
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
@ -10,36 +9,48 @@ use crate::models::{Area, Location, Role, User};
#[template(path = "location/overview.html")]
pub struct LocationsTemplate {
user: User,
grouped_locations: Vec<(Area, Vec<Location>)>
grouped_locations: Vec<(Area, Vec<Location>)>,
}
#[actix_web::get("/locations")]
pub async fn get(user: Identity, pool: web::Data<PgPool>) -> impl Responder {
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()).await.unwrap();
if current_user.role == Role::AreaManager || current_user.role == Role::Admin {
pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl Responder {
if user.role == Role::AreaManager || user.role == Role::Admin {
let mut locations;
let mut grouped_locations: Vec<(Area, Vec<Location>)>;
if current_user.role == Role::Admin {
locations = Location::read_all_including_area(pool.get_ref()).await.unwrap();
if user.role == Role::Admin {
locations = Location::read_all(pool.get_ref()).await.unwrap();
grouped_locations = Area::read_all(pool.get_ref())
.await
.unwrap()
.into_iter()
.map(|a| (a, Vec::new()))
.collect();
} else {
locations = Location::read_by_area(pool.get_ref(), current_user.area_id).await.unwrap();
locations = Location::read_by_area(pool.get_ref(), user.area_id)
.await
.unwrap();
let area = Area::read_by_id(pool.get_ref(), user.area_id)
.await
.unwrap();
grouped_locations = vec![(area, Vec::new())];
}
let mut grouped_locations = Vec::new();
let areas: Vec<Area> = locations.iter().map(|l| l.area.clone().unwrap()).collect();
for area in areas {
let (locations_in_this_area, rest): (Vec<_>, Vec<_>) = locations.into_iter().partition(|l| l.area_id == area.id);
for entry in grouped_locations.iter_mut() {
let (mut locations_in_this_area, rest): (Vec<_>, Vec<_>) =
locations.into_iter().partition(|l| l.area_id == entry.0.id);
locations = rest;
grouped_locations.push((area, locations_in_this_area))
entry.1.append(&mut locations_in_this_area);
}
let template = LocationsTemplate { user: current_user, grouped_locations };
let template = LocationsTemplate {
user: user.into_inner(),
grouped_locations,
};
return template.to_response()
return template.to_response();
}
return HttpResponse::Unauthorized().finish();

View File

@ -1,4 +1,3 @@
use actix_identity::Identity;
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use serde::Deserialize;
use sqlx::PgPool;
@ -12,21 +11,30 @@ pub struct NewLocationForm {
}
#[actix_web::post("/locations/new")]
pub async fn post(user: Identity, pool: web::Data<PgPool>, form: web::Form<NewLocationForm>) -> impl Responder {
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
pub async fn post(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
form: web::Form<NewLocationForm>,
) -> impl Responder {
if current_user.role == Role::AreaManager || current_user.role == Role::Admin {
let mut area_id = current_user.area_id;
if user.role == Role::AreaManager || user.role == Role::Admin {
let mut area_id = user.area_id;
if current_user.role == Role::Admin && form.area.is_some() {
if user.role == Role::Admin && form.area.is_some() {
area_id = form.area.unwrap();
}
match Location::create(pool.get_ref(), &form.name, area_id).await {
Ok(_) => return HttpResponse::Found().insert_header((LOCATION, "/locations")).finish(),
Err(err) => println!("{}", err)
Ok(_) => {
return HttpResponse::Found()
.insert_header((LOCATION, "/locations"))
.insert_header(("HX-LOCATION", "/locations"))
.finish()
}
Err(err) => {
println!("{}", err);
return HttpResponse::InternalServerError().finish();
}
}
}

View File

@ -52,6 +52,7 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(assignment::post_new::post);
cfg.service(area::get_new::get);
cfg.service(area::post_new::post);
cfg.service(get_export::get);
}

View File

@ -40,6 +40,24 @@ impl Location {
Ok(locations)
}
pub async fn read_all(pool: &PgPool) -> anyhow::Result<Vec<Location>> {
let records = query!("SELECT * FROM location")
.fetch_all(pool)
.await?;
let locations = records
.iter()
.map(|lr| Location {
id: lr.id,
name: lr.name.to_string(),
area_id: lr.areaid,
area: None,
})
.collect();
Ok(locations)
}
pub async fn read_all_including_area(pool: &PgPool) -> anyhow::Result<Vec<Location>> {
let records = query!("SELECT location.id AS locationId, location.name, location.areaId, area.id, area.name AS areaName FROM location JOIN area ON location.areaId = area.id;")
.fetch_all(pool)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -13,7 +13,7 @@
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" type="text" name="name" placeholder="Leipzig Ost"/>
<input class="input" type="text" name="name" required placeholder="Leipzig Ost"/>
</div>
</div>
</div>
@ -24,10 +24,24 @@
<div class="field-body">
<div class="field is-grouped">
<div class="control">
<input class="button is-link" type="submit" value="Erstellen">
<button class="button is-success">
<span class="icon">
<svg class="feather">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
</span>
<span>Speichern</span>
</button>
</div>
<div class="control">
<a class="button is-link is-light" hx-boost="true" href="/locations">Zurück</a>
<a class="button is-link is-light" hx-boost="true" href="/locations">
<span class="icon">
<svg class="feather">
<use href="/static/feather-sprite.svg#arrow-left" />
</svg>
</span>
<span>Zurück</span>
</a>
</div>
</div>
</div>

View File

@ -8,11 +8,37 @@
<link rel="stylesheet" href="/static/bulma.min.css">
<script src="/static/htmx.min.js"></script>
<script src="/static/response-targets.js"></script>
<style>
.feather {
width: 24px;
height: 24px;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
[class*=" icon"],
[class^=icon] {
display: inline-block;
width: 1em;
height: 1em;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
line-height: 1;
position: relative;
top: -.05em;
vertical-align: middle;
}
</style>
</head>
<body hx-ext="response-targets">
{% block body %}
{% endblock %}
{% block body %}
{% endblock %}
</body>
</html>

View File

@ -13,7 +13,7 @@
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" type="text" name="name" placeholder="Zentralstadion"/>
<input class="input" type="text" name="name" required placeholder="Zentralstadion"/>
</div>
</div>
</div>
@ -28,7 +28,7 @@
<div class="field is-narrow">
<div class="control">
<div class="select is-fullwidth">
<select name="area">
<select required name="area">
{% for area in areas.as_ref().unwrap() %}
<option value="{{ area.id }}">{{ area.name }}</option>
{% endfor %}
@ -45,10 +45,24 @@
<div class="field-body">
<div class="field is-grouped">
<div class="control">
<input class="button is-link" type="submit" value="Erstellen">
<button class="button is-success">
<span class="icon">
<svg class="feather">
<use href="/static/feather-sprite.svg#check-circle" />
</svg>
</span>
<span>Speichern</span>
</button>
</div>
<div class="control">
<a class="button is-link is-light" href="/locations">Zurück</a>
<a class="button is-link is-light" hx-boost="true" href="/locations">
<span class="icon">
<svg class="feather">
<use href="/static/feather-sprite.svg#arrow-left" />
</svg>
</span>
<span>Zurück</span>
</a>
</div>
</div>
</div>

View File

@ -10,27 +10,57 @@
</h3>
</div>
<div class="level-right">
<a class="button" hx-boost="true" href="/area/new">Neuen Bereich anlegen</a>
<a class="button" hx-boost="true" href="/locations/new">Neuen Ort anlegen</a>
{% if user.role == Role::Admin %}
<a class="button is-link is-light" hx-boost="true" href="/area/new">
<span class="icon">
<svg class="feather">
<use href="/static/feather-sprite.svg#plus-circle" />
</svg>
</span>
<span>Neuen Bereich anlegen</span>
</a>
{% endif %}
<a class="button is-link is-light" hx-boost="true" href="/locations/new">
<span class="icon">
<svg class="feather">
<use href="/static/feather-sprite.svg#plus-circle" />
</svg>
</span>
<span>Neuen Ort anlegen</span>
</a>
</div>
</div>
{% if grouped_locations.len() == 0 %}
<div class="box">
<h5 class="title is-5">keine Orte vorhanden</h5>
<h5 class="title is-5">keine Bereiche gefunden</h5>
</div>
{% else %}
{% for gl in grouped_locations %}
<div class="box">
<h5 class="title is-5">{{ gl.0.name }}</h5>
<h5 class="title is-5">Bereich {{ gl.0.name }}</h5>
{% for l in gl.1 %}
<div class="level">
<div clas="level-left">
<em>{{ l.name }}</em>
</div>
<div clas="level-right">
<a class="button">Bearbeiten</a>
<a class="button">Löschen</a>
<a class="button is-primary is-light">
<span class="icon">
<svg class="feather">
<use href="/static/feather-sprite.svg#edit" />
</svg>
</span>
<span>Bearbeiten</span>
</a>
<a class="button is-danger is-light">
<span class="icon">
<svg class="feather">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
</span>
<span>Löschen</span>
</a>
</div>
</div>
{% endfor %}

View File

@ -50,7 +50,7 @@
<div class="navbar-item">
angemeldet als {{ user.name }}
<div class="buttons ml-3">
<a class="button is-primary">
<a class="button is-success">
Profil
</a>
<a href="/logout" class="button is-light">