update
This commit is contained in:
@@ -98,7 +98,7 @@ impl InventoryCategoryList {
|
||||
id="select-category"
|
||||
href=(
|
||||
format!(
|
||||
"/inventory/category/{id}",
|
||||
"/inventory/category/{id}/",
|
||||
id=category.id
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,13 +2,11 @@ use maud::{html, Markup, DOCTYPE};
|
||||
|
||||
pub mod home;
|
||||
pub mod inventory;
|
||||
pub mod triplist;
|
||||
|
||||
mod theme;
|
||||
pub mod trip;
|
||||
|
||||
pub use home::*;
|
||||
pub use inventory::*;
|
||||
pub use triplist::*;
|
||||
pub use trip::*;
|
||||
|
||||
pub struct Root {
|
||||
doc: Markup,
|
||||
|
||||
349
rust/src/components/trip.rs
Normal file
349
rust/src/components/trip.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
use crate::models;
|
||||
use crate::models::*;
|
||||
|
||||
use maud::{html, Markup};
|
||||
|
||||
pub struct TripManager {
|
||||
doc: Markup,
|
||||
}
|
||||
|
||||
impl TripManager {
|
||||
pub fn build(trips: Vec<models::Trip>) -> Self {
|
||||
let doc = html!(
|
||||
div ."p-8" {
|
||||
(TripTable::build(trips).into_markup())
|
||||
(NewTrip::build().into_markup())
|
||||
}
|
||||
);
|
||||
|
||||
Self { doc }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TripTable {
|
||||
doc: Markup,
|
||||
}
|
||||
|
||||
impl From<TripManager> for Markup {
|
||||
fn from(val: TripManager) -> Self {
|
||||
val.doc
|
||||
}
|
||||
}
|
||||
|
||||
impl TripTable {
|
||||
pub fn build(trips: Vec<models::Trip>) -> Self {
|
||||
let doc = html!(
|
||||
h1 ."text-2xl" ."mb-5" {"Trips"}
|
||||
table
|
||||
."table"
|
||||
."table-auto"
|
||||
."border-collapse"
|
||||
."border-spacing-0"
|
||||
."border"
|
||||
."w-full"
|
||||
{
|
||||
thead ."bg-gray-200" {
|
||||
tr ."h-10" {
|
||||
th ."border" ."p-2" { "Name" }
|
||||
th ."border" ."p-2" { "From" }
|
||||
th ."border" ."p-2" { "To" }
|
||||
th ."border" ."p-2" { "Nights" }
|
||||
th ."border" ."p-2" { "State" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@for trip in trips {
|
||||
tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" {
|
||||
td ."border" ."p-0" ."m-0" {
|
||||
a ."inline-block" ."p-2" ."m-0" ."w-full"
|
||||
href=(format!("/trip/{id}/", id=trip.id))
|
||||
{ (trip.name) }
|
||||
}
|
||||
td ."border" ."p-0" ."m-0" {
|
||||
a ."inline-block" ."p-2" ."m-0" ."w-full"
|
||||
href=(format!("/trip/{id}/", id=trip.id))
|
||||
{ (trip.start_date) }
|
||||
}
|
||||
td ."border" ."p-0" ."m-0" {
|
||||
a ."inline-block" ."p-2" ."m-0" ."w-full"
|
||||
href=(format!("/trip/{id}/", id=trip.id))
|
||||
{ (trip.end_date) }
|
||||
}
|
||||
td ."border" ."p-0" ."m-0" {
|
||||
a ."inline-block" ."p-2" ."m-0" ."w-full"
|
||||
href=(format!("/trip/{id}/", id=trip.id))
|
||||
{ ((trip.end_date - trip.start_date).whole_days()) }
|
||||
}
|
||||
td ."border" ."p-0" ."m-0" {
|
||||
a ."inline-block" ."p-2" ."m-0" ."w-full"
|
||||
href=(format!("/trip/{id}/", id=trip.id))
|
||||
{ (trip.state.to_string()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Self { doc }
|
||||
}
|
||||
|
||||
pub fn into_markup(self) -> Markup {
|
||||
self.doc
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NewTrip {
|
||||
doc: Markup,
|
||||
}
|
||||
|
||||
impl NewTrip {
|
||||
pub fn build() -> Self {
|
||||
let doc = html!(
|
||||
form
|
||||
name="new_trip"
|
||||
action="/trip/"
|
||||
target="_self"
|
||||
method="post"
|
||||
."mt-8" ."p-5" ."border-2" ."border-gray-200"
|
||||
{
|
||||
div ."mb-5" ."flex" ."flex-row" ."trips-center" {
|
||||
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
|
||||
p ."inline" ."text-xl" { "Add new trip" }
|
||||
}
|
||||
div ."w-11/12" ."m-auto" {
|
||||
div ."mx-auto" ."pb-8" {
|
||||
div ."flex" ."flex-row" ."justify-center" {
|
||||
label for="trip-name" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Name" }
|
||||
span ."w-1/2" {
|
||||
input
|
||||
type="text"
|
||||
id="trip-name"
|
||||
name="new-trip-name"
|
||||
."block"
|
||||
."w-full"
|
||||
."p-2"
|
||||
."bg-gray-50"
|
||||
."border-2"
|
||||
."rounded"
|
||||
."focus:outline-none"
|
||||
."focus:bg-white"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
div ."mx-auto" ."pb-8" {
|
||||
div ."flex" ."flex-row" ."justify-center" {
|
||||
label for="trip-name" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Start date" }
|
||||
span ."w-1/2" {
|
||||
input
|
||||
type="date"
|
||||
id="start-date"
|
||||
name="new-trip-start-date"
|
||||
."block"
|
||||
."w-full"
|
||||
."p-2"
|
||||
."bg-gray-50"
|
||||
."appearance-none"
|
||||
."border-2"
|
||||
."border-gray-300"
|
||||
."rounded"
|
||||
."focus:outline-none"
|
||||
."focus:bg-white"
|
||||
."focus:border-purple-500"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
div ."mx-auto" ."pb-8" {
|
||||
div ."flex" ."flex-row" ."justify-center" {
|
||||
label for="trip-name" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Start date" }
|
||||
span ."w-1/2" {
|
||||
input
|
||||
type="date"
|
||||
id="end-date"
|
||||
name="new-trip-end-date"
|
||||
."block"
|
||||
."w-full"
|
||||
."p-2"
|
||||
."bg-gray-50"
|
||||
."appearance-none"
|
||||
."border-2"
|
||||
."border-gray-300"
|
||||
."rounded"
|
||||
."focus:outline-none"
|
||||
."focus:bg-white"
|
||||
."focus:border-purple-500"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
input
|
||||
type="submit"
|
||||
value="Add"
|
||||
."py-2"
|
||||
."border-2"
|
||||
."rounded"
|
||||
."border-gray-300"
|
||||
."mx-auto"
|
||||
."w-full"
|
||||
{}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Self { doc }
|
||||
}
|
||||
|
||||
pub fn into_markup(self) -> Markup {
|
||||
self.doc
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Trip {
|
||||
doc: Markup,
|
||||
}
|
||||
|
||||
impl Trip {
|
||||
pub fn build(trip: &models::Trip) -> Self {
|
||||
let doc = html!(
|
||||
div ."p-8" {
|
||||
div ."flex" ."flex-row" ."items-center" ."gap-x-3" {
|
||||
h1 ."text-2xl" ."font-semibold"{ (trip.name) }
|
||||
}
|
||||
div ."my-6" {
|
||||
(TripInfo::build(&trip).into_markup())
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Self { doc }
|
||||
}
|
||||
|
||||
pub fn into_markup(self) -> Markup {
|
||||
self.doc
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TripInfo {
|
||||
doc: Markup,
|
||||
}
|
||||
|
||||
impl TripInfo {
|
||||
pub fn build(trip: &models::Trip) -> Self {
|
||||
let doc = html!(
|
||||
table
|
||||
."table"
|
||||
."table-auto"
|
||||
."border-collapse"
|
||||
."border-spacing-0"
|
||||
."border"
|
||||
."w-full"
|
||||
{
|
||||
tbody {
|
||||
tr {
|
||||
td ."border" ."p-2" { "State" }
|
||||
td ."border" ."p-2" { (trip.state.to_string()) }
|
||||
}
|
||||
tr {
|
||||
td ."border" ."p-2" { "Location" }
|
||||
td ."border" ."p-2" { (trip.location) }
|
||||
}
|
||||
tr {
|
||||
td ."border" ."p-2" { "Start date" }
|
||||
td ."border" ."p-2" { (trip.start_date) }
|
||||
}
|
||||
tr {
|
||||
td ."border" ."p-2" { "End date" }
|
||||
td ."border" ."p-2" { (trip.end_date) }
|
||||
}
|
||||
tr {
|
||||
td ."border" ."p-2" { "Temp (min)" }
|
||||
td ."border" ."p-2" { (trip.temp_min) }
|
||||
}
|
||||
tr {
|
||||
td ."border" ."p-2" { "Temp (max)" }
|
||||
td ."border" ."p-2" { (trip.temp_max) }
|
||||
}
|
||||
tr {
|
||||
td ."border" ."p-2" { "Types" }
|
||||
td ."border" ."p-2" {
|
||||
ul
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-wrap"
|
||||
."gap-2"
|
||||
."justify-between"
|
||||
{
|
||||
@let types = trip.types();
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-wrap"
|
||||
."gap-2"
|
||||
."justify-start"
|
||||
{
|
||||
@for triptype in types.iter().filter(|t| t.active) {
|
||||
a href=(format!("type/{}/remove", triptype.id)) {
|
||||
li
|
||||
."border"
|
||||
."rounded-2xl"
|
||||
."py-0.5"
|
||||
."px-2"
|
||||
."bg-green-100"
|
||||
."cursor-pointer"
|
||||
."flex"
|
||||
."flex-column"
|
||||
."items-center"
|
||||
."hover:bg-red-200"
|
||||
."gap-1"
|
||||
{
|
||||
span { (triptype.name) }
|
||||
span ."mdi" ."mdi-delete" ."text-sm" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-wrap"
|
||||
."gap-2"
|
||||
."justify-start"
|
||||
{
|
||||
@for triptype in types.iter().filter(|t| !t.active) {
|
||||
a href=(format!("type/{}/add", triptype.id)) {
|
||||
li
|
||||
."border"
|
||||
."rounded-2xl"
|
||||
."py-0.5"
|
||||
."px-2"
|
||||
."bg-gray-100"
|
||||
."cursor-pointer"
|
||||
."flex"
|
||||
."flex-column"
|
||||
."items-center"
|
||||
."hover:bg-green-200"
|
||||
."gap-1"
|
||||
."opacity-60"
|
||||
{
|
||||
span { (triptype.name) }
|
||||
span ."mdi" ."mdi-plus" ."text-sm" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Self { doc }
|
||||
}
|
||||
|
||||
pub fn into_markup(self) -> Markup {
|
||||
self.doc
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use crate::models::*;
|
||||
|
||||
use maud::{html, Markup};
|
||||
|
||||
pub struct TripList {
|
||||
doc: Markup,
|
||||
}
|
||||
|
||||
impl TripList {
|
||||
pub fn build(package_lists: Vec<Trip>) -> Self {
|
||||
let doc = html!(
|
||||
table {
|
||||
thead {
|
||||
td {
|
||||
td { "ID" }
|
||||
td { "Name" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@for list in package_lists {
|
||||
tr {
|
||||
td { (list.id.to_string()) }
|
||||
td { (list.name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Self { doc }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TripList> for Markup {
|
||||
fn from(val: TripList) -> Self {
|
||||
val.doc
|
||||
}
|
||||
}
|
||||
258
rust/src/main.rs
258
rust/src/main.rs
@@ -82,9 +82,13 @@ async fn main() -> Result<(), sqlx::Error> {
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/trips/", get(trips))
|
||||
.route("/trip/", post(trip_create))
|
||||
.route("/trip/:id/", get(trip))
|
||||
.route("/trip/:id/type/:id/add", get(trip_type_add))
|
||||
.route("/trip/:id/type/:id/remove", get(trip_type_remove))
|
||||
.route("/inventory/", get(inventory_inactive))
|
||||
.route("/inventory/item/", post(inventory_item_create))
|
||||
.route("/inventory/category/:id", get(inventory_active))
|
||||
.route("/inventory/category/:id/", get(inventory_active))
|
||||
.route("/inventory/item/:id/delete", get(inventory_item_delete))
|
||||
.route("/inventory/item/:id/edit", post(inventory_item_edit))
|
||||
.route("/inventory/item/:id/cancel", get(inventory_item_cancel))
|
||||
@@ -123,7 +127,7 @@ impl Default for InventoryQuery {
|
||||
}
|
||||
|
||||
async fn inventory_active(
|
||||
Path(id): Path<String>,
|
||||
Path(id): Path<Uuid>,
|
||||
State(mut state): State<AppState>,
|
||||
Query(inventory_query): Query<InventoryQuery>,
|
||||
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||
@@ -141,13 +145,8 @@ async fn inventory_inactive(
|
||||
|
||||
async fn inventory(
|
||||
mut state: AppState,
|
||||
active_id: Option<String>,
|
||||
active_id: Option<Uuid>,
|
||||
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||
let active_id = active_id
|
||||
.map(|id| Uuid::try_parse(&id))
|
||||
.transpose()
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, Html::from(e.to_string())))?;
|
||||
|
||||
state.client_state.active_category_id = active_id;
|
||||
|
||||
let mut categories = query("SELECT id,name,description FROM inventoryitemcategories")
|
||||
@@ -191,29 +190,6 @@ async fn inventory(
|
||||
))
|
||||
}
|
||||
|
||||
async fn trips(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||
let trips = query("SELECT * FROM trips")
|
||||
.fetch(&state.database_pool)
|
||||
.map_ok(std::convert::TryInto::try_into)
|
||||
.try_collect::<Vec<Result<Trip, models::Error>>>()
|
||||
.await
|
||||
// we have two error handling lines here. these are distinct errors
|
||||
// this one is the SQL error that may arise during the query
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<Trip>, models::Error>>()
|
||||
// and this one is the model mapping error that may arise e.g. during
|
||||
// reading of the rows
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Html::from(Root::build(TripList::build(trips).into(), &TopLevelPage::Trips).into_string()),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NewItem {
|
||||
#[serde(rename = "new-item-name")]
|
||||
@@ -285,7 +261,7 @@ async fn inventory_item_create(
|
||||
})?;
|
||||
|
||||
Ok(Redirect::to(&format!(
|
||||
"/inventory/category/{id}",
|
||||
"/inventory/category/{id}/",
|
||||
id = new_item.category_id
|
||||
)))
|
||||
}
|
||||
@@ -342,7 +318,7 @@ async fn inventory_item_delete(
|
||||
// "SELECT
|
||||
// i.id, i.name, i.description, i.weight, i.category_id
|
||||
// FROM inventoryitemcategories AS c
|
||||
// LEFT JOIN inventoryitems AS i
|
||||
// INNER JOIN inventoryitems AS i
|
||||
// ON i.category_id = c.id WHERE c.id = '{id}';",
|
||||
// id = id,
|
||||
// ))
|
||||
@@ -390,7 +366,7 @@ async fn inventory_item_edit(
|
||||
format!("item with id {id} not found", id = id),
|
||||
))?;
|
||||
|
||||
Ok(Redirect::to(&format!("/inventory/category/{id}", id = id)))
|
||||
Ok(Redirect::to(&format!("/inventory/category/{id}/", id = id)))
|
||||
}
|
||||
|
||||
async fn inventory_item_cancel(
|
||||
@@ -406,7 +382,219 @@ async fn inventory_item_cancel(
|
||||
))?;
|
||||
|
||||
Ok(Redirect::to(&format!(
|
||||
"/inventory/category/{id}",
|
||||
"/inventory/category/{id}/",
|
||||
id = id.category_id
|
||||
)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NewTrip {
|
||||
#[serde(rename = "new-trip-name")]
|
||||
name: String,
|
||||
#[serde(rename = "new-trip-start-date")]
|
||||
start_date: time::Date,
|
||||
#[serde(rename = "new-trip-end-date")]
|
||||
end_date: time::Date,
|
||||
}
|
||||
|
||||
async fn trip_create(
|
||||
State(state): State<AppState>,
|
||||
Form(new_trip): Form<NewTrip>,
|
||||
) -> Result<Redirect, (StatusCode, String)> {
|
||||
let id = Uuid::new_v4();
|
||||
query(
|
||||
"INSERT INTO trips
|
||||
(id, name, start_date, end_date)
|
||||
VALUES
|
||||
(?, ?, ?, ?)",
|
||||
)
|
||||
.bind(id.to_string())
|
||||
.bind(&new_trip.name)
|
||||
.bind(new_trip.start_date)
|
||||
.bind(new_trip.end_date)
|
||||
.execute(&state.database_pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::Database(ref error) => {
|
||||
let sqlite_error = error.downcast_ref::<SqliteError>();
|
||||
if let Some(code) = sqlite_error.code() {
|
||||
match &*code {
|
||||
"2067" => {
|
||||
// SQLITE_CONSTRAINT_UNIQUE
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!(
|
||||
"trip with name \"{name}\" already exists",
|
||||
name = new_trip.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
_ => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("got error with unknown code: {}", sqlite_error.to_string()),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("got error without code: {}", sqlite_error.to_string()),
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("got unknown error: {}", e.to_string()),
|
||||
),
|
||||
})?;
|
||||
|
||||
Ok(Redirect::to(&format!("/trips/{id}/", id = id.to_string())))
|
||||
}
|
||||
|
||||
async fn trips(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||
let trips: Vec<models::Trip> = query("SELECT * FROM trips")
|
||||
.fetch(&state.database_pool)
|
||||
.map_ok(std::convert::TryInto::try_into)
|
||||
.try_collect::<Vec<Result<models::Trip, models::Error>>>()
|
||||
.await
|
||||
// we have two error handling lines here. these are distinct errors
|
||||
// this one is the SQL error that may arise during the query
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<models::Trip>, models::Error>>()
|
||||
// and this one is the model mapping error that may arise e.g. during
|
||||
// reading of the rows
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Html::from(
|
||||
Root::build(TripManager::build(trips).into(), &TopLevelPage::Trips).into_string(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
async fn trip(
|
||||
Path(id): Path<Uuid>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<(StatusCode, Html<String>), (StatusCode, Html<String>)> {
|
||||
let mut trip: models::Trip =
|
||||
query("SELECT id,name,start_date,end_date,state,location,temp_min,temp_max FROM trips WHERE id = ?")
|
||||
.bind(id.to_string())
|
||||
.fetch_one(&state.database_pool)
|
||||
.map_ok(std::convert::TryInto::try_into)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| match e {
|
||||
sqlx::Error::RowNotFound => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Html::from(format!("trip with id {} not found", id)),
|
||||
),
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())),
|
||||
})?
|
||||
.map_err(|e: Error| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?;
|
||||
|
||||
trip.load_triptypes(&state.database_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Html::from(
|
||||
Root::build(
|
||||
components::Trip::build(&trip).into_markup(),
|
||||
&TopLevelPage::Trips,
|
||||
)
|
||||
.into_string(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
async fn trip_type_remove(
|
||||
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Redirect, (StatusCode, Html<String>)> {
|
||||
let results = query(
|
||||
"DELETE FROM trips_to_triptypes AS ttt
|
||||
WHERE ttt.trip_id = ?
|
||||
AND ttt.trip_type_id = ?
|
||||
",
|
||||
)
|
||||
.bind(trip_id.to_string())
|
||||
.bind(type_id.to_string())
|
||||
.execute(&state.database_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, Html::from(e.to_string())))?;
|
||||
|
||||
if results.rows_affected() == 0 {
|
||||
Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Html::from(format!("type {type_id} is not active for trip {trip_id}")),
|
||||
))
|
||||
} else {
|
||||
Ok(Redirect::to(&format!("/trip/{trip_id}/")))
|
||||
}
|
||||
}
|
||||
|
||||
async fn trip_type_add(
|
||||
Path((trip_id, type_id)): Path<(Uuid, Uuid)>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Redirect, (StatusCode, Html<String>)> {
|
||||
let results = query(
|
||||
"INSERT INTO trips_to_triptypes
|
||||
(trip_id, trip_type_id) VALUES (?, ?)",
|
||||
)
|
||||
.bind(trip_id.to_string())
|
||||
.bind(type_id.to_string())
|
||||
.execute(&state.database_pool)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::Database(ref error) => {
|
||||
let sqlite_error = error.downcast_ref::<SqliteError>();
|
||||
if let Some(code) = sqlite_error.code() {
|
||||
match &*code {
|
||||
"787" => {
|
||||
// SQLITE_CONSTRAINT_FOREIGNKEY
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
// TODO: this is not perfect, as both foreign keys
|
||||
// may be responsible for the error. how can we tell
|
||||
// which one?
|
||||
Html::from(format!("invalid id: {}", code.to_string())),
|
||||
)
|
||||
}
|
||||
"2067" => {
|
||||
// SQLITE_CONSTRAINT_UNIQUE
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Html::from(format!(
|
||||
"type {type_id} is already active for trip {trip_id}"
|
||||
)),
|
||||
)
|
||||
}
|
||||
_ => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Html::from(format!(
|
||||
"got error with unknown code: {}",
|
||||
sqlite_error.to_string()
|
||||
)),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Html::from(format!(
|
||||
"got error without code: {}",
|
||||
sqlite_error.to_string()
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Html::from(format!("got unknown error: {}", e.to_string())),
|
||||
),
|
||||
})?;
|
||||
|
||||
Ok(Redirect::to(&format!("/trip/{trip_id}/")))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use sqlx::{sqlite::SqliteRow, Row};
|
||||
use sqlx::{
|
||||
database::Database,
|
||||
database::HasValueRef,
|
||||
sqlite::{Sqlite, SqliteRow},
|
||||
Decode, Row,
|
||||
};
|
||||
use std::convert;
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
@@ -13,6 +19,7 @@ pub enum Error {
|
||||
SqlError { description: String },
|
||||
UuidError { description: String },
|
||||
NotFoundError { description: String },
|
||||
InvalidEnumError { description: String },
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
@@ -27,6 +34,9 @@ impl fmt::Display for Error {
|
||||
Self::NotFoundError { description } => {
|
||||
write!(f, "Not found: {description}")
|
||||
}
|
||||
Self::InvalidEnumError { description } => {
|
||||
write!(f, "Enum error: {description}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,11 +66,41 @@ impl convert::From<sqlx::Error> for Error {
|
||||
|
||||
impl error::Error for Error {}
|
||||
|
||||
#[derive(sqlx::Type)]
|
||||
pub enum TripState {
|
||||
Planning,
|
||||
Planned,
|
||||
Active,
|
||||
Review,
|
||||
Done,
|
||||
}
|
||||
|
||||
impl fmt::Display for TripState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Planning => "Planning",
|
||||
Self::Planned => "Planned",
|
||||
Self::Active => "Active",
|
||||
Self::Review => "Review",
|
||||
Self::Done => "Done",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Trip {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub start_date: time::Date,
|
||||
pub end_date: time::Date,
|
||||
pub state: TripState,
|
||||
pub location: String,
|
||||
pub temp_min: i32,
|
||||
pub temp_max: i32,
|
||||
types: Option<Vec<TripType>>,
|
||||
}
|
||||
|
||||
impl TryFrom<SqliteRow> for Trip {
|
||||
@@ -70,18 +110,91 @@ impl TryFrom<SqliteRow> for Trip {
|
||||
let name: &str = row.try_get("name")?;
|
||||
let id: &str = row.try_get("id")?;
|
||||
let start_date: time::Date = row.try_get("start_date")?;
|
||||
let end_date: time::Date = row.try_get("start_date")?;
|
||||
let end_date: time::Date = row.try_get("end_date")?;
|
||||
let state: TripState = row.try_get("state")?;
|
||||
let location = row.try_get("location")?;
|
||||
let temp_min = row.try_get("temp_min")?;
|
||||
let temp_max = row.try_get("temp_max")?;
|
||||
|
||||
let id: Uuid = Uuid::try_parse(id)?;
|
||||
|
||||
Ok(Trip {
|
||||
name: name.to_string(),
|
||||
id,
|
||||
name: name.to_string(),
|
||||
start_date,
|
||||
end_date,
|
||||
state,
|
||||
location,
|
||||
temp_min,
|
||||
temp_max,
|
||||
types: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Trip {
|
||||
pub fn types(&'a self) -> &Vec<TripType> {
|
||||
self.types
|
||||
.as_ref()
|
||||
.expect("you need to call load_triptypes()")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Trip {
|
||||
pub async fn load_triptypes(
|
||||
&'a mut self,
|
||||
pool: &sqlx::Pool<sqlx::Sqlite>,
|
||||
) -> Result<(), Error> {
|
||||
let types = sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
type.id as id,
|
||||
type.name as name,
|
||||
CASE WHEN inner.id IS NOT NULL THEN true ELSE false END AS active
|
||||
FROM triptypes AS type
|
||||
LEFT JOIN (
|
||||
SELECT type.id as id, type.name as name
|
||||
FROM trips as trip
|
||||
INNER JOIN trips_to_triptypes as ttt
|
||||
ON ttt.trip_id = trip.id
|
||||
INNER JOIN triptypes AS type
|
||||
ON type.id == ttt.trip_type_id
|
||||
WHERE trip.id = ?
|
||||
) AS inner
|
||||
ON inner.id = type.id
|
||||
",
|
||||
)
|
||||
.bind(self.id.to_string())
|
||||
.fetch(pool)
|
||||
.map_ok(std::convert::TryInto::try_into)
|
||||
.try_collect::<Vec<Result<TripType, Error>>>()
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<TripType>, Error>>()?;
|
||||
|
||||
self.types = Some(types);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TripType {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
impl TryFrom<SqliteRow> for TripType {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
|
||||
let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
|
||||
let name: String = row.try_get::<&str, _>("name")?.to_string();
|
||||
let active: bool = row.try_get("active")?;
|
||||
|
||||
Ok(Self { id, name, active })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Category {
|
||||
pub id: Uuid,
|
||||
|
||||
Reference in New Issue
Block a user