update
This commit is contained in:
@@ -28,7 +28,7 @@ features = ["full"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
features = ["derive", "rc"]
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "0.8"
|
||||
|
||||
236
api/src/db.rs
236
api/src/db.rs
@@ -1,3 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use rusqlite;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -12,6 +15,8 @@ use super::PreparationStep;
|
||||
use super::Trip;
|
||||
use super::TripParameters;
|
||||
use super::TripState;
|
||||
use super::TripItem;
|
||||
use super::TripItemStatus;
|
||||
|
||||
pub fn load() -> rusqlite::Result<()> {
|
||||
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 {
|
||||
conn.execute(
|
||||
"INSERT INTO
|
||||
@@ -1040,3 +1059,220 @@ pub fn get_trips() -> rusqlite::Result<Vec<Trip>> {
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ pub mod trip;
|
||||
pub use trip::Trip;
|
||||
pub use trip::TripParameters;
|
||||
pub use trip::TripState;
|
||||
pub use trip::TripItem;
|
||||
pub use trip::TripItemStatus;
|
||||
|
||||
pub fn get_list(id: Uuid) -> Option<packagelist::PackageList> {
|
||||
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> {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -131,6 +131,40 @@ pub fn new() -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
|
||||
.map(|| warp::reply::json(&super::get_trips()))
|
||||
.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)
|
||||
.or(lists)
|
||||
.or(list)
|
||||
@@ -138,6 +172,8 @@ pub fn new() -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
|
||||
.or(list_items)
|
||||
.or(preparation)
|
||||
.or(trips)
|
||||
.or(trip)
|
||||
.or(trip_items)
|
||||
.recover(handle_rejection)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::PackageItem;
|
||||
use crate::PackageList;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TripItemStatus {
|
||||
@@ -9,6 +14,36 @@ pub enum TripItemStatus {
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TripParameters {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import PackageList from "./routes/PackageList.svelte";
|
||||
import Preparation from "./routes/Preparation.svelte";
|
||||
import Trips from "./routes/Trips.svelte";
|
||||
import Trip from "./routes/Trip.svelte";
|
||||
import NotFound from "./routes/NotFound.svelte";
|
||||
|
||||
function normalize(path) {
|
||||
return path.replace(/\/+$/, '') + "/";
|
||||
@@ -32,6 +34,10 @@
|
||||
} else if (urlParts[0] == "trips" && urlParts.length == 1) {
|
||||
console.log("=> 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) {
|
||||
console.log("=> PackageList");
|
||||
currentRoute = PackageList;
|
||||
@@ -45,6 +51,7 @@
|
||||
data = {list_id: urlParts[1], item_id: urlParts[3]};
|
||||
} else {
|
||||
console.log("No matching route found");
|
||||
currentRoute = NotFound;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
ui/src/routes/NotFound.svelte
Normal file
10
ui/src/routes/NotFound.svelte
Normal 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
78
ui/src/routes/Trip.svelte
Normal 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>
|
||||
@@ -36,7 +36,7 @@
|
||||
{:then trips}
|
||||
{#each trips as trip}
|
||||
<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.parameters.days}</td>
|
||||
{#if trip.state == "active"}
|
||||
|
||||
Reference in New Issue
Block a user