1use axum::{
2 extract::{Path, Query, State},
3 http::StatusCode,
4 response::{IntoResponse, Json},
5};
6use serde_json::json;
7
8use super::models::*;
9use super::server::AppState;
10use crate::{
11 events::EventManager, search::SearchManager, tasks::TaskManager, workspace::WorkspaceManager,
12};
13
14pub async fn list_tasks(
16 State(state): State<AppState>,
17 Query(query): Query<TaskListQuery>,
18) -> impl IntoResponse {
19 let task_mgr = TaskManager::new(&state.db_pool);
20
21 let parent_filter = query.parent.as_deref().map(|p| {
23 if p == "null" {
24 None
25 } else {
26 p.parse::<i64>().ok()
27 }
28 });
29
30 match task_mgr
31 .find_tasks(query.status.as_deref(), parent_filter)
32 .await
33 {
34 Ok(tasks) => (StatusCode::OK, Json(ApiResponse { data: tasks })).into_response(),
35 Err(e) => (
36 StatusCode::INTERNAL_SERVER_ERROR,
37 Json(ApiError {
38 code: "DATABASE_ERROR".to_string(),
39 message: format!("Failed to list tasks: {}", e),
40 details: None,
41 }),
42 )
43 .into_response(),
44 }
45}
46
47pub async fn get_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
49 let task_mgr = TaskManager::new(&state.db_pool);
50
51 match task_mgr.get_task(id).await {
52 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
53 Err(e) if e.to_string().contains("not found") => (
54 StatusCode::NOT_FOUND,
55 Json(ApiError {
56 code: "TASK_NOT_FOUND".to_string(),
57 message: format!("Task {} not found", id),
58 details: None,
59 }),
60 )
61 .into_response(),
62 Err(e) => (
63 StatusCode::INTERNAL_SERVER_ERROR,
64 Json(ApiError {
65 code: "DATABASE_ERROR".to_string(),
66 message: format!("Failed to get task: {}", e),
67 details: None,
68 }),
69 )
70 .into_response(),
71 }
72}
73
74pub async fn create_task(
76 State(state): State<AppState>,
77 Json(req): Json<CreateTaskRequest>,
78) -> impl IntoResponse {
79 let task_mgr = TaskManager::new(&state.db_pool);
80
81 let result = task_mgr
83 .add_task(&req.name, req.spec.as_deref(), req.parent_id)
84 .await;
85
86 match result {
87 Ok(mut task) => {
88 if let Some(priority) = req.priority {
90 if let Ok(updated_task) = task_mgr
91 .update_task(task.id, None, None, None, None, None, Some(priority))
92 .await
93 {
94 task = updated_task;
95 }
96 }
98 (StatusCode::CREATED, Json(ApiResponse { data: task })).into_response()
99 },
100 Err(e) => (
101 StatusCode::BAD_REQUEST,
102 Json(ApiError {
103 code: "INVALID_REQUEST".to_string(),
104 message: format!("Failed to create task: {}", e),
105 details: None,
106 }),
107 )
108 .into_response(),
109 }
110}
111
112pub async fn update_task(
114 State(state): State<AppState>,
115 Path(id): Path<i64>,
116 Json(req): Json<UpdateTaskRequest>,
117) -> impl IntoResponse {
118 let task_mgr = TaskManager::new(&state.db_pool);
119
120 match task_mgr.get_task(id).await {
122 Err(e) if e.to_string().contains("not found") => {
123 return (
124 StatusCode::NOT_FOUND,
125 Json(ApiError {
126 code: "TASK_NOT_FOUND".to_string(),
127 message: format!("Task {} not found", id),
128 details: None,
129 }),
130 )
131 .into_response()
132 },
133 Err(e) => {
134 return (
135 StatusCode::INTERNAL_SERVER_ERROR,
136 Json(ApiError {
137 code: "DATABASE_ERROR".to_string(),
138 message: format!("Database error: {}", e),
139 details: None,
140 }),
141 )
142 .into_response()
143 },
144 Ok(_) => {},
145 }
146
147 match task_mgr
150 .update_task(
151 id,
152 req.name.as_deref(),
153 req.spec.as_deref(),
154 None, req.status.as_deref(),
156 None, req.priority,
158 )
159 .await
160 {
161 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
162 Err(e) => (
163 StatusCode::BAD_REQUEST,
164 Json(ApiError {
165 code: "INVALID_REQUEST".to_string(),
166 message: format!("Failed to update task: {}", e),
167 details: None,
168 }),
169 )
170 .into_response(),
171 }
172}
173
174pub async fn delete_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
176 let task_mgr = TaskManager::new(&state.db_pool);
177
178 match task_mgr.delete_task(id).await {
179 Ok(_) => (StatusCode::NO_CONTENT).into_response(),
180 Err(e) if e.to_string().contains("not found") => (
181 StatusCode::NOT_FOUND,
182 Json(ApiError {
183 code: "TASK_NOT_FOUND".to_string(),
184 message: format!("Task {} not found", id),
185 details: None,
186 }),
187 )
188 .into_response(),
189 Err(e) => (
190 StatusCode::BAD_REQUEST,
191 Json(ApiError {
192 code: "INVALID_REQUEST".to_string(),
193 message: format!("Failed to delete task: {}", e),
194 details: None,
195 }),
196 )
197 .into_response(),
198 }
199}
200
201pub async fn start_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
203 let task_mgr = TaskManager::new(&state.db_pool);
204
205 match task_mgr.start_task(id, false).await {
206 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
207 Err(e) if e.to_string().contains("not found") => (
208 StatusCode::NOT_FOUND,
209 Json(ApiError {
210 code: "TASK_NOT_FOUND".to_string(),
211 message: format!("Task {} not found", id),
212 details: None,
213 }),
214 )
215 .into_response(),
216 Err(e) => (
217 StatusCode::BAD_REQUEST,
218 Json(ApiError {
219 code: "INVALID_REQUEST".to_string(),
220 message: format!("Failed to start task: {}", e),
221 details: None,
222 }),
223 )
224 .into_response(),
225 }
226}
227
228pub async fn done_task(State(state): State<AppState>) -> impl IntoResponse {
230 let task_mgr = TaskManager::new(&state.db_pool);
231
232 match task_mgr.done_task().await {
233 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
234 Err(e) if e.to_string().contains("No current task") => (
235 StatusCode::BAD_REQUEST,
236 Json(ApiError {
237 code: "NO_CURRENT_TASK".to_string(),
238 message: "No current task to complete".to_string(),
239 details: None,
240 }),
241 )
242 .into_response(),
243 Err(e) => (
244 StatusCode::BAD_REQUEST,
245 Json(ApiError {
246 code: "INVALID_REQUEST".to_string(),
247 message: format!("Failed to complete task: {}", e),
248 details: None,
249 }),
250 )
251 .into_response(),
252 }
253}
254
255pub async fn switch_task(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
257 let task_mgr = TaskManager::new(&state.db_pool);
258
259 match task_mgr.switch_to_task(id).await {
260 Ok(task) => (StatusCode::OK, Json(ApiResponse { data: task })).into_response(),
261 Err(e) if e.to_string().contains("not found") => (
262 StatusCode::NOT_FOUND,
263 Json(ApiError {
264 code: "TASK_NOT_FOUND".to_string(),
265 message: format!("Task {} not found", id),
266 details: None,
267 }),
268 )
269 .into_response(),
270 Err(e) => (
271 StatusCode::BAD_REQUEST,
272 Json(ApiError {
273 code: "INVALID_REQUEST".to_string(),
274 message: format!("Failed to switch task: {}", e),
275 details: None,
276 }),
277 )
278 .into_response(),
279 }
280}
281
282pub async fn spawn_subtask(
285 State(state): State<AppState>,
286 Path(_parent_id): Path<i64>, Json(req): Json<SpawnSubtaskRequest>,
288) -> impl IntoResponse {
289 let task_mgr = TaskManager::new(&state.db_pool);
290
291 match task_mgr.spawn_subtask(&req.name, req.spec.as_deref()).await {
293 Ok(response) => (StatusCode::CREATED, Json(ApiResponse { data: response })).into_response(),
294 Err(e) if e.to_string().contains("No current task") => (
295 StatusCode::BAD_REQUEST,
296 Json(ApiError {
297 code: "NO_CURRENT_TASK".to_string(),
298 message: "No current task to spawn subtask from".to_string(),
299 details: None,
300 }),
301 )
302 .into_response(),
303 Err(e) => (
304 StatusCode::BAD_REQUEST,
305 Json(ApiError {
306 code: "INVALID_REQUEST".to_string(),
307 message: format!("Failed to spawn subtask: {}", e),
308 details: None,
309 }),
310 )
311 .into_response(),
312 }
313}
314
315pub async fn get_current_task(State(state): State<AppState>) -> impl IntoResponse {
317 let workspace_mgr = WorkspaceManager::new(&state.db_pool);
318
319 match workspace_mgr.get_current_task().await {
320 Ok(response) => {
321 if response.task.is_some() {
322 (StatusCode::OK, Json(ApiResponse { data: response })).into_response()
323 } else {
324 (
325 StatusCode::OK,
326 Json(json!({
327 "data": null,
328 "message": "No current task"
329 })),
330 )
331 .into_response()
332 }
333 },
334 Err(e) => (
335 StatusCode::INTERNAL_SERVER_ERROR,
336 Json(ApiError {
337 code: "DATABASE_ERROR".to_string(),
338 message: format!("Failed to get current task: {}", e),
339 details: None,
340 }),
341 )
342 .into_response(),
343 }
344}
345
346pub async fn pick_next_task(State(state): State<AppState>) -> impl IntoResponse {
348 let task_mgr = TaskManager::new(&state.db_pool);
349
350 match task_mgr.pick_next().await {
351 Ok(response) => (StatusCode::OK, Json(ApiResponse { data: response })).into_response(),
352 Err(e) => (
353 StatusCode::INTERNAL_SERVER_ERROR,
354 Json(ApiError {
355 code: "DATABASE_ERROR".to_string(),
356 message: format!("Failed to pick next task: {}", e),
357 details: None,
358 }),
359 )
360 .into_response(),
361 }
362}
363
364pub async fn list_events(
366 State(state): State<AppState>,
367 Path(task_id): Path<i64>,
368 Query(query): Query<EventListQuery>,
369) -> impl IntoResponse {
370 let event_mgr = EventManager::new(&state.db_pool);
371
372 match event_mgr
374 .list_events(
375 Some(task_id),
376 query.limit.map(|l| l as i64),
377 query.event_type,
378 query.since,
379 )
380 .await
381 {
382 Ok(events) => (StatusCode::OK, Json(ApiResponse { data: events })).into_response(),
383 Err(e) => (
384 StatusCode::INTERNAL_SERVER_ERROR,
385 Json(ApiError {
386 code: "DATABASE_ERROR".to_string(),
387 message: format!("Failed to list events: {}", e),
388 details: None,
389 }),
390 )
391 .into_response(),
392 }
393}
394
395pub async fn create_event(
397 State(state): State<AppState>,
398 Path(task_id): Path<i64>,
399 Json(req): Json<CreateEventRequest>,
400) -> impl IntoResponse {
401 let event_mgr = EventManager::new(&state.db_pool);
402
403 if !["decision", "blocker", "milestone", "note"].contains(&req.event_type.as_str()) {
405 return (
406 StatusCode::BAD_REQUEST,
407 Json(ApiError {
408 code: "INVALID_REQUEST".to_string(),
409 message: format!("Invalid event type: {}", req.event_type),
410 details: None,
411 }),
412 )
413 .into_response();
414 }
415
416 match event_mgr
418 .add_event(task_id, &req.event_type, &req.data)
419 .await
420 {
421 Ok(event) => (StatusCode::CREATED, Json(ApiResponse { data: event })).into_response(),
422 Err(e) => (
423 StatusCode::BAD_REQUEST,
424 Json(ApiError {
425 code: "INVALID_REQUEST".to_string(),
426 message: format!("Failed to create event: {}", e),
427 details: None,
428 }),
429 )
430 .into_response(),
431 }
432}
433
434pub async fn search(
436 State(state): State<AppState>,
437 Query(query): Query<SearchQuery>,
438) -> impl IntoResponse {
439 let search_mgr = SearchManager::new(&state.db_pool);
440
441 match search_mgr
442 .unified_search(
443 &query.query,
444 query.include_tasks,
445 query.include_events,
446 query.limit.map(|l| l as i64),
447 )
448 .await
449 {
450 Ok(results) => (StatusCode::OK, Json(ApiResponse { data: results })).into_response(),
451 Err(e) => (
452 StatusCode::INTERNAL_SERVER_ERROR,
453 Json(ApiError {
454 code: "DATABASE_ERROR".to_string(),
455 message: format!("Search failed: {}", e),
456 details: None,
457 }),
458 )
459 .into_response(),
460 }
461}