feat: ability to complete a task #35

Merged
matous-volf merged 6 commits from feat/task-complete into main 2024-09-07 12:58:36 +00:00
6 changed files with 142 additions and 44 deletions

1
Cargo.lock generated
View File

@ -2907,6 +2907,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
"time",
"tracing", "tracing",
"tracing-wasm", "tracing-wasm",
"validator", "validator",

View File

@ -23,6 +23,7 @@ tracing-wasm = "0.2.1"
serde_with = { version = "3.9.0", features = ["chrono_0_4"] } serde_with = { version = "3.9.0", features = ["chrono_0_4"] }
async-std = "1.12.0" async-std = "1.12.0"
dioxus-query = "0.5.1" dioxus-query = "0.5.1"
time = "0.3.36"
[features] [features]
default = [] default = []

View File

@ -3,15 +3,21 @@ use crate::models::task::Task;
use dioxus::core_macro::rsx; use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element; use dioxus::dioxus_core::Element;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_query::prelude::use_query_client;
use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::server::tasks::complete_task;
#[component] #[component]
pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element { pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element {
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
let mut task_being_edited = use_context::<Signal<Option<Task>>>(); let mut task_being_edited = use_context::<Signal<Option<Task>>>();
rsx! { rsx! {
div { div {
class: format!("flex flex-col {}", class.unwrap_or("")), class: format!("flex flex-col {}", class.unwrap_or("")),
for task in tasks { {tasks.iter().cloned().map(|task| {
let task_clone = task.clone();
rsx! {
div { div {
key: "{task.id()}", key: "{task.id()}",
class: format!( class: format!(
@ -33,7 +39,28 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
), ),
onclick: move |_| task_being_edited.set(Some(task.clone())), onclick: move |_| task_being_edited.set(Some(task.clone())),
i { i {
class: "fa-regular fa-square text-3xl text-zinc-600", class: format!(
"{} text-3xl text-zinc-500",
if *(task_clone.category()) == Category::Done {
"fa solid fa-square-check"
} else {
"fa-regular fa-square"
}
),
onclick: move |event| {
// To prevent editing the task.
event.stop_propagation();
let task = task_clone.clone();
async move {
let completed_task = complete_task(task.id()).await;
query_client.invalidate_queries(&[
QueryKey::Tasks,
QueryKey::TasksInCategory(
completed_task.unwrap().category().clone()
),
]);
}
}
}, },
div { div {
class: "flex flex-col", class: "flex flex-col",
@ -67,6 +94,7 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
} }
} }
} }
})}
} }
} }
} }

View File

@ -83,7 +83,7 @@ impl FromSql<Jsonb, Pg> for Category {
} }
} }
#[derive(Serialize, Deserialize, Hash, Clone, Debug)] #[derive(Serialize, Deserialize, PartialEq, Hash, Clone, Debug)]
pub enum ReoccurrenceInterval { pub enum ReoccurrenceInterval {
coderabbitai[bot] commented 2024-09-07 12:55:28 +00:00 (Migrated from github.com)
Review

Approved: Enhancements to ReoccurrenceInterval and Reoccurrence.

The addition of PartialEq to ReoccurrenceInterval and the new start_date method in Reoccurrence are beneficial for improving the model's functionality and encapsulation. Consider adding documentation for the new start_date method to clarify its usage and purpose.

Also applies to: 105-107

**Approved: Enhancements to `ReoccurrenceInterval` and `Reoccurrence`.** The addition of `PartialEq` to `ReoccurrenceInterval` and the new `start_date` method in `Reoccurrence` are beneficial for improving the model's functionality and encapsulation. Consider adding documentation for the new `start_date` method to clarify its usage and purpose. Also applies to: 105-107 <!-- This is an auto-generated comment by CodeRabbit -->
Day, Day,
Month, Month,
@ -102,6 +102,10 @@ impl Reoccurrence {
Self { start_date, interval, length } Self { start_date, interval, length }
} }
pub fn start_date(&self) -> NaiveDate {
self.start_date
}
pub fn interval(&self) -> &ReoccurrenceInterval { pub fn interval(&self) -> &ReoccurrenceInterval {
&self.interval &self.interval
} }

View File

@ -69,3 +69,9 @@ impl NewTask {
Self { title, deadline, category, project_id } Self { title, deadline, category, project_id }
} }
} }
impl From<Task> for NewTask {
fn from(task: Task) -> Self {
Self::new(task.title, task.deadline, task.category, task.project_id)
}
}
coderabbitai[bot] commented 2024-09-07 12:55:28 +00:00 (Migrated from github.com)
Review

Approved: Implementation of From<Task> for NewTask.

The new implementation for converting a Task instance to a NewTask is well-implemented and promotes code reuse and simplicity. Consider adding unit tests to cover this new functionality to ensure it behaves as expected.

Would you like me to help with writing the unit tests for this conversion?

**Approved: Implementation of `From<Task> for NewTask`.** The new implementation for converting a `Task` instance to a `NewTask` is well-implemented and promotes code reuse and simplicity. Consider adding unit tests to cover this new functionality to ensure it behaves as expected. Would you like me to help with writing the unit tests for this conversion? <!-- This is an auto-generated comment by CodeRabbit -->

View File

@ -1,12 +1,14 @@
use chrono::{Datelike, Days, Months, NaiveDate};
use crate::errors::error::Error; use crate::errors::error::Error;
use crate::errors::error_vec::ErrorVec; use crate::errors::error_vec::ErrorVec;
use crate::models::task::{NewTask, Task}; use crate::models::task::{NewTask, Task};
use crate::server::database_connection::establish_database_connection; use crate::server::database_connection::establish_database_connection;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper};
use dioxus::prelude::*; use dioxus::prelude::*;
use time::util::days_in_year_month;
use validator::Validate; use validator::Validate;
use crate::errors::task_error::TaskError; use crate::errors::task_error::TaskError;
use crate::models::category::Category; use crate::models::category::{Category, ReoccurrenceInterval};
#[server] #[server]
pub(crate) async fn create_task(new_task: NewTask) pub(crate) async fn create_task(new_task: NewTask)
@ -30,6 +32,24 @@ pub(crate) async fn create_task(new_task: NewTask)
Ok(new_task) Ok(new_task)
} }
#[server]
pub(crate) async fn get_task(task_id: i32) -> Result<Task, ServerFnError<ErrorVec<Error>>> {
use crate::schema::tasks::dsl::*;
let mut connection = establish_database_connection()
.map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?;
let task = tasks
.find(task_id)
.select(Task::as_select())
.first(&mut connection)
.optional()
.map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?;
// TODO: Handle not finding the task.
Ok(task.unwrap())
}
#[server] #[server]
pub(crate) async fn get_tasks_in_category(filtered_category: Category) pub(crate) async fn get_tasks_in_category(filtered_category: Category)
-> Result<Vec<Task>, ServerFnError<ErrorVec<Error>>> { -> Result<Vec<Task>, ServerFnError<ErrorVec<Error>>> {
@ -78,3 +98,41 @@ pub(crate) async fn edit_task(task_id: i32, new_task: NewTask)
Ok(updated_task) Ok(updated_task)
} }
#[server]
pub(crate) async fn complete_task(task_id: i32) -> Result<Task, ServerFnError<ErrorVec<Error>>> {
let task = get_task(task_id).await?;
let mut new_task = NewTask::from(task);
if let Category::Calendar {
reoccurrence: Some(reoccurrence),
date,
..
} = &mut new_task.category {
match reoccurrence.interval() {
ReoccurrenceInterval::Day => *date = *date + Days::new(reoccurrence.length() as u64),
ReoccurrenceInterval::Month | ReoccurrenceInterval::Year => {
*date = *date + Months::new(
reoccurrence.length() *
if *(reoccurrence.interval()) == ReoccurrenceInterval::Year
{ 12 } else { 1 }
);
*date = NaiveDate::from_ymd_opt(
date.year(),
date.month(),
reoccurrence.start_date().day().min(days_in_year_month(
date.year(),
(date.month() as u8).try_into().unwrap(),
) as u32),
).unwrap()
}
}
} else {
new_task.category = Category::Done;
}
let updated_task = edit_task(task_id, new_task).await
.map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?;
Ok(updated_task)
}