Files
herolib/lib/biz/planner/models/sprint.v
2025-07-19 15:54:23 +02:00

432 lines
11 KiB
V

module models
import time
// Sprint represents a Scrum sprint
pub struct Sprint {
BaseModel
pub mut:
name string @[required]
description string
project_id int // Links to Project
sprint_number int // Sequential number within project
status SprintStatus
start_date time.Time
end_date time.Time
goal string // Sprint goal
capacity f32 // Team capacity in hours
commitment int // Story points committed
completed int // Story points completed
velocity f32 // Actual velocity (story points / sprint duration)
tasks []int // Task IDs in this sprint
team_members []SprintMember // Team members and their capacity
retrospective SprintRetrospective
review_notes string
demo_url string
burndown_data []BurndownPoint
daily_standups []DailyStandup
impediments []Impediment
custom_fields map[string]string
}
// SprintStatus for sprint lifecycle
pub enum SprintStatus {
planning
active
completed
cancelled
}
// SprintMember represents a team member's participation in a sprint
pub struct SprintMember {
pub mut:
user_id int
sprint_id int
capacity_hours f32 // Available hours for this sprint
allocated_hours f32 // Hours allocated to tasks
actual_hours f32 // Hours actually worked
availability f32 // Percentage availability (0.0 to 1.0)
role string
joined_at time.Time
}
// SprintRetrospective for sprint retrospective data
pub struct SprintRetrospective {
pub mut:
conducted_at time.Time
facilitator_id int
participants []int // User IDs
what_went_well []string
what_went_wrong []string
action_items []ActionItem
team_mood f32 // 1.0 to 5.0 scale
notes string
}
// ActionItem for retrospective action items
pub struct ActionItem {
pub mut:
description string
assignee_id int
due_date time.Time
status ActionItemStatus
created_at time.Time
}
// ActionItemStatus for action item tracking
pub enum ActionItemStatus {
open
in_progress
completed
cancelled
}
// BurndownPoint for burndown chart data
pub struct BurndownPoint {
pub mut:
date time.Time
remaining_points int
remaining_hours f32
completed_points int
added_points int // Points added during sprint
removed_points int // Points removed during sprint
}
// DailyStandup for daily standup meeting data
pub struct DailyStandup {
pub mut:
date time.Time
facilitator_id int
participants []int // User IDs
updates []StandupUpdate
impediments []int // Impediment IDs discussed
duration_minutes int
notes string
}
// StandupUpdate for individual team member updates
pub struct StandupUpdate {
pub mut:
user_id int
yesterday string // What did you do yesterday?
today string // What will you do today?
blockers string // Any blockers or impediments?
mood f32 // 1.0 to 5.0 scale
}
// Impediment for tracking sprint impediments
pub struct Impediment {
pub mut:
id int
sprint_id int
title string
description string
reported_by int
assigned_to int
status ImpedimentStatus
severity Priority
reported_at time.Time
resolved_at time.Time
resolution string
}
// ImpedimentStatus for impediment tracking
pub enum ImpedimentStatus {
open
in_progress
resolved
cancelled
}
// get_duration returns the sprint duration in days
pub fn (s Sprint) get_duration() int {
if s.start_date.unix == 0 || s.end_date.unix == 0 {
return 0
}
return int((s.end_date.unix - s.start_date.unix) / 86400)
}
// get_days_remaining returns the number of days remaining in the sprint
pub fn (s Sprint) get_days_remaining() int {
if s.end_date.unix == 0 || s.status != .active {
return 0
}
now := time.now()
if now > s.end_date {
return 0
}
return int((s.end_date.unix - now.unix) / 86400)
}
// get_days_elapsed returns the number of days elapsed in the sprint
pub fn (s Sprint) get_days_elapsed() int {
if s.start_date.unix == 0 {
return 0
}
now := time.now()
if now < s.start_date {
return 0
}
end_time := if s.status == .completed && s.end_date.unix > 0 { s.end_date } else { now }
return int((end_time.unix - s.start_date.unix) / 86400)
}
// is_active checks if the sprint is currently active
pub fn (s Sprint) is_active() bool {
return s.status == .active
}
// is_overdue checks if the sprint has passed its end date
pub fn (s Sprint) is_overdue() bool {
return s.status == .active && time.now() > s.end_date
}
// get_completion_percentage returns the completion percentage based on story points
pub fn (s Sprint) get_completion_percentage() f32 {
if s.commitment == 0 {
return 0
}
return f32(s.completed) / f32(s.commitment) * 100
}
// get_velocity calculates the actual velocity for the sprint
pub fn (s Sprint) get_velocity() f32 {
duration := s.get_duration()
if duration == 0 {
return 0
}
return f32(s.completed) / f32(duration)
}
// get_team_capacity returns the total team capacity in hours
pub fn (s Sprint) get_team_capacity() f32 {
mut total := f32(0)
for member in s.team_members {
total += member.capacity_hours
}
return total
}
// get_team_utilization returns the team utilization percentage
pub fn (s Sprint) get_team_utilization() f32 {
capacity := s.get_team_capacity()
if capacity == 0 {
return 0
}
mut actual := f32(0)
for member in s.team_members {
actual += member.actual_hours
}
return (actual / capacity) * 100
}
// add_task adds a task to the sprint
pub fn (mut s Sprint) add_task(task_id int, by_user_id int) {
if task_id !in s.tasks {
s.tasks << task_id
s.update_timestamp(by_user_id)
}
}
// remove_task removes a task from the sprint
pub fn (mut s Sprint) remove_task(task_id int, by_user_id int) {
s.tasks = s.tasks.filter(it != task_id)
s.update_timestamp(by_user_id)
}
// add_team_member adds a team member to the sprint
pub fn (mut s Sprint) add_team_member(user_id int, capacity_hours f32, availability f32, role string, by_user_id int) {
// Check if member already exists
for i, member in s.team_members {
if member.user_id == user_id {
// Update existing member
s.team_members[i].capacity_hours = capacity_hours
s.team_members[i].availability = availability
s.team_members[i].role = role
s.update_timestamp(by_user_id)
return
}
}
// Add new member
s.team_members << SprintMember{
user_id: user_id
sprint_id: s.id
capacity_hours: capacity_hours
availability: availability
role: role
joined_at: time.now()
}
s.update_timestamp(by_user_id)
}
// remove_team_member removes a team member from the sprint
pub fn (mut s Sprint) remove_team_member(user_id int, by_user_id int) {
for i, member in s.team_members {
if member.user_id == user_id {
s.team_members.delete(i)
s.update_timestamp(by_user_id)
return
}
}
}
// start_sprint starts the sprint
pub fn (mut s Sprint) start_sprint(by_user_id int) {
s.status = .active
if s.start_date.unix == 0 {
s.start_date = time.now()
}
s.update_timestamp(by_user_id)
}
// complete_sprint completes the sprint
pub fn (mut s Sprint) complete_sprint(by_user_id int) {
s.status = .completed
s.velocity = s.get_velocity()
s.update_timestamp(by_user_id)
}
// cancel_sprint cancels the sprint
pub fn (mut s Sprint) cancel_sprint(by_user_id int) {
s.status = .cancelled
s.update_timestamp(by_user_id)
}
// update_commitment updates the story points commitment
pub fn (mut s Sprint) update_commitment(points int, by_user_id int) {
s.commitment = points
s.update_timestamp(by_user_id)
}
// update_completed updates the completed story points
pub fn (mut s Sprint) update_completed(points int, by_user_id int) {
s.completed = points
s.update_timestamp(by_user_id)
}
// add_burndown_point adds a burndown chart data point
pub fn (mut s Sprint) add_burndown_point(remaining_points int, remaining_hours f32, completed_points int, by_user_id int) {
s.burndown_data << BurndownPoint{
date: time.now()
remaining_points: remaining_points
remaining_hours: remaining_hours
completed_points: completed_points
}
s.update_timestamp(by_user_id)
}
// add_daily_standup adds a daily standup record
pub fn (mut s Sprint) add_daily_standup(facilitator_id int, participants []int, updates []StandupUpdate, duration_minutes int, notes string, by_user_id int) {
s.daily_standups << DailyStandup{
date: time.now()
facilitator_id: facilitator_id
participants: participants
updates: updates
duration_minutes: duration_minutes
notes: notes
}
s.update_timestamp(by_user_id)
}
// add_impediment adds an impediment to the sprint
pub fn (mut s Sprint) add_impediment(title string, description string, reported_by int, severity Priority, by_user_id int) {
s.impediments << Impediment{
id: s.impediments.len + 1
sprint_id: s.id
title: title
description: description
reported_by: reported_by
status: .open
severity: severity
reported_at: time.now()
}
s.update_timestamp(by_user_id)
}
// resolve_impediment resolves an impediment
pub fn (mut s Sprint) resolve_impediment(impediment_id int, resolution string, by_user_id int) {
for i, mut impediment in s.impediments {
if impediment.id == impediment_id {
s.impediments[i].status = .resolved
s.impediments[i].resolved_at = time.now()
s.impediments[i].resolution = resolution
s.update_timestamp(by_user_id)
return
}
}
}
// conduct_retrospective conducts a sprint retrospective
pub fn (mut s Sprint) conduct_retrospective(facilitator_id int, participants []int, went_well []string, went_wrong []string, action_items []ActionItem, team_mood f32, notes string, by_user_id int) {
s.retrospective = SprintRetrospective{
conducted_at: time.now()
facilitator_id: facilitator_id
participants: participants
what_went_well: went_well
what_went_wrong: went_wrong
action_items: action_items
team_mood: team_mood
notes: notes
}
s.update_timestamp(by_user_id)
}
// get_health_score calculates a health score for the sprint
pub fn (s Sprint) get_health_score() f32 {
mut score := f32(1.0)
// Completion rate (40% weight)
completion := s.get_completion_percentage()
if completion < 70 {
score -= 0.4 * (70 - completion) / 70
}
// Team utilization (30% weight)
utilization := s.get_team_utilization()
if utilization < 80 || utilization > 120 {
if utilization < 80 {
score -= 0.3 * (80 - utilization) / 80
} else {
score -= 0.3 * (utilization - 120) / 120
}
}
// Impediments (20% weight)
open_impediments := s.impediments.filter(it.status == .open).len
if open_impediments > 0 {
score -= 0.2 * f32(open_impediments) / 5 // Assume 5+ impediments is very bad
}
// Schedule adherence (10% weight)
if s.is_overdue() {
score -= 0.1
}
if score < 0 {
score = 0
}
return score
}
// get_health_status returns a human-readable health status
pub fn (s Sprint) get_health_status() string {
health := s.get_health_score()
if health >= 0.8 {
return 'Excellent'
} else if health >= 0.6 {
return 'Good'
} else if health >= 0.4 {
return 'At Risk'
} else {
return 'Critical'
}
}