module models import time // Project represents a project in the system pub struct Project { BaseModel pub mut: name string @[required] description string customer_id int // Links to Customer status ProjectStatus priority Priority start_date time.Time end_date time.Time actual_start_date time.Time actual_end_date time.Time budget f64 actual_cost f64 estimated_hours f32 actual_hours f32 progress f32 // 0.0 to 1.0 milestones []int // Milestone IDs sprints []int // Sprint IDs tasks []int // Task IDs issues []int // Issue IDs team_members []ProjectRole // Users and their roles in this project project_manager_id int // User ID of project manager client_contact_id int // Contact ID from customer billing_type ProjectBillingType hourly_rate f64 // Default hourly rate for this project currency string = 'USD' risk_level RiskLevel methodology ProjectMethodology repository_url string documentation_url string slack_channel string custom_fields map[string]string labels []int // Label IDs } // ProjectBillingType for different billing models pub enum ProjectBillingType { fixed_price time_and_materials retainer milestone_based } // RiskLevel for project risk assessment pub enum RiskLevel { low medium high critical } // ProjectMethodology for project management approach pub enum ProjectMethodology { agile scrum kanban waterfall hybrid } // get_duration returns the planned duration in days pub fn (p Project) get_duration() int { if p.start_date.unix == 0 || p.end_date.unix == 0 { return 0 } return int((p.end_date.unix - p.start_date.unix) / 86400) // 86400 seconds in a day } // get_actual_duration returns the actual duration in days pub fn (p Project) get_actual_duration() int { if p.actual_start_date.unix == 0 || p.actual_end_date.unix == 0 { return 0 } return int((p.actual_end_date.unix - p.actual_start_date.unix) / 86400) } // is_overdue checks if the project is past its end date pub fn (p Project) is_overdue() bool { if p.end_date.unix == 0 || p.status in [.completed, .cancelled] { return false } return time.now() > p.end_date } // is_over_budget checks if the project is over budget pub fn (p Project) is_over_budget() bool { return p.budget > 0 && p.actual_cost > p.budget } // get_budget_variance returns the budget variance (positive = under budget, negative = over budget) pub fn (p Project) get_budget_variance() f64 { return p.budget - p.actual_cost } // get_budget_variance_percentage returns the budget variance as a percentage pub fn (p Project) get_budget_variance_percentage() f64 { if p.budget == 0 { return 0 } return (p.get_budget_variance() / p.budget) * 100 } // get_schedule_variance returns schedule variance in days pub fn (p Project) get_schedule_variance() int { planned_duration := p.get_duration() if planned_duration == 0 { return 0 } if p.status == .completed { actual_duration := p.get_actual_duration() return planned_duration - actual_duration } // For ongoing projects, calculate based on current date if p.start_date.unix == 0 { return 0 } days_elapsed := int((time.now().unix - p.start_date.unix) / 86400) expected_progress := f32(days_elapsed) / f32(planned_duration) if expected_progress == 0 { return 0 } schedule_performance := p.progress / expected_progress return int(f32(planned_duration) * (schedule_performance - 1)) } // add_team_member adds a user to the project with a specific role pub fn (mut p Project) add_team_member(user_id int, role string, permissions []string) { // Check if user is already in the project for i, member in p.team_members { if member.user_id == user_id { // Update existing member p.team_members[i].role = role p.team_members[i].permissions = permissions return } } // Add new member p.team_members << ProjectRole{ user_id: user_id project_id: p.id role: role permissions: permissions assigned_at: time.now() } } // remove_team_member removes a user from the project pub fn (mut p Project) remove_team_member(user_id int) bool { for i, member in p.team_members { if member.user_id == user_id { p.team_members.delete(i) return true } } return false } // has_team_member checks if a user is a team member pub fn (p Project) has_team_member(user_id int) bool { for member in p.team_members { if member.user_id == user_id { return true } } return false } // get_team_member_role returns the role of a team member pub fn (p Project) get_team_member_role(user_id int) ?string { for member in p.team_members { if member.user_id == user_id { return member.role } } return none } // add_milestone adds a milestone to the project pub fn (mut p Project) add_milestone(milestone_id int) { if milestone_id !in p.milestones { p.milestones << milestone_id } } // remove_milestone removes a milestone from the project pub fn (mut p Project) remove_milestone(milestone_id int) { p.milestones = p.milestones.filter(it != milestone_id) } // add_sprint adds a sprint to the project pub fn (mut p Project) add_sprint(sprint_id int) { if sprint_id !in p.sprints { p.sprints << sprint_id } } // remove_sprint removes a sprint from the project pub fn (mut p Project) remove_sprint(sprint_id int) { p.sprints = p.sprints.filter(it != sprint_id) } // add_task adds a task to the project pub fn (mut p Project) add_task(task_id int) { if task_id !in p.tasks { p.tasks << task_id } } // remove_task removes a task from the project pub fn (mut p Project) remove_task(task_id int) { p.tasks = p.tasks.filter(it != task_id) } // add_issue adds an issue to the project pub fn (mut p Project) add_issue(issue_id int) { if issue_id !in p.issues { p.issues << issue_id } } // remove_issue removes an issue from the project pub fn (mut p Project) remove_issue(issue_id int) { p.issues = p.issues.filter(it != issue_id) } // start_project marks the project as started pub fn (mut p Project) start_project(by_user_id int) { p.status = .active p.actual_start_date = time.now() p.update_timestamp(by_user_id) } // complete_project marks the project as completed pub fn (mut p Project) complete_project(by_user_id int) { p.status = .completed p.actual_end_date = time.now() p.progress = 1.0 p.update_timestamp(by_user_id) } // cancel_project marks the project as cancelled pub fn (mut p Project) cancel_project(by_user_id int) { p.status = .cancelled p.update_timestamp(by_user_id) } // put_on_hold puts the project on hold pub fn (mut p Project) put_on_hold(by_user_id int) { p.status = .on_hold p.update_timestamp(by_user_id) } // update_progress updates the project progress pub fn (mut p Project) update_progress(progress f32, by_user_id int) { if progress < 0 { p.progress = 0 } else if progress > 1 { p.progress = 1 } else { p.progress = progress } p.update_timestamp(by_user_id) } // add_cost adds to the actual cost pub fn (mut p Project) add_cost(amount f64, by_user_id int) { p.actual_cost += amount p.update_timestamp(by_user_id) } // add_hours adds to the actual hours pub fn (mut p Project) add_hours(hours f32, by_user_id int) { p.actual_hours += hours p.update_timestamp(by_user_id) } // calculate_health returns a project health score based on various factors pub fn (p Project) calculate_health() f32 { mut score := f32(1.0) // Budget health (25% weight) if p.budget > 0 { budget_ratio := p.actual_cost / p.budget if budget_ratio > 1.2 { score -= 0.25 } else if budget_ratio > 1.0 { score -= 0.125 } } // Schedule health (25% weight) schedule_var := p.get_schedule_variance() if schedule_var < -7 { // More than a week behind score -= 0.25 } else if schedule_var < 0 { score -= 0.125 } // Progress health (25% weight) if p.progress < 0.5 && p.status == .active { days_elapsed := int((time.now().unix - p.start_date.unix) / 86400) total_days := p.get_duration() if total_days > 0 { expected_progress := f32(days_elapsed) / f32(total_days) if p.progress < expected_progress * 0.8 { score -= 0.25 } } } // Risk level (25% weight) match p.risk_level { .critical { score -= 0.25 } .high { score -= 0.125 } else {} } if score < 0 { score = 0 } return score } // get_health_status returns a human-readable health status pub fn (p Project) get_health_status() string { health := p.calculate_health() if health >= 0.8 { return 'Excellent' } else if health >= 0.6 { return 'Good' } else if health >= 0.4 { return 'At Risk' } else { return 'Critical' } }