2023-05-08 22:31:01 +02:00
|
|
|
use axum::{
|
2023-05-11 16:51:57 +02:00
|
|
|
extract::{Path, Query, State},
|
2023-08-29 21:34:00 +02:00
|
|
|
http::header::{self, HeaderMap, HeaderName, HeaderValue},
|
2023-08-29 21:34:00 +02:00
|
|
|
response::{IntoResponse, Redirect},
|
2023-05-08 22:31:01 +02:00
|
|
|
routing::{get, post},
|
2023-05-10 00:42:42 +02:00
|
|
|
Form, Router,
|
2023-05-08 22:31:01 +02:00
|
|
|
};
|
2023-05-10 00:42:42 +02:00
|
|
|
|
|
|
|
|
use serde::Deserialize;
|
2023-05-08 00:05:45 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
use uuid::Uuid;
|
2023-05-08 00:05:45 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
use std::fmt;
|
2023-08-29 21:34:00 +02:00
|
|
|
use std::net::{IpAddr, SocketAddr};
|
|
|
|
|
use std::str::FromStr;
|
2023-05-08 00:05:45 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
mod error;
|
2023-08-29 21:34:00 +02:00
|
|
|
mod html;
|
2023-05-08 00:05:45 +02:00
|
|
|
mod models;
|
2023-08-29 21:34:00 +02:00
|
|
|
mod sqlite;
|
2023-08-29 21:34:00 +02:00
|
|
|
mod view;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
|
|
|
|
use error::{Error, RequestError, StartError};
|
2023-05-08 00:05:45 +02:00
|
|
|
|
2023-05-10 00:42:42 +02:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct AppState {
|
2023-08-29 21:34:00 +02:00
|
|
|
database_pool: sqlite::Pool<sqlite::Sqlite>,
|
2023-05-10 00:42:42 +02:00
|
|
|
client_state: ClientState,
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
use clap::Parser;
|
|
|
|
|
|
|
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
|
#[command(author, version, about, long_about = None)]
|
|
|
|
|
struct Args {
|
|
|
|
|
#[arg(long)]
|
2023-08-29 21:34:00 +02:00
|
|
|
database_url: String,
|
|
|
|
|
#[arg(long, default_value_t = 3000)]
|
|
|
|
|
port: u16,
|
2023-08-29 21:34:00 +02:00
|
|
|
#[arg(long)]
|
|
|
|
|
bind: String,
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
2023-05-10 00:42:42 +02:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct ClientState {
|
2023-05-08 22:31:01 +02:00
|
|
|
pub active_category_id: Option<Uuid>,
|
2023-05-11 16:51:57 +02:00
|
|
|
pub edit_item: Option<Uuid>,
|
2023-08-29 21:34:00 +02:00
|
|
|
pub trip_edit_attribute: Option<models::trips::TripAttribute>,
|
2023-08-29 21:33:59 +02:00
|
|
|
pub trip_type_edit: Option<Uuid>,
|
2023-05-08 00:05:45 +02:00
|
|
|
}
|
|
|
|
|
|
2023-05-10 00:42:42 +02:00
|
|
|
impl ClientState {
|
2023-05-08 00:05:45 +02:00
|
|
|
pub fn new() -> Self {
|
2023-05-10 00:42:42 +02:00
|
|
|
ClientState {
|
2023-05-08 22:31:01 +02:00
|
|
|
active_category_id: None,
|
2023-05-11 16:51:57 +02:00
|
|
|
edit_item: None,
|
2023-08-29 21:33:59 +02:00
|
|
|
trip_edit_attribute: None,
|
2023-08-29 21:33:59 +02:00
|
|
|
trip_type_edit: None,
|
2023-05-08 00:05:45 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-10 00:48:25 +02:00
|
|
|
impl Default for ClientState {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::new()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
struct UriPath(String);
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for UriPath {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
|
write!(f, "{}", self.0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> Into<&'a str> for &'a UriPath {
|
|
|
|
|
fn into(self) -> &'a str {
|
|
|
|
|
self.0.as_str()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
|
|
|
pub enum TopLevelPage {
|
|
|
|
|
Inventory,
|
|
|
|
|
Trips,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TopLevelPage {
|
|
|
|
|
fn id(&self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Inventory => "inventory",
|
|
|
|
|
Self::Trips => "trips",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn path(&self) -> UriPath {
|
|
|
|
|
UriPath(
|
|
|
|
|
match self {
|
|
|
|
|
Self::Inventory => "/inventory/",
|
|
|
|
|
Self::Trips => "/trips/",
|
|
|
|
|
}
|
|
|
|
|
.to_string(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Inventory => "Inventory",
|
|
|
|
|
Self::Trips => "Trips",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
enum HtmxEvents {
|
|
|
|
|
TripItemEdited,
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
impl From<HtmxEvents> for HeaderValue {
|
|
|
|
|
fn from(val: HtmxEvents) -> Self {
|
|
|
|
|
HeaderValue::from_static(val.to_str())
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HtmxEvents {
|
2023-08-29 21:34:00 +02:00
|
|
|
fn to_str(&self) -> &'static str {
|
2023-08-29 21:34:00 +02:00
|
|
|
match self {
|
|
|
|
|
Self::TripItemEdited => "TripItemEdited",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum HtmxResponseHeaders {
|
|
|
|
|
Trigger,
|
2023-08-29 21:34:00 +02:00
|
|
|
PushUrl,
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
impl From<HtmxResponseHeaders> for HeaderName {
|
|
|
|
|
fn from(val: HtmxResponseHeaders) -> Self {
|
|
|
|
|
match val {
|
|
|
|
|
HtmxResponseHeaders::Trigger => HeaderName::from_static("hx-trigger"),
|
|
|
|
|
HtmxResponseHeaders::PushUrl => HeaderName::from_static("hx-push-url"),
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum HtmxRequestHeaders {
|
|
|
|
|
HtmxRequest,
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
impl From<HtmxRequestHeaders> for HeaderName {
|
|
|
|
|
fn from(val: HtmxRequestHeaders) -> Self {
|
|
|
|
|
match val {
|
|
|
|
|
HtmxRequestHeaders::HtmxRequest => HeaderName::from_static("hx-request"),
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-08 00:05:45 +02:00
|
|
|
#[tokio::main]
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn main() -> Result<(), StartError> {
|
2023-05-08 22:31:01 +02:00
|
|
|
tracing_subscriber::fmt()
|
|
|
|
|
.with_max_level(tracing::Level::DEBUG)
|
|
|
|
|
.init();
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let args = Args::parse();
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let database_pool = sqlite::init_database_pool(&args.database_url).await?;
|
|
|
|
|
sqlite::migrate(&database_pool).await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-05-10 00:42:42 +02:00
|
|
|
let state = AppState {
|
|
|
|
|
database_pool,
|
|
|
|
|
client_state: ClientState::new(),
|
|
|
|
|
};
|
|
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
let icon_handler = || async {
|
|
|
|
|
(
|
|
|
|
|
[(header::CONTENT_TYPE, "image/svg+xml")],
|
|
|
|
|
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/luggage.svg")),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
|
2023-05-08 00:05:45 +02:00
|
|
|
// build our application with a route
|
|
|
|
|
let app = Router::new()
|
2023-08-29 21:33:59 +02:00
|
|
|
.route("/favicon.svg", get(icon_handler))
|
|
|
|
|
.route("/assets/luggage.svg", get(icon_handler))
|
2023-05-08 00:05:45 +02:00
|
|
|
.route("/", get(root))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route(
|
|
|
|
|
"/notfound",
|
|
|
|
|
get(|| async {
|
|
|
|
|
Error::Request(RequestError::NotFound {
|
|
|
|
|
message: "hi".to_string(),
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.nest(
|
2023-08-29 21:34:00 +02:00
|
|
|
(&TopLevelPage::Trips.path()).into(),
|
2023-08-29 21:34:00 +02:00
|
|
|
Router::new()
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/", get(trips).post(trip_create))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/types/", get(trips_types).post(trip_type_create))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/types/:id/edit/name/submit", post(trips_types_edit_name))
|
|
|
|
|
.route("/:id/", get(trip))
|
|
|
|
|
.route("/:id/comment/submit", post(trip_comment_set))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/:id/categories/:id/select", post(trip_category_select))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/:id/packagelist/", get(trip_packagelist))
|
|
|
|
|
.route(
|
|
|
|
|
"/:id/packagelist/item/:id/pack",
|
|
|
|
|
post(trip_item_packagelist_set_pack_htmx),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/:id/packagelist/item/:id/unpack",
|
|
|
|
|
post(trip_item_packagelist_set_unpack_htmx),
|
|
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.route(
|
|
|
|
|
"/:id/packagelist/item/:id/ready",
|
|
|
|
|
post(trip_item_packagelist_set_ready_htmx),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/:id/packagelist/item/:id/unready",
|
|
|
|
|
post(trip_item_packagelist_set_unready_htmx),
|
|
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/:id/state/:id", post(trip_state_set))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/:id/total_weight", get(trip_total_weight_htmx))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/:id/type/:id/add", get(trip_type_add))
|
|
|
|
|
.route("/:id/type/:id/remove", get(trip_type_remove))
|
|
|
|
|
.route("/:id/edit/:attribute/submit", post(trip_edit_attribute))
|
|
|
|
|
.route(
|
|
|
|
|
"/:id/items/:id/pick",
|
|
|
|
|
get(trip_item_set_pick).post(trip_item_set_pick_htmx),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/:id/items/:id/unpick",
|
|
|
|
|
get(trip_item_set_unpick).post(trip_item_set_unpick_htmx),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/:id/items/:id/pack",
|
|
|
|
|
get(trip_item_set_pack).post(trip_item_set_pack_htmx),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/:id/items/:id/unpack",
|
|
|
|
|
get(trip_item_set_unpack).post(trip_item_set_unpack_htmx),
|
2023-08-29 21:34:00 +02:00
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/:id/items/:id/ready",
|
|
|
|
|
get(trip_item_set_ready).post(trip_item_set_ready_htmx),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/:id/items/:id/unready",
|
|
|
|
|
get(trip_item_set_unready).post(trip_item_set_unready_htmx),
|
2023-08-29 21:34:00 +02:00
|
|
|
),
|
2023-08-29 21:34:00 +02:00
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.nest(
|
2023-08-29 21:34:00 +02:00
|
|
|
(&TopLevelPage::Inventory.path()).into(),
|
2023-08-29 21:34:00 +02:00
|
|
|
Router::new()
|
|
|
|
|
.route("/", get(inventory_inactive))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/categories/:id/select", post(inventory_category_select))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/category/", post(inventory_category_create))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/category/:id/", get(inventory_active))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/item/", post(inventory_item_create))
|
|
|
|
|
.route("/item/:id/", get(inventory_item))
|
|
|
|
|
.route("/item/:id/cancel", get(inventory_item_cancel))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/item/:id/delete", get(inventory_item_delete))
|
|
|
|
|
.route("/item/:id/edit", post(inventory_item_edit))
|
2023-08-29 21:34:00 +02:00
|
|
|
.route("/item/name/validate", post(inventory_item_validate_name)),
|
2023-08-29 21:33:59 +02:00
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.fallback(|| async {
|
|
|
|
|
Error::Request(RequestError::NotFound {
|
|
|
|
|
message: "no route found".to_string(),
|
|
|
|
|
})
|
|
|
|
|
})
|
2023-05-10 00:42:42 +02:00
|
|
|
.with_state(state);
|
2023-05-08 00:05:45 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let addr = SocketAddr::from((
|
|
|
|
|
IpAddr::from_str(&args.bind)
|
|
|
|
|
.map_err(|error| format!("error parsing bind address {}: {}", &args.bind, error))
|
|
|
|
|
.unwrap(),
|
|
|
|
|
args.port,
|
|
|
|
|
));
|
2023-05-08 00:05:45 +02:00
|
|
|
tracing::debug!("listening on {}", addr);
|
2023-08-29 21:34:00 +02:00
|
|
|
axum::Server::try_bind(&addr)
|
|
|
|
|
.map_err(|error| format!("error binding to {}: {}", addr, error))
|
|
|
|
|
.unwrap()
|
2023-05-08 00:05:45 +02:00
|
|
|
.serve(app.into_make_service())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn root() -> impl IntoResponse {
|
2023-08-29 21:34:00 +02:00
|
|
|
view::Root::build(&view::home::Home::build(), None)
|
2023-05-08 00:05:45 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
#[derive(Deserialize, Default)]
|
2023-05-11 16:51:57 +02:00
|
|
|
struct InventoryQuery {
|
|
|
|
|
edit_item: Option<Uuid>,
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-08 00:05:45 +02:00
|
|
|
async fn inventory_active(
|
2023-05-11 16:51:57 +02:00
|
|
|
State(mut state): State<AppState>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Path(id): Path<Uuid>,
|
2023-05-11 16:51:57 +02:00
|
|
|
Query(inventory_query): Query<InventoryQuery>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-05-11 16:51:57 +02:00
|
|
|
state.client_state.edit_item = inventory_query.edit_item;
|
2023-08-29 21:34:00 +02:00
|
|
|
state.client_state.active_category_id = Some(id);
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let inventory = models::inventory::Inventory::load(&state.database_pool).await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let active_category: Option<&models::inventory::Category> = state
|
2023-08-29 21:34:00 +02:00
|
|
|
.client_state
|
|
|
|
|
.active_category_id
|
|
|
|
|
.map(|id| {
|
|
|
|
|
inventory
|
|
|
|
|
.categories
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|category| category.id == id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("a category with id {id} does not exist"),
|
|
|
|
|
}))
|
2023-08-29 21:34:00 +02:00
|
|
|
})
|
|
|
|
|
.transpose()?;
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::Root::build(
|
|
|
|
|
&view::inventory::Inventory::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
active_category,
|
|
|
|
|
&inventory.categories,
|
|
|
|
|
state.client_state.edit_item,
|
2023-08-29 21:34:00 +02:00
|
|
|
),
|
2023-08-29 21:34:00 +02:00
|
|
|
Some(&TopLevelPage::Inventory),
|
2023-08-29 21:34:00 +02:00
|
|
|
))
|
2023-05-08 00:05:45 +02:00
|
|
|
}
|
|
|
|
|
|
2023-05-10 00:42:42 +02:00
|
|
|
async fn inventory_inactive(
|
2023-05-11 16:51:57 +02:00
|
|
|
State(mut state): State<AppState>,
|
|
|
|
|
Query(inventory_query): Query<InventoryQuery>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-05-11 16:51:57 +02:00
|
|
|
state.client_state.edit_item = inventory_query.edit_item;
|
2023-08-29 21:34:00 +02:00
|
|
|
state.client_state.active_category_id = None;
|
2023-05-08 00:05:45 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let inventory = models::inventory::Inventory::load(&state.database_pool).await?;
|
2023-05-08 00:05:45 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::Root::build(
|
|
|
|
|
&view::inventory::Inventory::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
None,
|
|
|
|
|
&inventory.categories,
|
|
|
|
|
state.client_state.edit_item,
|
2023-05-08 00:05:45 +02:00
|
|
|
),
|
2023-08-29 21:34:00 +02:00
|
|
|
Some(&TopLevelPage::Inventory),
|
2023-05-08 00:05:45 +02:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-10 00:42:42 +02:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct NewItem {
|
|
|
|
|
#[serde(rename = "new-item-name")]
|
|
|
|
|
name: String,
|
|
|
|
|
#[serde(rename = "new-item-weight")]
|
|
|
|
|
weight: u32,
|
|
|
|
|
// damn i just love how serde is integrated everywhere, just add a feature to the uuid in
|
|
|
|
|
// cargo.toml and go
|
|
|
|
|
#[serde(rename = "new-item-category-id")]
|
|
|
|
|
category_id: Uuid,
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct NewItemName {
|
|
|
|
|
#[serde(rename = "new-item-name")]
|
|
|
|
|
name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn inventory_item_validate_name(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Form(new_item): Form<NewItemName>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let exists =
|
|
|
|
|
models::inventory::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::inventory::InventoryNewItemFormName::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
Some(&new_item.name),
|
|
|
|
|
exists,
|
2023-08-29 21:33:59 +02:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-10 00:42:42 +02:00
|
|
|
async fn inventory_item_create(
|
|
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:34:00 +02:00
|
|
|
headers: HeaderMap,
|
2023-05-10 00:42:42 +02:00
|
|
|
Form(new_item): Form<NewItem>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
if new_item.name.is_empty() {
|
2023-08-29 21:34:00 +02:00
|
|
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
|
|
|
|
name: "name".to_string(),
|
|
|
|
|
}));
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let _new_id = models::inventory::InventoryItem::save(
|
2023-08-29 21:34:00 +02:00
|
|
|
&state.database_pool,
|
|
|
|
|
&new_item.name,
|
|
|
|
|
new_item.category_id,
|
2023-08-29 21:33:59 +02:00
|
|
|
new_item.weight,
|
2023-05-10 00:42:42 +02:00
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?;
|
2023-05-10 00:42:42 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
if is_htmx(&headers) {
|
2023-08-29 21:34:00 +02:00
|
|
|
let inventory = models::inventory::Inventory::load(&state.database_pool).await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
|
|
|
|
// it's impossible to NOT find the item here, as we literally just added
|
2023-08-29 21:34:00 +02:00
|
|
|
// it.
|
2023-08-29 21:34:00 +02:00
|
|
|
let active_category: Option<&models::inventory::Category> = Some(
|
2023-08-29 21:34:00 +02:00
|
|
|
inventory
|
|
|
|
|
.categories
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|category| category.id == new_item.category_id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.unwrap(),
|
2023-08-29 21:34:00 +02:00
|
|
|
);
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::inventory::Inventory::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
active_category,
|
|
|
|
|
&inventory.categories,
|
|
|
|
|
state.client_state.edit_item,
|
2023-08-29 21:34:00 +02:00
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.into_response())
|
2023-08-29 21:34:00 +02:00
|
|
|
} else {
|
|
|
|
|
Ok(Redirect::to(&format!(
|
|
|
|
|
"/inventory/category/{id}/",
|
|
|
|
|
id = new_item.category_id
|
|
|
|
|
))
|
|
|
|
|
.into_response())
|
|
|
|
|
}
|
2023-05-10 00:42:42 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
fn get_referer<'a>(headers: &'a HeaderMap) -> Result<&'a str, Error> {
|
|
|
|
|
headers
|
|
|
|
|
.get("referer")
|
|
|
|
|
.ok_or(Error::Request(RequestError::RefererNotFound))?
|
|
|
|
|
.to_str()
|
|
|
|
|
.map_err(|error| {
|
|
|
|
|
Error::Request(RequestError::RefererInvalid {
|
|
|
|
|
message: error.to_string(),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-10 00:42:42 +02:00
|
|
|
async fn inventory_item_delete(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<Uuid>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let deleted = models::inventory::InventoryItem::delete(&state.database_pool, id).await?;
|
2023-05-10 00:42:42 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
if !deleted {
|
2023-08-29 21:34:00 +02:00
|
|
|
Err(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("item with id {id} not found"),
|
|
|
|
|
}))
|
2023-05-12 00:31:08 +02:00
|
|
|
} else {
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(Redirect::to(get_referer(&headers)?))
|
2023-05-12 00:31:08 +02:00
|
|
|
}
|
2023-05-10 00:42:42 +02:00
|
|
|
}
|
|
|
|
|
|
2023-05-12 00:31:08 +02:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct EditItem {
|
|
|
|
|
#[serde(rename = "edit-item-name")]
|
|
|
|
|
name: String,
|
|
|
|
|
#[serde(rename = "edit-item-weight")]
|
|
|
|
|
weight: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn inventory_item_edit(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Form(edit_item): Form<EditItem>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
if edit_item.name.is_empty() {
|
2023-08-29 21:34:00 +02:00
|
|
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
|
|
|
|
name: "name".to_string(),
|
|
|
|
|
}));
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let id = models::inventory::InventoryItem::update(
|
|
|
|
|
&state.database_pool,
|
|
|
|
|
id,
|
|
|
|
|
&edit_item.name,
|
|
|
|
|
edit_item.weight,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-05-12 00:31:08 +02:00
|
|
|
|
2023-05-18 00:11:52 +02:00
|
|
|
Ok(Redirect::to(&format!("/inventory/category/{id}/", id = id)))
|
2023-05-12 00:31:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn inventory_item_cancel(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let id = models::inventory::InventoryItem::find(&state.database_pool, id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?
|
|
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("item with id {id} not found"),
|
|
|
|
|
}))?;
|
2023-05-12 00:31:08 +02:00
|
|
|
|
|
|
|
|
Ok(Redirect::to(&format!(
|
2023-05-18 00:11:52 +02:00
|
|
|
"/inventory/category/{id}/",
|
2023-08-29 21:34:00 +02:00
|
|
|
id = id.category.id
|
2023-05-12 00:31:08 +02:00
|
|
|
)))
|
|
|
|
|
}
|
2023-05-18 00:11:52 +02:00
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct NewTrip {
|
|
|
|
|
#[serde(rename = "new-trip-name")]
|
|
|
|
|
name: String,
|
|
|
|
|
#[serde(rename = "new-trip-start-date")]
|
2023-08-29 21:33:59 +02:00
|
|
|
date_start: time::Date,
|
2023-05-18 00:11:52 +02:00
|
|
|
#[serde(rename = "new-trip-end-date")]
|
2023-08-29 21:33:59 +02:00
|
|
|
date_end: time::Date,
|
2023-05-18 00:11:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_create(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Form(new_trip): Form<NewTrip>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
if new_trip.name.is_empty() {
|
2023-08-29 21:34:00 +02:00
|
|
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
|
|
|
|
name: "name".to_string(),
|
|
|
|
|
}));
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let new_id = models::trips::Trip::save(
|
2023-08-29 21:34:00 +02:00
|
|
|
&state.database_pool,
|
|
|
|
|
&new_trip.name,
|
|
|
|
|
new_trip.date_start,
|
|
|
|
|
new_trip.date_end,
|
2023-05-18 00:11:52 +02:00
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?;
|
2023-05-18 00:11:52 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(Redirect::to(&format!("/trips/{new_id}/")))
|
2023-05-18 00:11:52 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn trips(State(state): State<AppState>) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let trips = models::trips::Trip::all(&state.database_pool).await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::Root::build(
|
|
|
|
|
&view::trip::TripManager::build(trips),
|
|
|
|
|
Some(&TopLevelPage::Trips),
|
2023-05-18 00:11:52 +02:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
struct TripQuery {
|
2023-08-29 21:34:00 +02:00
|
|
|
edit: Option<models::trips::TripAttribute>,
|
2023-08-29 21:33:59 +02:00
|
|
|
category: Option<Uuid>,
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-05-18 00:11:52 +02:00
|
|
|
async fn trip(
|
2023-08-29 21:33:59 +02:00
|
|
|
State(mut state): State<AppState>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Path(id): Path<Uuid>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Query(trip_query): Query<TripQuery>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:33:59 +02:00
|
|
|
state.client_state.trip_edit_attribute = trip_query.edit;
|
2023-08-29 21:33:59 +02:00
|
|
|
state.client_state.active_category_id = trip_query.category;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let mut trip: models::trips::Trip = models::trips::Trip::find(&state.database_pool, id)
|
|
|
|
|
.await?
|
|
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("trip with id {id} not found"),
|
|
|
|
|
}))?;
|
2023-05-18 00:11:52 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
trip.load_trips_types(&state.database_pool).await?;
|
2023-05-18 00:11:52 +02:00
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
trip.sync_trip_items_with_inventory(&state.database_pool)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
trip.load_categories(&state.database_pool).await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let active_category: Option<&models::trips::TripCategory> = state
|
2023-08-29 21:34:00 +02:00
|
|
|
.client_state
|
|
|
|
|
.active_category_id
|
|
|
|
|
.map(|id| {
|
|
|
|
|
trip.categories()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|category| category.category.id == id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("an active category with id {id} does not exist"),
|
|
|
|
|
}))
|
2023-08-29 21:34:00 +02:00
|
|
|
})
|
|
|
|
|
.transpose()?;
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::Root::build(
|
|
|
|
|
&view::trip::Trip::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
&trip,
|
|
|
|
|
state.client_state.trip_edit_attribute,
|
|
|
|
|
active_category,
|
2023-08-29 21:33:59 +02:00
|
|
|
),
|
2023-08-29 21:34:00 +02:00
|
|
|
Some(&TopLevelPage::Trips),
|
2023-05-18 00:11:52 +02:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_type_remove(
|
|
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let found =
|
|
|
|
|
models::trips::Trip::trip_type_remove(&state.database_pool, trip_id, type_id).await?;
|
2023-05-18 00:11:52 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
if !found {
|
2023-08-29 21:34:00 +02:00
|
|
|
Err(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("type {type_id} is not active for trip {trip_id}"),
|
|
|
|
|
}))
|
2023-05-18 00:11:52 +02:00
|
|
|
} else {
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(Redirect::to(&format!("/trips/{trip_id}/")))
|
2023-05-18 00:11:52 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_type_add(
|
|
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::Trip::trip_type_add(&state.database_pool, trip_id, type_id).await?;
|
2023-05-18 00:11:52 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(Redirect::to(&format!("/trips/{trip_id}/")))
|
2023-05-18 00:11:52 +02:00
|
|
|
}
|
2023-08-29 21:33:59 +02:00
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct CommentUpdate {
|
|
|
|
|
#[serde(rename = "new-comment")]
|
|
|
|
|
new_comment: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_comment_set(
|
|
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Path(trip_id): Path<Uuid>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Form(comment_update): Form<CommentUpdate>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let found = models::trips::Trip::set_comment(
|
|
|
|
|
&state.database_pool,
|
|
|
|
|
trip_id,
|
|
|
|
|
&comment_update.new_comment,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
if !found {
|
2023-08-29 21:34:00 +02:00
|
|
|
Err(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("trip with id {trip_id} not found"),
|
|
|
|
|
}))
|
2023-08-29 21:33:59 +02:00
|
|
|
} else {
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(Redirect::to(&format!("/trips/{id}/", id = trip_id)))
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct TripUpdate {
|
|
|
|
|
#[serde(rename = "new-value")]
|
|
|
|
|
new_value: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_edit_attribute(
|
|
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:34:00 +02:00
|
|
|
Path((trip_id, attribute)): Path<(Uuid, models::trips::TripAttribute)>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Form(trip_update): Form<TripUpdate>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
if attribute == models::trips::TripAttribute::Name {
|
2023-08-29 21:34:00 +02:00
|
|
|
if trip_update.new_value.is_empty() {
|
2023-08-29 21:34:00 +02:00
|
|
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
|
|
|
|
name: "name".to_string(),
|
|
|
|
|
}));
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::Trip::set_attribute(
|
2023-08-29 21:34:00 +02:00
|
|
|
&state.database_pool,
|
|
|
|
|
trip_id,
|
|
|
|
|
attribute,
|
|
|
|
|
&trip_update.new_value,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(Redirect::to(&format!("/trips/{trip_id}/")))
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_item_set_state(
|
|
|
|
|
state: &AppState,
|
|
|
|
|
trip_id: Uuid,
|
|
|
|
|
item_id: Uuid,
|
2023-08-29 21:34:00 +02:00
|
|
|
key: models::trips::TripItemStateKey,
|
2023-08-29 21:33:59 +02:00
|
|
|
value: bool,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<(), Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItem::set_state(&state.database_pool, trip_id, item_id, key, value).await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(())
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn trip_row(
|
|
|
|
|
state: &AppState,
|
|
|
|
|
trip_id: Uuid,
|
|
|
|
|
item_id: Uuid,
|
|
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let item = models::trips::TripItem::find(&state.database_pool, trip_id, item_id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or_else(|| {
|
2023-08-29 21:34:00 +02:00
|
|
|
Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("item with id {item_id} not found for trip {trip_id}"),
|
|
|
|
|
})
|
2023-08-29 21:34:00 +02:00
|
|
|
})?;
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let item_row = view::trip::TripItemListRow::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_id,
|
|
|
|
|
&item,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::inventory::InventoryItem::get_category_max_weight(
|
2023-08-29 21:34:00 +02:00
|
|
|
&state.database_pool,
|
|
|
|
|
item.item.category_id,
|
|
|
|
|
)
|
|
|
|
|
.await?,
|
2023-08-29 21:34:00 +02:00
|
|
|
);
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let category =
|
|
|
|
|
models::trips::TripCategory::find(&state.database_pool, trip_id, item.item.category_id)
|
|
|
|
|
.await?
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("category with id {} not found", item.item.category_id),
|
|
|
|
|
})
|
|
|
|
|
})?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
// TODO biggest_category_weight?
|
2023-08-29 21:34:00 +02:00
|
|
|
let category_row = view::trip::TripCategoryListRow::build(trip_id, &category, true, 0, true);
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(html::concat(item_row, category_row))
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
async fn trip_item_set_pick(
|
2023-08-29 21:33:59 +02:00
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
headers: HeaderMap,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
|
|
|
|
Ok::<_, Error>(
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pick,
|
2023-08-29 21:34:00 +02:00
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?,
|
2023-08-29 21:34:00 +02:00
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn trip_item_set_pick_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pick,
|
2023-08-29 21:34:00 +02:00
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
headers.insert::<HeaderName>(
|
|
|
|
|
HtmxResponseHeaders::Trigger.into(),
|
|
|
|
|
HtmxEvents::TripItemEdited.into(),
|
|
|
|
|
);
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok((headers, trip_row(&state, trip_id, item_id).await?))
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
async fn trip_item_set_unpick(
|
2023-08-29 21:33:59 +02:00
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
headers: HeaderMap,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
|
|
|
|
Ok::<_, Error>(
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pick,
|
2023-08-29 21:34:00 +02:00
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.await?,
|
2023-08-29 21:34:00 +02:00
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn trip_item_set_unpick_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pick,
|
2023-08-29 21:34:00 +02:00
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
headers.insert::<HeaderName>(
|
|
|
|
|
HtmxResponseHeaders::Trigger.into(),
|
|
|
|
|
HtmxEvents::TripItemEdited.into(),
|
|
|
|
|
);
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok((headers, trip_row(&state, trip_id, item_id).await?))
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
async fn trip_item_set_pack(
|
2023-08-29 21:33:59 +02:00
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
headers: HeaderMap,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
|
|
|
|
Ok::<_, Error>(
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pack,
|
2023-08-29 21:34:00 +02:00
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?,
|
2023-08-29 21:34:00 +02:00
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn trip_item_set_pack_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pack,
|
2023-08-29 21:34:00 +02:00
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
headers.insert::<HeaderName>(
|
|
|
|
|
HtmxResponseHeaders::Trigger.into(),
|
|
|
|
|
HtmxEvents::TripItemEdited.into(),
|
|
|
|
|
);
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok((headers, trip_row(&state, trip_id, item_id).await?))
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
async fn trip_item_set_unpack(
|
2023-08-29 21:33:59 +02:00
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:33:59 +02:00
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
headers: HeaderMap,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
|
|
|
|
Ok::<_, Error>(
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pack,
|
2023-08-29 21:34:00 +02:00
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.await?,
|
2023-08-29 21:34:00 +02:00
|
|
|
)
|
2023-08-29 21:34:00 +02:00
|
|
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn trip_item_set_unpack_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pack,
|
2023-08-29 21:34:00 +02:00
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
headers.insert::<HeaderName>(
|
|
|
|
|
HtmxResponseHeaders::Trigger.into(),
|
|
|
|
|
HtmxEvents::TripItemEdited.into(),
|
|
|
|
|
);
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok((headers, trip_row(&state, trip_id, item_id).await?))
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn trip_item_set_ready(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<Redirect, Error> {
|
|
|
|
|
Ok::<_, Error>(
|
|
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
|
|
|
|
models::trips::TripItemStateKey::Ready,
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?,
|
|
|
|
|
)
|
|
|
|
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_item_set_ready_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
) -> Result<impl IntoResponse, Error> {
|
|
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
|
|
|
|
models::trips::TripItemStateKey::Ready,
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
headers.insert::<HeaderName>(
|
|
|
|
|
HtmxResponseHeaders::Trigger.into(),
|
|
|
|
|
HtmxEvents::TripItemEdited.into(),
|
|
|
|
|
);
|
|
|
|
|
Ok((headers, trip_row(&state, trip_id, item_id).await?))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_item_set_unready(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<Redirect, Error> {
|
|
|
|
|
Ok::<_, Error>(
|
|
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
|
|
|
|
models::trips::TripItemStateKey::Ready,
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.await?,
|
|
|
|
|
)
|
|
|
|
|
.map(|_| -> Result<Redirect, Error> { Ok(Redirect::to(get_referer(&headers)?)) })?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_item_set_unready_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
) -> Result<impl IntoResponse, Error> {
|
|
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
|
|
|
|
models::trips::TripItemStateKey::Ready,
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
headers.insert::<HeaderName>(
|
|
|
|
|
HtmxResponseHeaders::Trigger.into(),
|
|
|
|
|
HtmxEvents::TripItemEdited.into(),
|
|
|
|
|
);
|
|
|
|
|
Ok((headers, trip_row(&state, trip_id, item_id).await?))
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
async fn trip_total_weight_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(trip_id): Path<Uuid>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let total_weight =
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::Trip::find_total_picked_weight(&state.database_pool, trip_id).await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::trip::TripInfoTotalWeightRow::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_id,
|
|
|
|
|
total_weight,
|
2023-08-29 21:34:00 +02:00
|
|
|
))
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct NewCategory {
|
|
|
|
|
#[serde(rename = "new-category-name")]
|
|
|
|
|
name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn inventory_category_create(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Form(new_category): Form<NewCategory>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
if new_category.name.is_empty() {
|
2023-08-29 21:34:00 +02:00
|
|
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
|
|
|
|
name: "name".to_string(),
|
|
|
|
|
}));
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let _new_id =
|
|
|
|
|
models::inventory::Category::save(&state.database_pool, &new_category.name).await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
|
|
|
|
Ok(Redirect::to("/inventory/"))
|
|
|
|
|
}
|
2023-08-29 21:33:59 +02:00
|
|
|
|
|
|
|
|
async fn trip_state_set(
|
|
|
|
|
State(state): State<AppState>,
|
2023-08-29 21:34:00 +02:00
|
|
|
headers: HeaderMap,
|
2023-08-29 21:34:00 +02:00
|
|
|
Path((trip_id, new_state)): Path<(Uuid, models::trips::TripState)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let exists = models::trips::Trip::set_state(&state.database_pool, trip_id, &new_state).await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
if !exists {
|
2023-08-29 21:34:00 +02:00
|
|
|
return Err(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("trip with id {trip_id} not found"),
|
|
|
|
|
}));
|
2023-08-29 21:34:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if is_htmx(&headers) {
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::trip::TripInfoStateRow::build(&new_state).into_response())
|
2023-08-29 21:33:59 +02:00
|
|
|
} else {
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(Redirect::to(&format!("/trips/{id}/", id = trip_id)).into_response())
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
fn is_htmx(headers: &HeaderMap) -> bool {
|
|
|
|
|
headers
|
|
|
|
|
.get::<HeaderName>(HtmxRequestHeaders::HtmxRequest.into())
|
|
|
|
|
.map(|value| value == "true")
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:33:59 +02:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
struct TripTypeQuery {
|
|
|
|
|
edit: Option<Uuid>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trips_types(
|
|
|
|
|
State(mut state): State<AppState>,
|
|
|
|
|
Query(trip_type_query): Query<TripTypeQuery>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:33:59 +02:00
|
|
|
state.client_state.trip_type_edit = trip_type_query.edit;
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let trip_types: Vec<models::trips::TripsType> =
|
|
|
|
|
models::trips::TripsType::all(&state.database_pool).await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::Root::build(
|
|
|
|
|
&view::trip::types::TypeList::build(&state.client_state, trip_types),
|
|
|
|
|
Some(&TopLevelPage::Trips),
|
2023-08-29 21:33:59 +02:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct NewTripType {
|
|
|
|
|
#[serde(rename = "new-trip-type-name")]
|
|
|
|
|
name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_type_create(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Form(new_trip_type): Form<NewTripType>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
if new_trip_type.name.is_empty() {
|
2023-08-29 21:34:00 +02:00
|
|
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
|
|
|
|
name: "name".to_string(),
|
|
|
|
|
}));
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let _new_id = models::trips::TripsType::save(&state.database_pool, &new_trip_type.name).await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
|
|
|
|
Ok(Redirect::to("/trips/types/"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct TripTypeUpdate {
|
|
|
|
|
#[serde(rename = "new-value")]
|
|
|
|
|
new_value: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trips_types_edit_name(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(trip_type_id): Path<Uuid>,
|
|
|
|
|
Form(trip_update): Form<TripTypeUpdate>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<Redirect, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
if trip_update.new_value.is_empty() {
|
2023-08-29 21:34:00 +02:00
|
|
|
return Err(Error::Request(RequestError::EmptyFormElement {
|
|
|
|
|
name: "name".to_string(),
|
|
|
|
|
}));
|
2023-08-29 21:33:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let exists = models::trips::TripsType::set_name(
|
|
|
|
|
&state.database_pool,
|
|
|
|
|
trip_type_id,
|
|
|
|
|
&trip_update.new_value,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-29 21:33:59 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
if !exists {
|
2023-08-29 21:34:00 +02:00
|
|
|
return Err(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("trip type with id {trip_type_id} not found"),
|
|
|
|
|
}));
|
2023-08-29 21:33:59 +02:00
|
|
|
} else {
|
|
|
|
|
Ok(Redirect::to("/trips/types/"))
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-29 21:34:00 +02:00
|
|
|
|
|
|
|
|
async fn inventory_item(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let item = models::inventory::InventoryItem::find(&state.database_pool, id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("inventory item with id {id} not found"),
|
|
|
|
|
}))?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::Root::build(
|
|
|
|
|
&view::inventory::InventoryItem::build(&state.client_state, &item),
|
|
|
|
|
Some(&TopLevelPage::Inventory),
|
2023-08-29 21:34:00 +02:00
|
|
|
))
|
|
|
|
|
}
|
2023-08-29 21:34:00 +02:00
|
|
|
|
|
|
|
|
async fn trip_category_select(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, category_id)): Path<(Uuid, Uuid)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let mut trip = models::trips::Trip::find(&state.database_pool, trip_id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("trip with id {trip_id} not found"),
|
|
|
|
|
}))?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
trip.load_categories(&state.database_pool).await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
|
|
|
|
let active_category = trip
|
|
|
|
|
.categories()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|c| c.category.id == category_id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("category with id {category_id} not found"),
|
|
|
|
|
}))?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
headers.insert::<HeaderName>(
|
|
|
|
|
HtmxResponseHeaders::PushUrl.into(),
|
|
|
|
|
format!("?={category_id}").parse().unwrap(),
|
|
|
|
|
);
|
|
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok((
|
2023-08-29 21:34:00 +02:00
|
|
|
headers,
|
2023-08-29 21:34:00 +02:00
|
|
|
view::trip::TripItems::build(Some(active_category), &trip),
|
2023-08-29 21:34:00 +02:00
|
|
|
))
|
|
|
|
|
}
|
2023-08-29 21:34:00 +02:00
|
|
|
|
|
|
|
|
async fn inventory_category_select(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(category_id): Path<Uuid>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let inventory = models::inventory::Inventory::load(&state.database_pool).await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let active_category: Option<&models::inventory::Category> = Some(
|
2023-08-29 21:34:00 +02:00
|
|
|
inventory
|
|
|
|
|
.categories
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|category| category.id == category_id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("a category with id {category_id} not found"),
|
|
|
|
|
}))?,
|
2023-08-29 21:34:00 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
headers.insert::<HeaderName>(
|
|
|
|
|
HtmxResponseHeaders::PushUrl.into(),
|
2023-08-29 21:34:00 +02:00
|
|
|
format!("/inventory/category/{category_id}/")
|
2023-08-29 21:34:00 +02:00
|
|
|
.parse()
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok((
|
|
|
|
|
headers,
|
2023-08-29 21:34:00 +02:00
|
|
|
view::inventory::Inventory::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
active_category,
|
|
|
|
|
&inventory.categories,
|
|
|
|
|
state.client_state.edit_item,
|
|
|
|
|
),
|
|
|
|
|
))
|
|
|
|
|
}
|
2023-08-29 21:34:00 +02:00
|
|
|
|
|
|
|
|
async fn trip_packagelist(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(trip_id): Path<Uuid>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
let mut trip = models::trips::Trip::find(&state.database_pool, trip_id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("trip with id {trip_id} not found"),
|
|
|
|
|
}))?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
trip.load_categories(&state.database_pool).await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::Root::build(
|
|
|
|
|
&view::trip::packagelist::TripPackageList::build(&trip),
|
|
|
|
|
Some(&TopLevelPage::Trips),
|
2023-08-29 21:34:00 +02:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_item_packagelist_set_pack_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pack,
|
2023-08-29 21:34:00 +02:00
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
let item = models::trips::TripItem::find(&state.database_pool, trip_id, item_id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("an item with id {item_id} does not exist"),
|
|
|
|
|
}))?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::trip::packagelist::TripPackageListRowReady::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_id, &item,
|
2023-08-29 21:34:00 +02:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_item_packagelist_set_unpack_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
2023-08-29 21:34:00 +02:00
|
|
|
) -> Result<impl IntoResponse, Error> {
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
2023-08-29 21:34:00 +02:00
|
|
|
models::trips::TripItemStateKey::Pack,
|
2023-08-29 21:34:00 +02:00
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
|
|
|
|
// note that this cannot fail due to a missing item, as trip_item_set_state would already
|
|
|
|
|
// return 404. but error handling cannot hurt ;)
|
2023-08-29 21:34:00 +02:00
|
|
|
let item = models::trips::TripItem::find(&state.database_pool, trip_id, item_id)
|
2023-08-29 21:34:00 +02:00
|
|
|
.await?
|
2023-08-29 21:34:00 +02:00
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("an item with id {item_id} does not exist"),
|
|
|
|
|
}))?;
|
2023-08-29 21:34:00 +02:00
|
|
|
|
2023-08-29 21:34:00 +02:00
|
|
|
Ok(view::trip::packagelist::TripPackageListRowReady::build(
|
|
|
|
|
trip_id, &item,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_item_packagelist_set_ready_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
) -> Result<impl IntoResponse, Error> {
|
|
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
|
|
|
|
models::trips::TripItemStateKey::Ready,
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let item = models::trips::TripItem::find(&state.database_pool, trip_id, item_id)
|
|
|
|
|
.await?
|
|
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("an item with id {item_id} does not exist"),
|
|
|
|
|
}))?;
|
|
|
|
|
|
|
|
|
|
Ok(view::trip::packagelist::TripPackageListRowUnready::build(
|
|
|
|
|
trip_id, &item,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn trip_item_packagelist_set_unready_htmx(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((trip_id, item_id)): Path<(Uuid, Uuid)>,
|
|
|
|
|
) -> Result<impl IntoResponse, Error> {
|
|
|
|
|
trip_item_set_state(
|
|
|
|
|
&state,
|
|
|
|
|
trip_id,
|
|
|
|
|
item_id,
|
|
|
|
|
models::trips::TripItemStateKey::Ready,
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
// note that this cannot fail due to a missing item, as trip_item_set_state would already
|
|
|
|
|
// return 404. but error handling cannot hurt ;)
|
|
|
|
|
let item = models::trips::TripItem::find(&state.database_pool, trip_id, item_id)
|
|
|
|
|
.await?
|
|
|
|
|
.ok_or(Error::Request(RequestError::NotFound {
|
|
|
|
|
message: format!("an item with id {item_id} does not exist"),
|
|
|
|
|
}))?;
|
|
|
|
|
|
|
|
|
|
Ok(view::trip::packagelist::TripPackageListRowUnready::build(
|
2023-08-29 21:34:00 +02:00
|
|
|
trip_id, &item,
|
2023-08-29 21:34:00 +02:00
|
|
|
))
|
|
|
|
|
}
|