add todos
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
use super::Error;
|
||||
use crate::{sqlite, Context};
|
||||
|
||||
use futures::{TryFutureExt, TryStreamExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Inventory {
|
||||
|
||||
@@ -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
176
src/models/trips/todos.rs
Normal 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}"),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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)?))
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user