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::Template;
|
||||||
use askama_actix::TemplateToResponse;
|
use askama_actix::TemplateToResponse;
|
||||||
|
|
||||||
@ -12,6 +12,10 @@ struct NewAreaTemplate {
|
|||||||
|
|
||||||
#[actix_web::get("/area/new")]
|
#[actix_web::get("/area/new")]
|
||||||
async fn get(user: web::ReqData<User>) -> impl Responder {
|
async fn get(user: web::ReqData<User>) -> impl Responder {
|
||||||
|
if user.role != Role::Admin {
|
||||||
|
return HttpResponse::Unauthorized().finish();
|
||||||
|
}
|
||||||
|
|
||||||
let template = NewAreaTemplate {
|
let template = NewAreaTemplate {
|
||||||
user: user.into_inner(),
|
user: user.into_inner(),
|
||||||
};
|
};
|
||||||
|
@ -1 +1,2 @@
|
|||||||
pub mod get_new;
|
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 actix_web::{web, HttpResponse, Responder};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_actix::TemplateToResponse;
|
use askama_actix::TemplateToResponse;
|
||||||
@ -14,20 +13,16 @@ pub struct NewLocationTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/locations/new")]
|
#[actix_web::get("/locations/new")]
|
||||||
pub async fn get(user: Identity, pool: web::Data<PgPool>) -> impl Responder {
|
pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl Responder {
|
||||||
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
|
if user.role == Role::AreaManager || user.role == Role::Admin {
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if current_user.role == Role::AreaManager || current_user.role == Role::Admin {
|
|
||||||
let mut areas = None;
|
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());
|
areas = Some(Area::read_all(pool.get_ref()).await.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
let template = NewLocationTemplate {
|
let template = NewLocationTemplate {
|
||||||
user: current_user,
|
user: user.into_inner(),
|
||||||
areas,
|
areas,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use actix_identity::Identity;
|
|
||||||
use actix_web::{web, HttpResponse, Responder};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_actix::TemplateToResponse;
|
use askama_actix::TemplateToResponse;
|
||||||
@ -10,36 +9,48 @@ use crate::models::{Area, Location, Role, User};
|
|||||||
#[template(path = "location/overview.html")]
|
#[template(path = "location/overview.html")]
|
||||||
pub struct LocationsTemplate {
|
pub struct LocationsTemplate {
|
||||||
user: User,
|
user: User,
|
||||||
grouped_locations: Vec<(Area, Vec<Location>)>
|
grouped_locations: Vec<(Area, Vec<Location>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/locations")]
|
#[actix_web::get("/locations")]
|
||||||
pub async fn get(user: Identity, pool: web::Data<PgPool>) -> impl Responder {
|
pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl Responder {
|
||||||
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()).await.unwrap();
|
if user.role == Role::AreaManager || user.role == Role::Admin {
|
||||||
|
|
||||||
if current_user.role == Role::AreaManager || current_user.role == Role::Admin {
|
|
||||||
let mut locations;
|
let mut locations;
|
||||||
|
let mut grouped_locations: Vec<(Area, Vec<Location>)>;
|
||||||
|
|
||||||
if current_user.role == Role::Admin {
|
if user.role == Role::Admin {
|
||||||
locations = Location::read_all_including_area(pool.get_ref()).await.unwrap();
|
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 {
|
} 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();
|
for entry in grouped_locations.iter_mut() {
|
||||||
|
let (mut locations_in_this_area, rest): (Vec<_>, Vec<_>) =
|
||||||
let areas: Vec<Area> = locations.iter().map(|l| l.area.clone().unwrap()).collect();
|
locations.into_iter().partition(|l| l.area_id == entry.0.id);
|
||||||
|
|
||||||
for area in areas {
|
|
||||||
let (locations_in_this_area, rest): (Vec<_>, Vec<_>) = locations.into_iter().partition(|l| l.area_id == area.id);
|
|
||||||
locations = rest;
|
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();
|
return HttpResponse::Unauthorized().finish();
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use actix_identity::Identity;
|
|
||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@ -12,21 +11,30 @@ pub struct NewLocationForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/locations/new")]
|
#[actix_web::post("/locations/new")]
|
||||||
pub async fn post(user: Identity, pool: web::Data<PgPool>, form: web::Form<NewLocationForm>) -> impl Responder {
|
pub async fn post(
|
||||||
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
|
user: web::ReqData<User>,
|
||||||
.await
|
pool: web::Data<PgPool>,
|
||||||
.unwrap();
|
form: web::Form<NewLocationForm>,
|
||||||
|
) -> impl Responder {
|
||||||
|
|
||||||
if current_user.role == Role::AreaManager || current_user.role == Role::Admin {
|
if user.role == Role::AreaManager || user.role == Role::Admin {
|
||||||
let mut area_id = current_user.area_id;
|
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();
|
area_id = form.area.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
match Location::create(pool.get_ref(), &form.name, area_id).await {
|
match Location::create(pool.get_ref(), &form.name, area_id).await {
|
||||||
Ok(_) => return HttpResponse::Found().insert_header((LOCATION, "/locations")).finish(),
|
Ok(_) => {
|
||||||
Err(err) => println!("{}", err)
|
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(assignment::post_new::post);
|
||||||
|
|
||||||
cfg.service(area::get_new::get);
|
cfg.service(area::get_new::get);
|
||||||
|
cfg.service(area::post_new::post);
|
||||||
|
|
||||||
cfg.service(get_export::get);
|
cfg.service(get_export::get);
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,24 @@ impl Location {
|
|||||||
Ok(locations)
|
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>> {
|
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;")
|
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)
|
.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-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -24,10 +24,24 @@
|
|||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<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>
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,6 +8,32 @@
|
|||||||
<link rel="stylesheet" href="/static/bulma.min.css">
|
<link rel="stylesheet" href="/static/bulma.min.css">
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<script src="/static/response-targets.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>
|
</head>
|
||||||
|
|
||||||
<body hx-ext="response-targets">
|
<body hx-ext="response-targets">
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<div class="field is-narrow">
|
<div class="field is-narrow">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select name="area">
|
<select required name="area">
|
||||||
{% for area in areas.as_ref().unwrap() %}
|
{% for area in areas.as_ref().unwrap() %}
|
||||||
<option value="{{ area.id }}">{{ area.name }}</option>
|
<option value="{{ area.id }}">{{ area.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -45,10 +45,24 @@
|
|||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<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>
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,27 +10,57 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<a class="button" hx-boost="true" href="/area/new">Neuen Bereich anlegen</a>
|
{% if user.role == Role::Admin %}
|
||||||
<a class="button" hx-boost="true" href="/locations/new">Neuen Ort anlegen</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if grouped_locations.len() == 0 %}
|
{% if grouped_locations.len() == 0 %}
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h5 class="title is-5">keine Orte vorhanden</h5>
|
<h5 class="title is-5">keine Bereiche gefunden</h5>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for gl in grouped_locations %}
|
{% for gl in grouped_locations %}
|
||||||
<div class="box">
|
<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 %}
|
{% for l in gl.1 %}
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<div clas="level-left">
|
<div clas="level-left">
|
||||||
<em>{{ l.name }}</em>
|
<em>{{ l.name }}</em>
|
||||||
</div>
|
</div>
|
||||||
<div clas="level-right">
|
<div clas="level-right">
|
||||||
<a class="button">Bearbeiten</a>
|
<a class="button is-primary is-light">
|
||||||
<a class="button">Löschen</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
angemeldet als {{ user.name }}
|
angemeldet als {{ user.name }}
|
||||||
<div class="buttons ml-3">
|
<div class="buttons ml-3">
|
||||||
<a class="button is-primary">
|
<a class="button is-success">
|
||||||
Profil
|
Profil
|
||||||
</a>
|
</a>
|
||||||
<a href="/logout" class="button is-light">
|
<a href="/logout" class="button is-light">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user