add todos

This commit is contained in:
2023-09-13 00:44:59 +02:00
parent 4c850f6c0b
commit 6a6c62d736
11 changed files with 495 additions and 5 deletions

View File

@@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n todo.id AS id,\n todo.description AS description,\n todo.done AS done\n FROM trip_todos AS todo\n INNER JOIN trips\n ON trips.id = todo.trip_id\n WHERE \n trips.id = $1\n AND trips.user_id = $2\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "done",
"ordinal": 2,
"type_info": "Bool"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false
]
},
"hash": "17dfc8ae16d077ed71976012315376e1df403cb81ef173cb8811a5481186db7a"
}

View File

@@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n todo.id AS id,\n todo.description AS description,\n todo.done AS done\n FROM trip_todos AS todo\n INNER JOIN trips\n ON trips.id = todo.trip_id\n WHERE \n trips.id = $1\n AND todo.id = $2\n AND trips.user_id = $3\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "done",
"ordinal": 2,
"type_info": "Bool"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false
]
},
"hash": "a9e75a36e019bb54ff06443a2ce98c788f88be295b6171f2f36e08c91109e380"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE trip_todos\n SET done = ?\n WHERE trip_id = ?\n AND id = ?\n AND EXISTS(SELECT 1 FROM trips WHERE id = ? AND user_id = ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "d29d72b5c9dbf34d672aa271823b3b87f29e85ccdf67dc40f9ce372d5db9727d"
}

View File

@@ -1 +1,9 @@
-- Add migration script here
CREATE TABLE "trip_todos" (
id VARCHAR(36) NOT NULL,
trip_id VARCHAR(36) NOT NULL,
description TEXT NOT NULL,
done BOOLEAN NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(trip_id) REFERENCES "trips" (id)
)

View File

@@ -1,7 +1,6 @@
use super::Error;
use crate::{sqlite, Context};
use futures::{TryFutureExt, TryStreamExt};
use uuid::Uuid;
pub struct Inventory {

View File

@@ -8,11 +8,12 @@ use super::{
use crate::{sqlite, Context};
use futures::{TryFutureExt, TryStreamExt};
use serde::{Deserialize, Serialize};
use time;
use uuid::Uuid;
pub mod todos;
// #[macro_use]
// mod macros {
// macro_rules! build_state_query {
@@ -491,6 +492,7 @@ impl TryFrom<DbTripRow> for Trip {
temp_min: row.temp_min,
temp_max: row.temp_max,
comment: row.comment,
todos: None,
types: None,
categories: None,
})
@@ -508,8 +510,9 @@ pub struct Trip {
pub temp_min: Option<i64>,
pub temp_max: Option<i64>,
pub comment: Option<String>,
pub(crate) types: Option<Vec<TripType>>,
pub(crate) categories: Option<Vec<TripCategory>>,
pub todos: Option<Vec<todos::Todo>>,
pub types: Option<Vec<TripType>>,
pub categories: Option<Vec<TripCategory>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -990,7 +993,12 @@ impl Trip {
pub fn categories(&self) -> &Vec<TripCategory> {
self.categories
.as_ref()
.expect("you need to call load_trips_types()")
.expect("you need to call load_categories()")
}
#[tracing::instrument]
pub fn todos(&self) -> &Vec<todos::Todo> {
self.todos.as_ref().expect("you need to call load_todos()")
}
#[tracing::instrument]
@@ -1009,6 +1017,12 @@ impl Trip {
.sum::<i64>()
}
#[tracing::instrument]
pub async fn load_todos(&mut self, ctx: &Context, pool: &sqlite::Pool) -> Result<(), Error> {
self.todos = Some(todos::Todo::load(ctx, pool, self.id).await?);
Ok(())
}
#[tracing::instrument]
pub async fn load_trips_types(
&mut self,

176
src/models/trips/todos.rs Normal file
View File

@@ -0,0 +1,176 @@
use uuid::Uuid;
use crate::{
models::{Error, QueryError},
sqlite, Context,
};
#[derive(Debug, PartialEq, Eq)]
pub enum State {
Todo,
Done,
}
impl From<bool> for State {
fn from(done: bool) -> Self {
if done {
Self::Done
} else {
Self::Todo
}
}
}
impl From<State> for bool {
fn from(value: State) -> Self {
match value {
State::Todo => false,
State::Done => true,
}
}
}
#[derive(Debug)]
pub struct Todo {
pub id: Uuid,
pub description: String,
pub state: State,
}
struct TodoRow {
id: String,
description: String,
done: bool,
}
impl TryFrom<TodoRow> for Todo {
type Error = Error;
fn try_from(row: TodoRow) -> Result<Self, Self::Error> {
Ok(Todo {
id: Uuid::try_parse(&row.id)?,
description: row.description,
state: row.done.into(),
})
}
}
impl Todo {
pub fn is_done(&self) -> bool {
self.state == State::Done
}
pub async fn load(
ctx: &Context,
pool: &sqlite::Pool,
trip_id: Uuid,
) -> Result<Vec<Self>, Error> {
let trip_id_param = trip_id.to_string();
let user_id = ctx.user.id.to_string();
let todos: Vec<Todo> = crate::query_all!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Todo,
},
pool,
TodoRow,
Todo,
r#"
SELECT
todo.id AS id,
todo.description AS description,
todo.done AS done
FROM trip_todos AS todo
INNER JOIN trips
ON trips.id = todo.trip_id
WHERE
trips.id = $1
AND trips.user_id = $2
"#,
trip_id_param,
user_id,
)
.await?;
Ok(todos)
}
#[tracing::instrument]
pub async fn find(
ctx: &Context,
pool: &sqlite::Pool,
trip_id: Uuid,
todo_id: Uuid,
) -> Result<Option<Self>, Error> {
let trip_id_param = trip_id.to_string();
let todo_id_param = todo_id.to_string();
let user_id = ctx.user.id.to_string();
crate::query_one!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Todo,
},
pool,
TodoRow,
Self,
r#"
SELECT
todo.id AS id,
todo.description AS description,
todo.done AS done
FROM trip_todos AS todo
INNER JOIN trips
ON trips.id = todo.trip_id
WHERE
trips.id = $1
AND todo.id = $2
AND trips.user_id = $3
"#,
trip_id_param,
todo_id_param,
user_id,
)
.await
}
#[tracing::instrument]
pub async fn set_state(
ctx: &Context,
pool: &sqlite::Pool,
trip_id: Uuid,
todo_id: Uuid,
state: State,
) -> Result<(), Error> {
let user_id = ctx.user.id.to_string();
let trip_id_param = trip_id.to_string();
let todo_id_param = todo_id.to_string();
let done = state == State::Done;
let result = crate::execute!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Update,
component: sqlite::Component::Trips,
},
pool,
r#"
UPDATE trip_todos
SET done = ?
WHERE trip_id = ?
AND id = ?
AND EXISTS(SELECT 1 FROM trips WHERE id = ? AND user_id = ?)"#,
done,
trip_id_param,
todo_id_param,
trip_id_param,
user_id
)
.await?;
(result.rows_affected() != 0).then_some(()).ok_or_else(|| {
Error::Query(QueryError::NotFound {
description: format!("todo {todo_id} not found for trip {trip_id}"),
})
})
}
}

View File

@@ -139,6 +139,14 @@ pub fn router(state: AppState) -> Router {
.route(
"/:id/items/:id/unready",
get(trip_item_set_unready).post(trip_item_set_unready_htmx),
)
.route(
"/:id/todo/:id/done",
get(trip_todo_done).post(trip_todo_done_htmx),
)
.route(
"/:id/todo/:id/undone",
get(trip_todo_undone).post(trip_todo_undone_htmx),
),
)
.nest(

View File

@@ -437,6 +437,8 @@ pub async fn trip(
trip.load_trips_types(&ctx, &state.database_pool).await?;
trip.load_todos(&ctx, &state.database_pool).await?;
trip.sync_trip_items_with_inventory(&ctx, &state.database_pool)
.await?;
@@ -1238,3 +1240,97 @@ pub async fn trip_item_packagelist_set_unready_htmx(
trip_id, &item,
))
}
#[tracing::instrument]
pub async fn trip_todo_done_htmx(
Extension(current_user): Extension<models::user::User>,
State(state): State<AppState>,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user);
models::trips::todos::Todo::set_state(
&ctx,
&state.database_pool,
trip_id,
todo_id,
models::trips::todos::State::Done,
)
.await?;
let todo_item = models::trips::todos::Todo::find(&ctx, &state.database_pool, trip_id, todo_id)
.await?
.ok_or_else(|| {
Error::Request(RequestError::NotFound {
message: format!("todo with id {todo_id} not found"),
})
})?;
Ok(view::trip::TripTodo::build(&trip_id, &todo_item))
}
#[tracing::instrument]
pub async fn trip_todo_done(
Extension(current_user): Extension<models::user::User>,
State(state): State<AppState>,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap,
) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user);
models::trips::todos::Todo::set_state(
&ctx,
&state.database_pool,
trip_id,
todo_id,
models::trips::todos::State::Done,
)
.await?;
Ok(Redirect::to(get_referer(&headers)?))
}
#[tracing::instrument]
pub async fn trip_todo_undone_htmx(
Extension(current_user): Extension<models::user::User>,
State(state): State<AppState>,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user);
models::trips::todos::Todo::set_state(
&ctx,
&state.database_pool,
trip_id,
todo_id,
models::trips::todos::State::Todo,
)
.await?;
let todo_item = models::trips::todos::Todo::find(&ctx, &state.database_pool, trip_id, todo_id)
.await?
.ok_or_else(|| {
Error::Request(RequestError::NotFound {
message: format!("todo with id {todo_id} not found"),
})
})?;
Ok(view::trip::TripTodo::build(&trip_id, &todo_item))
}
#[tracing::instrument]
pub async fn trip_todo_undone(
Extension(current_user): Extension<models::user::User>,
State(state): State<AppState>,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap,
) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user);
models::trips::todos::Todo::set_state(
&ctx,
&state.database_pool,
trip_id,
todo_id,
models::trips::todos::State::Todo,
)
.await?;
Ok(Redirect::to(get_referer(&headers)?))
}

View File

@@ -89,6 +89,7 @@ pub enum Component {
Inventory,
User,
Trips,
Todo,
}
impl fmt::Display for Component {
@@ -100,6 +101,7 @@ impl fmt::Display for Component {
Self::Inventory => "inventory",
Self::User => "user",
Self::Trips => "trips",
Self::Todo => "todo",
}
)
}
@@ -167,6 +169,7 @@ macro_rules! query_all {
( $class:expr, $pool:expr, $struct_row:path, $struct_into:path, $query:expr, $( $args:tt )* ) => {
{
use tracing::Instrument as _;
use futures::TryStreamExt as _;
async {
$crate::sqlite::sqlx_query($class, $query, &[]);
let result: Result<Vec<$struct_into>, Error> = sqlx::query_as!(
@@ -293,6 +296,7 @@ macro_rules! execute_returning {
( $class:expr, $pool:expr, $query:expr, $t:path, $fn:expr, $( $args:tt )*) => {
{
use tracing::Instrument as _;
use futures::TryFutureExt as _;
async {
$crate::sqlite::sqlx_query($class, $query, &[]);
let result: Result<$t, Error> = sqlx::query!(
@@ -317,6 +321,7 @@ macro_rules! execute_returning_uuid {
( $class:expr, $pool:expr, $query:expr, $( $args:tt )*) => {
{
use tracing::Instrument as _;
use futures::TryFutureExt as _;
async {
$crate::sqlite::sqlx_query($class, $query, &[]);
let result: Result<Uuid, Error> = sqlx::query!(

View File

@@ -370,6 +370,7 @@ impl Trip {
}
}
(TripInfo::build(trip_edit_attribute, trip))
(TripTodoList::build(trip))
(TripComment::build(trip))
(TripItems::build(active_category, trip))
}
@@ -794,6 +795,113 @@ impl TripInfo {
}
}
pub struct TripTodo;
impl TripTodo {
#[tracing::instrument]
pub fn build(trip_id: &Uuid, todo: &models::trips::todos::Todo) -> Markup {
let done = todo.is_done();
html!(
li
."flex"
."flex-row"
."justify-start"
."items-stretch"
."bg-green-50"[done]
."bg-red-50"[!done]
."hover:bg-white"[!done]
."h-full"
{
@if done {
a
."flex"
."flex-row"
."aspect-square"
href={
"/trips/" (trip_id)
"/todo/" (todo.id)
"/undone"
}
hx-post={
"/trips/" (trip_id)
"/todo/" (todo.id)
"/undone"
}
hx-target="closest li"
hx-swap="outerHTML"
{
span
."mdi"
."m-auto"
."text-xl"
."mdi-check"
{}
}
} @else {
a
."flex"
."flex-row"
."aspect-square"
href={
"/trips/" (trip_id)
"/todo/" (todo.id)
"/done"
}
hx-post={
"/trips/" (trip_id)
"/todo/" (todo.id)
"/done"
}
hx-target="closest li"
hx-swap="outerHTML"
{
span
."mdi"
."m-auto"
."text-xl"
."mdi-checkbox-blank-outline"
{}
}
}
span
."p-2"
{
(todo.description)
}
}
)
}
}
pub struct TripTodoList;
impl TripTodoList {
#[tracing::instrument]
pub fn build(trip: &models::trips::Trip) -> Markup {
let todos = trip.todos();
html!(
div {
h1 ."text-xl" ."mb-5" { "Todos" }
@if todos.is_empty() {
p { "no todos" }
} @else {
ul
."flex"
."flex-col"
{
@for todo in trip.todos() {
(TripTodo::build(&trip.id, &todo))
}
}
}
}
)
}
}
pub struct TripComment;
impl TripComment {