feat: rework location view, add icons
This commit is contained in:
parent
94c67a8b73
commit
4c58ff867d
@ -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(),
|
||||
};
|
||||
|
@ -1 +1,2 @@
|
||||
pub mod get_new;
|
||||
pub mod post_new;
|
||||
|
34
src/endpoints/area/post_new.rs
Normal file
34
src/endpoints/area/post_new.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
1
static/feather-sprite.svg
Normal file
1
static/feather-sprite.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 59 KiB |
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user