things3_core/database/mutations/
projects.rs1use crate::{
2 database::{
3 conversions::naive_date_to_things_timestamp, query_builders::TaskUpdateBuilder, validators,
4 ThingsDatabase,
5 },
6 error::{Result as ThingsResult, ThingsError},
7 models::ThingsId,
8};
9use chrono::Utc;
10use tracing::{info, instrument};
11
12impl ThingsDatabase {
13 #[instrument(skip(self))]
21 pub async fn create_project(
22 &self,
23 request: crate::models::CreateProjectRequest,
24 ) -> ThingsResult<ThingsId> {
25 crate::database::validate_date_range(request.start_date, request.deadline)?;
27
28 let id = ThingsId::new_things_native();
30
31 if let Some(area_uuid) = &request.area_uuid {
33 validators::validate_area_exists(&self.pool, area_uuid).await?;
34 }
35
36 let start_date_ts = request.start_date.map(naive_date_to_things_timestamp);
38 let deadline_ts = request.deadline.map(naive_date_to_things_timestamp);
39
40 let now = Utc::now().timestamp() as f64;
42
43 sqlx::query(
45 r"
46 INSERT INTO TMTask (
47 uuid, title, type, status, notes,
48 startDate, deadline, project, area, heading,
49 creationDate, userModificationDate,
50 trashed
51 ) VALUES (?, ?, 1, 0, ?, ?, ?, NULL, ?, NULL, ?, ?, 0)
52 ",
53 )
54 .bind(id.as_str())
55 .bind(&request.title)
56 .bind(request.notes.as_ref())
57 .bind(start_date_ts)
58 .bind(deadline_ts)
59 .bind(request.area_uuid.map(|u| u.into_string()))
60 .bind(now)
61 .bind(now)
62 .execute(&self.pool)
63 .await
64 .map_err(|e| ThingsError::unknown(format!("Failed to create project: {e}")))?;
65
66 if let Some(tags) = request.tags {
68 self.set_task_tags(&id, tags).await?;
69 }
70
71 info!("Created project with UUID: {}", id);
72 Ok(id)
73 }
74
75 #[instrument(skip(self))]
84 pub async fn update_project(
85 &self,
86 request: crate::models::UpdateProjectRequest,
87 ) -> ThingsResult<()> {
88 validators::validate_project_exists(&self.pool, &request.uuid).await?;
90
91 if request.start_date.is_some() || request.deadline.is_some() {
93 if let Some(current_project) = self.get_project_by_uuid(&request.uuid).await? {
95 let final_start = request.start_date.or(current_project.start_date);
96 let final_deadline = request.deadline.or(current_project.deadline);
97 crate::database::validate_date_range(final_start, final_deadline)?;
98 }
99 }
100
101 if let Some(area_uuid) = &request.area_uuid {
103 validators::validate_area_exists(&self.pool, area_uuid).await?;
104 }
105
106 let mut builder = TaskUpdateBuilder::new();
108
109 if request.title.is_some() {
111 builder = builder.add_field("title");
112 }
113 if request.notes.is_some() {
114 builder = builder.add_field("notes");
115 }
116 if request.start_date.is_some() {
117 builder = builder.add_field("startDate");
118 }
119 if request.deadline.is_some() {
120 builder = builder.add_field("deadline");
121 }
122 if request.area_uuid.is_some() {
123 builder = builder.add_field("area");
124 }
125
126 let has_db_fields = !builder.is_empty();
128
129 if has_db_fields {
130 let query_str = builder.build_query_string();
132 let mut q = sqlx::query(&query_str);
133
134 if let Some(ref title) = request.title {
136 q = q.bind(title);
137 }
138 if let Some(ref notes) = request.notes {
139 q = q.bind(notes);
140 }
141 if let Some(start_date) = request.start_date {
142 q = q.bind(naive_date_to_things_timestamp(start_date));
143 }
144 if let Some(deadline) = request.deadline {
145 q = q.bind(naive_date_to_things_timestamp(deadline));
146 }
147 if let Some(area_uuid) = request.area_uuid {
148 q = q.bind(area_uuid.into_string());
149 }
150
151 let now = Utc::now().timestamp() as f64;
153 q = q.bind(now).bind(request.uuid.as_str());
154
155 q.execute(&self.pool)
156 .await
157 .map_err(|e| ThingsError::unknown(format!("Failed to update project: {e}")))?;
158 } else if request.tags.is_none() {
159 return Ok(());
161 }
162
163 if let Some(tags) = request.tags {
165 self.set_task_tags(&request.uuid, tags).await?;
166 }
167
168 info!("Updated project with UUID: {}", request.uuid);
169 Ok(())
170 }
171
172 #[instrument(skip(self))]
178 pub async fn complete_project(
179 &self,
180 id: &ThingsId,
181 child_handling: crate::models::ProjectChildHandling,
182 ) -> ThingsResult<()> {
183 validators::validate_project_exists(&self.pool, id).await?;
185
186 let now = Utc::now().timestamp() as f64;
187
188 match child_handling {
190 crate::models::ProjectChildHandling::Error => {
191 let child_count: i64 = sqlx::query_scalar(
193 "SELECT COUNT(*) FROM TMTask WHERE project = ? AND trashed = 0",
194 )
195 .bind(id.as_str())
196 .fetch_one(&self.pool)
197 .await
198 .map_err(|e| {
199 ThingsError::unknown(format!("Failed to check for child tasks: {e}"))
200 })?;
201
202 if child_count > 0 {
203 return Err(ThingsError::unknown(format!(
204 "Project {} has {} child task(s). Use cascade or orphan mode to complete.",
205 id, child_count
206 )));
207 }
208 }
209 crate::models::ProjectChildHandling::Cascade => {
210 sqlx::query(
212 "UPDATE TMTask SET status = 3, stopDate = ?, userModificationDate = ? WHERE project = ? AND trashed = 0",
213 )
214 .bind(now)
215 .bind(now)
216 .bind(id.as_str())
217 .execute(&self.pool)
218 .await
219 .map_err(|e| ThingsError::unknown(format!("Failed to complete child tasks: {e}")))?;
220 }
221 crate::models::ProjectChildHandling::Orphan => {
222 sqlx::query(
224 "UPDATE TMTask SET project = NULL, userModificationDate = ? WHERE project = ? AND trashed = 0",
225 )
226 .bind(now)
227 .bind(id.as_str())
228 .execute(&self.pool)
229 .await
230 .map_err(|e| ThingsError::unknown(format!("Failed to orphan child tasks: {e}")))?;
231 }
232 }
233
234 sqlx::query(
236 "UPDATE TMTask SET status = 3, stopDate = ?, userModificationDate = ? WHERE uuid = ?",
237 )
238 .bind(now)
239 .bind(now)
240 .bind(id.as_str())
241 .execute(&self.pool)
242 .await
243 .map_err(|e| ThingsError::unknown(format!("Failed to complete project: {e}")))?;
244
245 info!("Completed project with UUID: {}", id);
246 Ok(())
247 }
248
249 #[instrument(skip(self))]
255 pub async fn delete_project(
256 &self,
257 id: &ThingsId,
258 child_handling: crate::models::ProjectChildHandling,
259 ) -> ThingsResult<()> {
260 validators::validate_project_exists(&self.pool, id).await?;
262
263 let now = Utc::now().timestamp() as f64;
264
265 match child_handling {
267 crate::models::ProjectChildHandling::Error => {
268 let child_count: i64 = sqlx::query_scalar(
270 "SELECT COUNT(*) FROM TMTask WHERE project = ? AND trashed = 0",
271 )
272 .bind(id.as_str())
273 .fetch_one(&self.pool)
274 .await
275 .map_err(|e| {
276 ThingsError::unknown(format!("Failed to check for child tasks: {e}"))
277 })?;
278
279 if child_count > 0 {
280 return Err(ThingsError::unknown(format!(
281 "Project {} has {} child task(s). Use cascade or orphan mode to delete.",
282 id, child_count
283 )));
284 }
285 }
286 crate::models::ProjectChildHandling::Cascade => {
287 sqlx::query(
289 "UPDATE TMTask SET trashed = 1, userModificationDate = ? WHERE project = ? AND trashed = 0",
290 )
291 .bind(now)
292 .bind(id.as_str())
293 .execute(&self.pool)
294 .await
295 .map_err(|e| ThingsError::unknown(format!("Failed to delete child tasks: {e}")))?;
296 }
297 crate::models::ProjectChildHandling::Orphan => {
298 sqlx::query(
300 "UPDATE TMTask SET project = NULL, userModificationDate = ? WHERE project = ? AND trashed = 0",
301 )
302 .bind(now)
303 .bind(id.as_str())
304 .execute(&self.pool)
305 .await
306 .map_err(|e| ThingsError::unknown(format!("Failed to orphan child tasks: {e}")))?;
307 }
308 }
309
310 sqlx::query("UPDATE TMTask SET trashed = 1, userModificationDate = ? WHERE uuid = ?")
312 .bind(now)
313 .bind(id.as_str())
314 .execute(&self.pool)
315 .await
316 .map_err(|e| ThingsError::unknown(format!("Failed to delete project: {e}")))?;
317
318 info!("Deleted project with UUID: {}", id);
319 Ok(())
320 }
321}