Skip to main content

max / goingson

5.8 KB · 202 lines History Blame Raw
1 //! Annotation and subtask commands for tasks.
2
3 use serde::Deserialize;
4 use std::sync::Arc;
5 use tauri::State;
6 use tracing::instrument;
7
8 use goingson_core::{Annotation, Subtask, TaskId, AnnotationId, SubtaskId};
9
10 use crate::state::{AppState, DESKTOP_USER_ID};
11 use super::{ApiError, OptionNotFound};
12
13 // ============ Types ============
14
15 #[derive(Debug, Deserialize)]
16 #[serde(rename_all = "camelCase")]
17 pub struct AnnotationInput {
18 pub note: String,
19 }
20
21 #[derive(Debug, Deserialize)]
22 #[serde(rename_all = "camelCase")]
23 pub struct SubtaskInput {
24 pub text: String,
25 }
26
27 // ============ Annotation Commands ============
28
29 /// Lists all annotations (timestamped notes) for a task.
30 ///
31 /// # Errors
32 ///
33 /// Returns `NOT_FOUND` if the task doesn't exist.
34 /// Returns `DATABASE_ERROR` if the query fails.
35 #[tauri::command]
36 #[instrument(skip_all)]
37 pub async fn list_annotations(state: State<'_, Arc<AppState>>, task_id: TaskId) -> Result<Vec<Annotation>, ApiError> {
38 // Verify task belongs to user
39 state.tasks
40 .get_by_id(task_id, DESKTOP_USER_ID)
41 .await?
42 .or_not_found("task", task_id)?;
43
44 Ok(state.tasks.get_annotations_for_task(task_id).await?)
45 }
46
47 /// Adds a timestamped note to a task.
48 ///
49 /// # Errors
50 ///
51 /// Returns `VALIDATION_ERROR` if the note is empty.
52 /// Returns `NOT_FOUND` if the task doesn't exist.
53 /// Returns `DATABASE_ERROR` if the insert fails.
54 #[tauri::command]
55 #[instrument(skip_all)]
56 pub async fn add_annotation(state: State<'_, Arc<AppState>>, task_id: TaskId, input: AnnotationInput) -> Result<Annotation, ApiError> {
57 if input.note.trim().is_empty() {
58 return Err(ApiError::validation("note", "Note is required"));
59 }
60
61 let annotation = state.tasks
62 .add_annotation(task_id, DESKTOP_USER_ID, &input.note)
63 .await?
64 .or_not_found("task", task_id)?;
65
66 Ok(annotation)
67 }
68
69 /// Deletes an annotation.
70 ///
71 /// # Errors
72 ///
73 /// Returns `DATABASE_ERROR` if the delete fails.
74 #[tauri::command]
75 #[instrument(skip_all)]
76 pub async fn delete_annotation(state: State<'_, Arc<AppState>>, annotation_id: AnnotationId) -> Result<bool, ApiError> {
77 Ok(state.tasks.delete_annotation(annotation_id, DESKTOP_USER_ID).await?)
78 }
79
80 // ============ Subtask Commands ============
81
82 /// Lists all subtasks (checklist items) for a task.
83 ///
84 /// # Errors
85 ///
86 /// Returns `NOT_FOUND` if the task doesn't exist.
87 /// Returns `DATABASE_ERROR` if the query fails.
88 #[tauri::command]
89 #[instrument(skip_all)]
90 pub async fn list_subtasks(state: State<'_, Arc<AppState>>, task_id: TaskId) -> Result<Vec<Subtask>, ApiError> {
91 // Verify task belongs to user
92 state.tasks
93 .get_by_id(task_id, DESKTOP_USER_ID)
94 .await?
95 .or_not_found("task", task_id)?;
96
97 Ok(state.tasks.get_subtasks_for_task(task_id).await?)
98 }
99
100 /// Adds a subtask (checklist item) to a task.
101 ///
102 /// # Errors
103 ///
104 /// Returns `VALIDATION_ERROR` if the text is empty.
105 /// Returns `NOT_FOUND` if the task doesn't exist.
106 /// Returns `DATABASE_ERROR` if the insert fails.
107 #[tauri::command]
108 #[instrument(skip_all)]
109 pub async fn add_subtask(state: State<'_, Arc<AppState>>, task_id: TaskId, input: SubtaskInput) -> Result<Subtask, ApiError> {
110 if input.text.trim().is_empty() {
111 return Err(ApiError::validation("text", "Subtask text is required"));
112 }
113
114 let subtask = state.tasks
115 .add_subtask(task_id, DESKTOP_USER_ID, &input.text)
116 .await?
117 .or_not_found("task", task_id)?;
118
119 Ok(subtask)
120 }
121
122 /// Toggles the completion status of a subtask.
123 ///
124 /// # Errors
125 ///
126 /// Returns `NOT_FOUND` if the subtask doesn't exist.
127 /// Returns `DATABASE_ERROR` if the update fails.
128 #[tauri::command]
129 #[instrument(skip_all)]
130 pub async fn toggle_subtask(state: State<'_, Arc<AppState>>, subtask_id: SubtaskId) -> Result<Subtask, ApiError> {
131 let subtask = state.tasks
132 .toggle_subtask(subtask_id, DESKTOP_USER_ID)
133 .await?
134 .or_not_found("subtask", subtask_id)?;
135
136 Ok(subtask)
137 }
138
139 /// Updates the text of a subtask.
140 ///
141 /// # Errors
142 ///
143 /// Returns `VALIDATION_ERROR` if the text is empty.
144 /// Returns `NOT_FOUND` if the subtask doesn't exist.
145 /// Returns `DATABASE_ERROR` if the update fails.
146 #[tauri::command]
147 #[instrument(skip_all)]
148 pub async fn update_subtask(state: State<'_, Arc<AppState>>, subtask_id: SubtaskId, input: SubtaskInput) -> Result<Subtask, ApiError> {
149 if input.text.trim().is_empty() {
150 return Err(ApiError::validation("text", "Subtask text is required"));
151 }
152
153 let subtask = state.tasks
154 .update_subtask(subtask_id, DESKTOP_USER_ID, &input.text)
155 .await?
156 .or_not_found("subtask", subtask_id)?;
157
158 Ok(subtask)
159 }
160
161 /// Deletes a subtask.
162 ///
163 /// # Errors
164 ///
165 /// Returns `DATABASE_ERROR` if the delete fails.
166 #[tauri::command]
167 #[instrument(skip_all)]
168 pub async fn delete_subtask(state: State<'_, Arc<AppState>>, subtask_id: SubtaskId) -> Result<bool, ApiError> {
169 Ok(state.tasks.delete_subtask(subtask_id, DESKTOP_USER_ID).await?)
170 }
171
172 /// Links another task as a subtask.
173 ///
174 /// Creates a subtask that references another task. This enables multi-phase
175 /// features where Phase 2 is a full task linked as a subtask of Phase 1.
176 /// The linked subtask's text shows the linked task's description, and its
177 /// completion status syncs with the linked task's status.
178 ///
179 /// # Errors
180 ///
181 /// Returns `NOT_FOUND` if either task doesn't exist.
182 /// Returns `DATABASE_ERROR` if the insert fails.
183 #[tauri::command]
184 #[instrument(skip_all)]
185 pub async fn add_subtask_link(
186 state: State<'_, Arc<AppState>>,
187 task_id: TaskId,
188 linked_task_id: TaskId,
189 ) -> Result<Subtask, ApiError> {
190 // Prevent linking a task to itself
191 if task_id == linked_task_id {
192 return Err(ApiError::validation("linked_task_id", "Cannot link a task to itself"));
193 }
194
195 let subtask = state.tasks
196 .add_subtask_link(task_id, DESKTOP_USER_ID, linked_task_id)
197 .await?
198 .or_not_found("task", task_id)?;
199
200 Ok(subtask)
201 }
202