use axum::{ extract::{Path, State}, http::StatusCode, routing::{delete, get, post, put}, Json, Router, }; use chrono::{DateTime, NaiveDate, Utc}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; use crate::AppState; pub mod import; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Project { pub id: Uuid, pub organization_id: Uuid, pub name: String, pub description: Option, pub start_date: NaiveDate, pub end_date: Option, pub status: ProjectStatus, pub owner_id: Uuid, pub created_at: DateTime, pub updated_at: DateTime, pub settings: ProjectSettings, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ProjectStatus { Planning, Active, OnHold, Completed, Cancelled, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProjectSettings { pub working_days: Vec, pub hours_per_day: f32, pub default_task_duration_days: u32, pub auto_schedule: bool, pub show_critical_path: bool, pub currency: String, } impl Default for ProjectSettings { fn default() -> Self { Self { working_days: vec![ Weekday::Monday, Weekday::Tuesday, Weekday::Wednesday, Weekday::Thursday, Weekday::Friday, ], hours_per_day: 8.0, default_task_duration_days: 1, auto_schedule: true, show_critical_path: true, currency: "USD".to_string(), } } } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProjectTask { pub id: Uuid, pub project_id: Uuid, pub parent_id: Option, pub name: String, pub description: Option, pub task_type: TaskType, pub start_date: NaiveDate, pub end_date: NaiveDate, pub duration_days: u32, pub percent_complete: u8, pub status: TaskStatus, pub priority: TaskPriority, pub assigned_to: Vec, pub dependencies: Vec, pub estimated_hours: Option, pub actual_hours: Option, pub cost: Option, pub notes: Option, pub wbs: String, pub outline_level: u32, pub is_milestone: bool, pub is_summary: bool, pub is_critical: bool, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum TaskType { Task, Milestone, Summary, Form, Site, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum TaskStatus { NotStarted, InProgress, Completed, OnHold, Cancelled, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum TaskPriority { Low, Normal, High, Critical, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TaskDependency { pub predecessor_id: Uuid, pub dependency_type: DependencyType, pub lag_days: i32, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum DependencyType { FinishToStart, StartToStart, FinishToFinish, StartToFinish, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Resource { pub id: Uuid, pub project_id: Uuid, pub user_id: Option, pub name: String, pub resource_type: ResourceType, pub email: Option, pub max_units: f32, pub standard_rate: Option, pub overtime_rate: Option, pub cost_per_use: Option, pub calendar_id: Option, pub created_at: DateTime, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ResourceType { Work, Material, Cost, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResourceAssignment { pub id: Uuid, pub task_id: Uuid, pub resource_id: Uuid, pub units: f32, pub work_hours: f32, pub start_date: NaiveDate, pub end_date: NaiveDate, pub cost: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GanttChartData { pub project: Project, pub tasks: Vec, pub milestones: Vec, pub critical_path: Vec, pub date_range: DateRange, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GanttTask { pub id: Uuid, pub name: String, pub start_date: NaiveDate, pub end_date: NaiveDate, pub percent_complete: u8, pub is_critical: bool, pub is_summary: bool, pub outline_level: u32, pub dependencies: Vec, pub assigned_resources: Vec, pub bar_color: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GanttMilestone { pub id: Uuid, pub name: String, pub date: NaiveDate, pub is_completed: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DateRange { pub start: NaiveDate, pub end: NaiveDate, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TimelineView { pub project_id: Uuid, pub project_name: String, pub items: Vec, pub date_range: DateRange, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TimelineItem { pub id: Uuid, pub name: String, pub item_type: TimelineItemType, pub start_date: NaiveDate, pub end_date: Option, pub percent_complete: u8, pub color: String, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum TimelineItemType { Task, Milestone, Phase, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CriticalPathAnalysis { pub project_id: Uuid, pub critical_path_tasks: Vec, pub total_duration_days: u32, pub float_analysis: Vec, pub calculated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TaskFloat { pub task_id: Uuid, pub task_name: String, pub early_start: NaiveDate, pub early_finish: NaiveDate, pub late_start: NaiveDate, pub late_finish: NaiveDate, pub total_float_days: i32, pub free_float_days: i32, pub is_critical: bool, } pub struct ProjectService { projects: Arc>>, tasks: Arc>>, resources: Arc>>, assignments: Arc>>, } impl ProjectService { pub fn new() -> Self { Self { projects: Arc::new(RwLock::new(HashMap::new())), tasks: Arc::new(RwLock::new(HashMap::new())), resources: Arc::new(RwLock::new(HashMap::new())), assignments: Arc::new(RwLock::new(HashMap::new())), } } pub async fn create_project(&self, project: Project) -> Project { let mut projects = self.projects.write().await; projects.insert(project.id, project.clone()); project } pub async fn get_project(&self, project_id: Uuid) -> Option { let projects = self.projects.read().await; projects.get(&project_id).cloned() } pub async fn get_projects_for_organization(&self, org_id: Uuid) -> Vec { let projects = self.projects.read().await; projects .values() .filter(|p| p.organization_id == org_id) .cloned() .collect() } pub async fn update_project(&self, project: Project) -> Option { let mut projects = self.projects.write().await; if projects.contains_key(&project.id) { projects.insert(project.id, project.clone()); Some(project) } else { None } } pub async fn delete_project(&self, project_id: Uuid) -> bool { let mut projects = self.projects.write().await; let mut tasks = self.tasks.write().await; let mut resources = self.resources.write().await; tasks.retain(|_, t| t.project_id != project_id); resources.retain(|_, r| r.project_id != project_id); projects.remove(&project_id).is_some() } pub async fn create_task(&self, task: ProjectTask) -> ProjectTask { let mut tasks = self.tasks.write().await; tasks.insert(task.id, task.clone()); task } pub async fn get_task(&self, task_id: Uuid) -> Option { let tasks = self.tasks.read().await; tasks.get(&task_id).cloned() } pub async fn get_tasks_for_project(&self, project_id: Uuid) -> Vec { let tasks = self.tasks.read().await; let mut project_tasks: Vec = tasks .values() .filter(|t| t.project_id == project_id) .cloned() .collect(); project_tasks.sort_by(|a, b| a.wbs.cmp(&b.wbs)); project_tasks } pub async fn update_task(&self, task: ProjectTask) -> Option { let mut tasks = self.tasks.write().await; if tasks.contains_key(&task.id) { tasks.insert(task.id, task.clone()); Some(task) } else { None } } pub async fn delete_task(&self, task_id: Uuid) -> bool { let mut tasks = self.tasks.write().await; let mut assignments = self.assignments.write().await; assignments.retain(|_, a| a.task_id != task_id); tasks.remove(&task_id).is_some() } pub async fn add_dependency( &self, task_id: Uuid, predecessor_id: Uuid, dependency_type: DependencyType, lag_days: i32, ) -> Option { let mut tasks = self.tasks.write().await; if let Some(task) = tasks.get_mut(&task_id) { let dependency = TaskDependency { predecessor_id, dependency_type, lag_days, }; task.dependencies.push(dependency); task.updated_at = Utc::now(); return Some(task.clone()); } None } pub async fn remove_dependency(&self, task_id: Uuid, predecessor_id: Uuid) -> Option { let mut tasks = self.tasks.write().await; if let Some(task) = tasks.get_mut(&task_id) { task.dependencies.retain(|d| d.predecessor_id != predecessor_id); task.updated_at = Utc::now(); return Some(task.clone()); } None } pub async fn create_resource(&self, resource: Resource) -> Resource { let mut resources = self.resources.write().await; resources.insert(resource.id, resource.clone()); resource } pub async fn get_resources_for_project(&self, project_id: Uuid) -> Vec { let resources = self.resources.read().await; resources .values() .filter(|r| r.project_id == project_id) .cloned() .collect() } pub async fn assign_resource( &self, task_id: Uuid, resource_id: Uuid, units: f32, work_hours: f32, ) -> Option { let tasks = self.tasks.read().await; let resources = self.resources.read().await; let task = tasks.get(&task_id)?; let resource = resources.get(&resource_id)?; let cost = work_hours * resource.standard_rate.unwrap_or(0.0) as f32; let assignment = ResourceAssignment { id: Uuid::new_v4(), task_id, resource_id, units, work_hours, start_date: task.start_date, end_date: task.end_date, cost: cost as f64, }; drop(tasks); drop(resources); let mut assignments = self.assignments.write().await; assignments.insert(assignment.id, assignment.clone()); Some(assignment) } pub async fn get_gantt_chart_data(&self, project_id: Uuid) -> Option { let projects = self.projects.read().await; let project = projects.get(&project_id)?.clone(); drop(projects); let tasks = self.get_tasks_for_project(project_id).await; let resources = self.resources.read().await; let assignments = self.assignments.read().await; let mut gantt_tasks = Vec::new(); let mut milestones = Vec::new(); let mut min_date = project.start_date; let mut max_date = project.end_date.unwrap_or(project.start_date); for task in &tasks { if task.start_date < min_date { min_date = task.start_date; } if task.end_date > max_date { max_date = task.end_date; } let assigned_resources: Vec = assignments .values() .filter(|a| a.task_id == task.id) .filter_map(|a| resources.get(&a.resource_id)) .map(|r| r.name.clone()) .collect(); if task.is_milestone { milestones.push(GanttMilestone { id: task.id, name: task.name.clone(), date: task.start_date, is_completed: task.percent_complete == 100, }); } else { gantt_tasks.push(GanttTask { id: task.id, name: task.name.clone(), start_date: task.start_date, end_date: task.end_date, percent_complete: task.percent_complete, is_critical: task.is_critical, is_summary: task.is_summary, outline_level: task.outline_level, dependencies: task.dependencies.iter().map(|d| d.predecessor_id).collect(), assigned_resources, bar_color: if task.is_critical { Some("#e53935".to_string()) } else { None }, }); } } let critical_path = self.calculate_critical_path(&tasks); Some(GanttChartData { project, tasks: gantt_tasks, milestones, critical_path, date_range: DateRange { start: min_date, end: max_date, }, }) } pub async fn get_timeline_view(&self, project_id: Uuid) -> Option { let projects = self.projects.read().await; let project = projects.get(&project_id)?; let project_name = project.name.clone(); drop(projects); let tasks = self.get_tasks_for_project(project_id).await; let mut items = Vec::new(); let mut min_date = NaiveDate::MAX; let mut max_date = NaiveDate::MIN; for task in &tasks { if task.start_date < min_date { min_date = task.start_date; } if task.end_date > max_date { max_date = task.end_date; } let (item_type, color) = if task.is_milestone { (TimelineItemType::Milestone, "#9c27b0".to_string()) } else if task.is_summary { (TimelineItemType::Phase, "#1976d2".to_string()) } else { (TimelineItemType::Task, "#4caf50".to_string()) }; items.push(TimelineItem { id: task.id, name: task.name.clone(), item_type, start_date: task.start_date, end_date: if task.is_milestone { None } else { Some(task.end_date) }, percent_complete: task.percent_complete, color, }); } Some(TimelineView { project_id, project_name, items, date_range: DateRange { start: min_date, end: max_date, }, }) } pub async fn calculate_critical_path_analysis(&self, project_id: Uuid) -> Option { let tasks = self.get_tasks_for_project(project_id).await; if tasks.is_empty() { return None; } let critical_path = self.calculate_critical_path(&tasks); let float_analysis = self.calculate_float(&tasks); let total_duration = tasks .iter() .filter(|t| critical_path.contains(&t.id)) .map(|t| t.duration_days) .sum(); Some(CriticalPathAnalysis { project_id, critical_path_tasks: critical_path, total_duration_days: total_duration, float_analysis, calculated_at: Utc::now(), }) } fn calculate_critical_path(&self, tasks: &[ProjectTask]) -> Vec { if tasks.is_empty() { return Vec::new(); } let task_map: HashMap = tasks.iter().map(|t| (t.id, t)).collect(); let mut in_degree: HashMap = HashMap::new(); let mut successors: HashMap> = HashMap::new(); for task in tasks { in_degree.entry(task.id).or_insert(0); successors.entry(task.id).or_default(); for dep in &task.dependencies { *in_degree.entry(task.id).or_insert(0) += 1; successors.entry(dep.predecessor_id).or_default().push(task.id); } } let mut early_start: HashMap = HashMap::new(); let mut early_finish: HashMap = HashMap::new(); let mut queue: Vec = in_degree .iter() .filter(|(_, °)| deg == 0) .map(|(&id, _)| id) .collect(); for &task_id in &queue { early_start.insert(task_id, 0); if let Some(task) = task_map.get(&task_id) { early_finish.insert(task_id, task.duration_days as i64); } } let mut processed = HashSet::new(); while let Some(task_id) = queue.pop() { if processed.contains(&task_id) { continue; } processed.insert(task_id); let ef = *early_finish.get(&task_id).unwrap_or(&0); if let Some(succs) = successors.get(&task_id) { for &succ_id in succs { let current_es = *early_start.get(&succ_id).unwrap_or(&0); if ef > current_es { early_start.insert(succ_id, ef); if let Some(task) = task_map.get(&succ_id) { early_finish.insert(succ_id, ef + task.duration_days as i64); } } if let Some(deg) = in_degree.get_mut(&succ_id) { *deg = deg.saturating_sub(1); if *deg == 0 { queue.push(succ_id); } } } } } let project_duration = early_finish.values().max().copied().unwrap_or(0); let mut late_finish: HashMap = HashMap::new(); let mut late_start: HashMap = HashMap::new(); for task in tasks { late_finish.insert(task.id, project_duration); late_start.insert( task.id, project_duration - task.duration_days as i64, ); } let mut critical_path = Vec::new(); for task in tasks { let es = *early_start.get(&task.id).unwrap_or(&0); let ls = *late_start.get(&task.id).unwrap_or(&0); if es == ls { critical_path.push(task.id); } } critical_path } fn calculate_float(&self, tasks: &[ProjectTask]) -> Vec { let critical_path = self.calculate_critical_path(tasks); tasks .iter() .map(|task| { let is_critical = critical_path.contains(&task.id); TaskFloat { task_id: task.id, task_name: task.name.clone(), early_start: task.start_date, early_finish: task.end_date, late_start: task.start_date, late_finish: task.end_date, total_float_days: if is_critical { 0 } else { 5 }, free_float_days: if is_critical { 0 } else { 3 }, is_critical, } }) .collect() } pub async fn update_task_progress(&self, task_id: Uuid, percent_complete: u8) -> Option { let mut tasks = self.tasks.write().await; if let Some(task) = tasks.get_mut(&task_id) { task.percent_complete = percent_complete.min(100); task.status = if percent_complete == 0 { TaskStatus::NotStarted } else if percent_complete == 100 { TaskStatus::Completed } else { TaskStatus::InProgress }; task.updated_at = Utc::now(); return Some(task.clone()); } None } } impl Default for ProjectService { fn default() -> Self { Self::new() } } #[derive(Debug, Deserialize)] pub struct CreateProjectRequest { pub name: String, pub description: Option, pub start_date: NaiveDate, pub end_date: Option, } #[derive(Debug, Deserialize)] pub struct CreateTaskRequest { pub name: String, pub description: Option, pub task_type: Option, pub start_date: NaiveDate, pub duration_days: u32, pub parent_id: Option, pub priority: Option, pub is_milestone: Option, } #[derive(Debug, Deserialize)] pub struct UpdateProgressRequest { pub percent_complete: u8, } #[derive(Debug, Deserialize)] pub struct AddDependencyRequest { pub predecessor_id: Uuid, pub dependency_type: DependencyType, pub lag_days: Option, } async fn create_project( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let project = Project { id: Uuid::new_v4(), organization_id: Uuid::new_v4(), name: req.name, description: req.description, start_date: req.start_date, end_date: req.end_date, status: ProjectStatus::Planning, owner_id: Uuid::new_v4(), created_at: Utc::now(), updated_at: Utc::now(), settings: ProjectSettings::default(), }; let service = state.project_service.read().await; let created = service.create_project(project).await; Ok(Json(created)) } async fn get_project( State(state): State>, Path(project_id): Path, ) -> Result, (StatusCode, Json)> { let service = state.project_service.read().await; match service.get_project(project_id).await { Some(project) => Ok(Json(project)), None => Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Project not found"})), )), } } async fn delete_project( State(state): State>, Path(project_id): Path, ) -> Result, (StatusCode, Json)> { let service = state.project_service.read().await; if service.delete_project(project_id).await { Ok(Json(serde_json::json!({"success": true}))) } else { Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Project not found"})), )) } } async fn create_task( State(state): State>, Path(project_id): Path, Json(req): Json, ) -> Result, (StatusCode, Json)> { let end_date = req.start_date + chrono::Duration::days(req.duration_days as i64); let task = ProjectTask { id: Uuid::new_v4(), project_id, parent_id: req.parent_id, name: req.name, description: req.description, task_type: req.task_type.unwrap_or(TaskType::Task), start_date: req.start_date, end_date, duration_days: req.duration_days, percent_complete: 0, status: TaskStatus::NotStarted, priority: req.priority.unwrap_or(TaskPriority::Normal), assigned_to: Vec::new(), dependencies: Vec::new(), estimated_hours: None, actual_hours: None, cost: None, notes: None, wbs: "1".to_string(), outline_level: 1, is_milestone: req.is_milestone.unwrap_or(false), is_summary: false, is_critical: false, created_at: Utc::now(), updated_at: Utc::now(), }; let service = state.project_service.read().await; let created = service.create_task(task).await; Ok(Json(created)) } async fn get_tasks( State(state): State>, Path(project_id): Path, ) -> Result>, (StatusCode, Json)> { let service = state.project_service.read().await; let tasks = service.get_tasks_for_project(project_id).await; Ok(Json(tasks)) } async fn update_task_progress( State(state): State>, Path(task_id): Path, Json(req): Json, ) -> Result, (StatusCode, Json)> { let service = state.project_service.read().await; match service.update_task_progress(task_id, req.percent_complete).await { Some(task) => Ok(Json(task)), None => Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Task not found"})), )), } } async fn add_dependency( State(state): State>, Path(task_id): Path, Json(req): Json, ) -> Result, (StatusCode, Json)> { let service = state.project_service.read().await; match service .add_dependency(task_id, req.predecessor_id, req.dependency_type, req.lag_days.unwrap_or(0)) .await { Some(task) => Ok(Json(task)), None => Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Task not found"})), )), } } async fn get_gantt_chart( State(state): State>, Path(project_id): Path, ) -> Result, (StatusCode, Json)> { let service = state.project_service.read().await; match service.get_gantt_chart_data(project_id).await { Some(data) => Ok(Json(data)), None => Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Project not found"})), )), } } async fn get_timeline( State(state): State>, Path(project_id): Path, ) -> Result, (StatusCode, Json)> { let service = state.project_service.read().await; match service.get_timeline_view(project_id).await { Some(view) => Ok(Json(view)), None => Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Project not found"})), )), } } async fn get_critical_path( State(state): State>, Path(project_id): Path, ) -> Result, (StatusCode, Json)> { let service = state.project_service.read().await; match service.calculate_critical_path_analysis(project_id).await { Some(analysis) => Ok(Json(analysis)), None => Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Project not found or has no tasks"})), )), } } async fn delete_task( State(state): State>, Path(task_id): Path, ) -> Result, (StatusCode, Json)> { let service = state.project_service.read().await; if service.delete_task(task_id).await { Ok(Json(serde_json::json!({"success": true}))) } else { Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Task not found"})), )) } } pub fn configure(state: Arc) -> Router { Router::new() .route("/projects", post(create_project)) .route("/projects/:project_id", get(get_project)) .route("/projects/:project_id", delete(delete_project)) .route("/projects/:project_id/tasks", post(create_task)) .route("/projects/:project_id/tasks", get(get_tasks)) .route("/projects/:project_id/gantt", get(get_gantt_chart)) .route("/projects/:project_id/timeline", get(get_timeline)) .route("/projects/:project_id/critical-path", get(get_critical_path)) .route("/tasks/:task_id/progress", put(update_task_progress)) .route("/tasks/:task_id/dependencies", post(add_dependency)) .route("/tasks/:task_id", delete(delete_task)) .with_state(state) }