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