This commit is contained in:
2021-11-20 17:19:07 +01:00
parent 2baa2c7257
commit b6686811d9
9 changed files with 414 additions and 2 deletions

View File

@@ -28,7 +28,7 @@ features = ["full"]
[dependencies.serde] [dependencies.serde]
version = "1.0" version = "1.0"
features = ["derive"] features = ["derive", "rc"]
[dependencies.uuid] [dependencies.uuid]
version = "0.8" version = "0.8"

View File

@@ -1,3 +1,6 @@
use std::collections::HashMap;
use std::rc::Rc;
use rusqlite; use rusqlite;
use uuid::Uuid; use uuid::Uuid;
@@ -12,6 +15,8 @@ use super::PreparationStep;
use super::Trip; use super::Trip;
use super::TripParameters; use super::TripParameters;
use super::TripState; use super::TripState;
use super::TripItem;
use super::TripItemStatus;
pub fn load() -> rusqlite::Result<()> { pub fn load() -> rusqlite::Result<()> {
let example_lists = vec![ let example_lists = vec![
@@ -726,6 +731,20 @@ pub fn load() -> rusqlite::Result<()> {
[], [],
)?; )?;
conn.execute(
"CREATE TABLE trip_items (
trip_id TEXT NOT NULL,
packagelistitem_id TEXT NOT NULL,
status INTEGER NOT NULL,
FOREIGN KEY(trip_id) REFERENCES trips(id)
FOREIGN KEY(packagelistitem_id) REFERENCES packagelistitems(id)
PRIMARY KEY(trip_id, packagelistitem_id)
)",
[],
)?;
for list in example_lists { for list in example_lists {
conn.execute( conn.execute(
"INSERT INTO "INSERT INTO
@@ -1040,3 +1059,220 @@ pub fn get_trips() -> rusqlite::Result<Vec<Trip>> {
Ok(trips) Ok(trips)
} }
pub fn get_trip(trip_id: Uuid) -> rusqlite::Result<Option<Trip>> {
let conn = rusqlite::Connection::open("./sqlite.db")?;
let mut statement = conn.prepare(
"SELECT
trips.id AS trip_id,
trips.name AS trip_name,
trips.date AS trip_date,
trips.days AS trip_days,
trips.state AS trip_state
FROM trips
WHERE
trip_id = ?1
",
)?;
let trip_query_result = statement.query_map(rusqlite::params![trip_id], |row| {
Ok(Trip{
id: row.get_unwrap(0),
name: row.get_unwrap(1),
date: row.get_unwrap(2),
parameters: TripParameters{
days: row.get_unwrap(3),
},
package_lists: vec![],
state: row.get_unwrap(4),
})
})?;
let mut trip_query_result: Vec<Trip> = trip_query_result.flatten().collect();
if trip_query_result.is_empty() {
return Ok(None);
}
let mut trip: Trip = trip_query_result.remove(0);
let mut statement = conn.prepare(
"SELECT
trip_packagelist.packagelist_id as package_list_id,
packagelists.name as package_list_name
FROM trip_packagelist
INNER JOIN packagelists
ON packagelists.id == trip_packagelist.packagelist_id
WHERE
trip_packagelist.trip_id = ?1
",
)?;
let lists: Vec<(Uuid, String)> = statement
.query_map(rusqlite::params![trip_id], |row| {
Ok((row.get_unwrap(0), row.get_unwrap(1)))
})?
.flatten()
.collect();
trip.set_package_lists(lists);
Ok(Some(trip))
}
pub fn get_trip_items(trip_id: Uuid) -> rusqlite::Result<Option<Vec<TripItem>>> {
// there has to be better way to get the rested option result
let trip: Option<Trip> = get_trip(trip_id)?;
println!("Getting trip items");
if let None = trip {
return Ok(None)
}
let trip = trip.unwrap();
let conn = rusqlite::Connection::open("./sqlite.db")?;
let mut statement = conn.prepare(
"SELECT
trip_items.packagelistitem_id,
status AS status,
pli.id AS packageitem_id,
pli.name AS packageitem_name,
pli.count AS packageitem_count,
FROM trip_items
INNER JOIN packagelistitems as pli
ON trip_items.packagelistitem_id = pli.id
WHERE
trip_items.trip_id = ?1
",
)?;
let mut result : Vec<TripItem> = Vec::new();
let trip_items : Vec<TripItem> = statement.query_map(rusqlite::params![trip_id], |row| {
let package_item = PackageItem::new(
row.get_unwrap(2),
row.get_unwrap(3),
ItemSize::None,
row.get_unwrap(4),
ItemUsage::Singleton,
Preparation::None,
);
let package_list_id = row.get_unwrap(5);
let package_list = Rc::new(PackageList::new(
package_list_id,
row.get_unwrap(6),
));
Ok(TripItem{
status: row.get_unwrap(1),
package_item,
package_list: Rc::clone(package_lists.get(&package_list_id).unwrap()),
})
})?.flatten().collect();
println!("Found {} trip items in database", trip_items.len());
// get all package list items, so we know what we SHOULD have
let mut package_list_items = vec![];
for package_list in &trip.package_lists {
package_list_items.append(&mut get_list_items(package_list.id)?);
}
println!("There should be {} items according to the package lists", package_list_items.len());
let package_list_item_ids: Vec<Uuid> = package_list_items.iter().map(|item| item.id).collect();
// three possibilities:
//
// * The trip item is there => use it
// * The trip item is not there => create it with defaults
// * There is a trip item that should no be there (e.g. because the package
// list was changed and a package item was removed)
//
// We can either just not use it (letting it rot in the database). It will
// never be removed automatically. But if we later add it again, something
// will break (e.g. if someone re-attaches the package list to the trip).
// first run: get all trip items that have the trip id set. We need to
// check this list against the items. If there are any returned that are
// NOT part of the package list items, we remove them
for package_item in package_list_items {
for trip_item in &trip_items {
let item_id = trip_item.package_item.id;
if !package_list_item_ids.contains(&item_id) {
// there is a trip item that does no longer have a corresponding
// package list item (because a package list was detached).
// Remove it
println!("Would remove package item with id {} (\"{}\")",
package_item.id, package_item.name);
// conn.execute(
// "DELETE
// FROM trip_items
// WHERE
// trip_items.trip_id = ?1
// AND
// trip_items.packagelistitem_id = ?2
// ",
// rusqlite::params![trip_id, package_item.id]
// )?;
continue
}
}
if !trip_items.iter().map(|i| i.package_item.id).collect::<Vec<Uuid>>().contains(&package_item.id) {
println!("Found new trip item!");
println!("Looking into hashmap with {:?}", package_item.id);
let list = Rc::clone(package_lists.get(&package_item.id).unwrap());
let new_trip_item = TripItem {
status: TripItemStatus::Pending,
package_item,
package_list: list,
};
conn.execute(
"INSERT INTO
trip_items (
trip_id,
packagelistitem_id,
status
) VALUES (
?1,
?2,
?3
)
",
rusqlite::params![
trip_id,
new_trip_item.package_item.id,
new_trip_item.status
]
)?;
result.push(new_trip_item);
continue;
}
// If we've come so far, the trip is actually in the database, so let's
// just use it
println!("Using trip item from database");
let list = Rc::clone(package_lists.get(&package_item.id).unwrap());
result.push(TripItem {
status: TripItemStatus::Pending,
package_item,
package_list: list,
});
}
Ok(Some(result))
}

View File

@@ -19,6 +19,8 @@ pub mod trip;
pub use trip::Trip; pub use trip::Trip;
pub use trip::TripParameters; pub use trip::TripParameters;
pub use trip::TripState; pub use trip::TripState;
pub use trip::TripItem;
pub use trip::TripItemStatus;
pub fn get_list(id: Uuid) -> Option<packagelist::PackageList> { pub fn get_list(id: Uuid) -> Option<packagelist::PackageList> {
self::db::get_list(id).unwrap() self::db::get_list(id).unwrap()
@@ -51,3 +53,11 @@ pub fn new_item(list_id: Uuid, item_name: String, item_count: i32) -> packagelis
pub fn get_trips() -> Vec<Trip> { pub fn get_trips() -> Vec<Trip> {
self::db::get_trips().unwrap() self::db::get_trips().unwrap()
} }
pub fn get_trip(trip_id: Uuid) -> Option<Trip> {
self::db::get_trip(trip_id).unwrap()
}
pub fn get_trip_items(trip_id: Uuid) -> Option<Vec<TripItem>> {
self::db::get_trip_items(trip_id).unwrap()
}

View File

@@ -131,6 +131,40 @@ pub fn new() -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
.map(|| warp::reply::json(&super::get_trips())) .map(|| warp::reply::json(&super::get_trips()))
.with(&cors); .with(&cors);
let trip = warp::path!("v1" / "trips" / String)
.and(warp::path::end())
.and(warp::get())
.and(accept_json)
.and_then(|trip_id: String| async move {
match Uuid::parse_str(&trip_id) {
Ok(trip_id) => {
match &super::get_trip(trip_id) {
Some(trip) => Ok(warp::reply::json(trip)),
None => Err(warp::reject::not_found()),
}
},
Err(_) => Err(warp::reject::custom(InvalidUuid)),
}
})
.with(&cors);
let trip_items = warp::path!("v1" / "trips" / String / "items")
.and(warp::path::end())
.and(warp::get())
.and(accept_json)
.and_then(|trip_id: String| async move {
match Uuid::parse_str(&trip_id) {
Ok(trip_id) => {
match &super::get_trip_items(trip_id) {
Some(trip) => Ok(warp::reply::json(trip)),
None => Err(warp::reject::not_found()),
}
},
Err(_) => Err(warp::reject::custom(InvalidUuid)),
}
})
.with(&cors);
root.or(v1) root.or(v1)
.or(lists) .or(lists)
.or(list) .or(list)
@@ -138,6 +172,8 @@ pub fn new() -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
.or(list_items) .or(list_items)
.or(preparation) .or(preparation)
.or(trips) .or(trips)
.or(trip)
.or(trip_items)
.recover(handle_rejection) .recover(handle_rejection)
.boxed() .boxed()
} }

View File

@@ -1,6 +1,11 @@
use std::rc::Rc;
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
use crate::PackageItem;
use crate::PackageList;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum TripItemStatus { pub enum TripItemStatus {
@@ -9,6 +14,36 @@ pub enum TripItemStatus {
Packed, Packed,
} }
impl rusqlite::types::FromSql for TripItemStatus {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
match value.as_i64()? {
1 => Ok(TripItemStatus::Pending),
2 => Ok(TripItemStatus::Ready),
3 => Ok(TripItemStatus::Packed),
v => Err(rusqlite::types::FromSqlError::OutOfRange(v)),
}
}
}
impl rusqlite::types::ToSql for TripItemStatus {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let v = rusqlite::types::Value::Integer(match self {
TripItemStatus::Pending => 1,
TripItemStatus::Ready => 2,
TripItemStatus::Packed => 3,
});
rusqlite::Result::Ok(rusqlite::types::ToSqlOutput::Owned(v))
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TripItem {
pub status: TripItemStatus,
pub package_item: PackageItem,
pub package_list: Rc<PackageList>,
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TripParameters { pub struct TripParameters {

View File

@@ -8,6 +8,8 @@
import PackageList from "./routes/PackageList.svelte"; import PackageList from "./routes/PackageList.svelte";
import Preparation from "./routes/Preparation.svelte"; import Preparation from "./routes/Preparation.svelte";
import Trips from "./routes/Trips.svelte"; import Trips from "./routes/Trips.svelte";
import Trip from "./routes/Trip.svelte";
import NotFound from "./routes/NotFound.svelte";
function normalize(path) { function normalize(path) {
return path.replace(/\/+$/, '') + "/"; return path.replace(/\/+$/, '') + "/";
@@ -32,6 +34,10 @@
} else if (urlParts[0] == "trips" && urlParts.length == 1) { } else if (urlParts[0] == "trips" && urlParts.length == 1) {
console.log("=> Trips"); console.log("=> Trips");
currentRoute = Trips; currentRoute = Trips;
} else if (urlParts[0] == "trips" && urlParts.length == 2) {
console.log("=> Trip");
currentRoute = Trip;
data = {id: urlParts[1]};
} else if (urlParts[0] == "lists" && urlParts.length == 2) { } else if (urlParts[0] == "lists" && urlParts.length == 2) {
console.log("=> PackageList"); console.log("=> PackageList");
currentRoute = PackageList; currentRoute = PackageList;
@@ -45,6 +51,7 @@
data = {list_id: urlParts[1], item_id: urlParts[3]}; data = {list_id: urlParts[1], item_id: urlParts[3]};
} else { } else {
console.log("No matching route found"); console.log("No matching route found");
currentRoute = NotFound;
} }
} }

View File

@@ -0,0 +1,10 @@
<script lang="ts">
export let redirect;
</script>
<main>
<div>
404 -- Not Found
</div>
<button on:click={() => redirect("/")}>Back to home</button>
</main>

78
ui/src/routes/Trip.svelte Normal file
View File

@@ -0,0 +1,78 @@
<script lang="ts">
export let redirect;
export let data;
export const url = "/trips/"
async function getTrip() {
let response = await fetch(`http://localhost:9000/v1/trips/${data.id}`, {
method: "GET",
headers: {
"Accept": "application/json"
},
cache: "no-store",
});
let trip = await response.json();
return trip;
}
async function getTripItems() {
let response = await fetch(`http://localhost:9000/v1/trips/${data.id}/items`, {
method: "GET",
headers: {
"Accept": "application/json"
},
cache: "no-store",
});
let items = await response.json();
return items;
}
</script>
<main>
<div class="container mx-auto mt-12">
{#await getTrip()}
<p>Loading</p>
{:then trip}
<h2 class="text-3xl mt-12 mb-20 font-semibold text-center mb-5 mt-3">{trip.name}</h2>
<table>
<tr>
<td>Date</td>
<td>{trip.date}</td>
</tr>
<tr>
<td>Duration</td>
<td>{trip.parameters.days} Days</td>
</tr>
<tr>
<td>Status</td>
<td>{trip.state}</td>
</tr>
</table>
<table class="table-auto w-full">
<thead>
<tr class="font-semibold tracking-wider text-left bg-gray-100 uppercase border-b border-gray-400">
<th class="p-3">Name</th>
<th class="p-3">Status</th>
</tr>
</thead>
<tbody>
{#await getTripItems()}
<p>Loading</p>
{:then items}
{#each items as item}
<tr class="border">
<td class="p-3">{item.packageItem.name}</td>
<td class="p-3">{item.status}</td>
</tr>
{/each}
{:catch error}
{error}
{/await}
</tbody>
</table>
{/await}
</div>
</main>

View File

@@ -36,7 +36,7 @@
{:then trips} {:then trips}
{#each trips as trip} {#each trips as trip}
<tr class="border" on:click={e => redirect(url + trip.id)}> <tr class="border" on:click={e => redirect(url + trip.id)}>
<td class="p-3">{trip.name}</td> <td class="p-3" on:click={() => redirect(`/trips/${trip.id}`)}>{trip.name}</td>
<td class="p-3">{trip.date}</td> <td class="p-3">{trip.date}</td>
<td class="p-3">{trip.parameters.days}</td> <td class="p-3">{trip.parameters.days}</td>
{#if trip.state == "active"} {#if trip.state == "active"}