intent_engine/dashboard/
handlers.rs

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
14/// Get all tasks with optional filters
15pub 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    // Convert parent filter to Option<Option<i64>>
22    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
47/// Get a single task by ID
48pub 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
74/// Create a new task
75pub 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    // Note: add_task doesn't support priority - it's set separately via update_task
82    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 priority was requested, update it
89            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                // Ignore priority update errors
97            }
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
112/// Update a task
113pub 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    // First check if task exists
121    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    // Update task fields
148    // Signature: update_task(id, name, spec, parent_id, status, complexity, priority)
149    match task_mgr
150        .update_task(
151            id,
152            req.name.as_deref(),
153            req.spec.as_deref(),
154            None, // parent_id - not supported via update API
155            req.status.as_deref(),
156            None, // complexity - not exposed in API
157            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
174/// Delete a task
175pub 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
201/// Start a task (set as current)
202pub 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
228/// Complete the current task
229pub 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
255/// Switch to a different task
256pub 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
282/// Spawn a subtask and switch to it
283/// Note: This creates a subtask of the CURRENT task, not an arbitrary parent
284pub async fn spawn_subtask(
285    State(state): State<AppState>,
286    Path(_parent_id): Path<i64>, // Ignored - uses current task
287    Json(req): Json<SpawnSubtaskRequest>,
288) -> impl IntoResponse {
289    let task_mgr = TaskManager::new(&state.db_pool);
290
291    // spawn_subtask uses the current task as parent automatically
292    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
315/// Get current task
316pub 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
346/// Pick next task recommendation
347pub 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
364/// List events for a task
365pub 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    // Signature: list_events(task_id, limit, log_type, since)
373    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
395/// Add an event to a task
396pub 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    // Validate event type
404    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    // add_event signature: (task_id, log_type, discussion_data)
417    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
434/// Unified search across tasks and events
435pub 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}