package projects import ( "git.aiterp.net/stufflog3/stufflog3/entities" "git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/usecases/items" "git.aiterp.net/stufflog3/stufflog3/usecases/scopes" "math" "sort" "time" ) type Entry struct { ID int `json:"id"` OwnerID string `json:"ownerId"` CreatedTime time.Time `json:"createdTime"` Name string `json:"name"` Status models.Status `json:"status"` StatusName string `json:"statusName"` OwnerName string `json:"ownerName,omitempty"` Tags []string `json:"tags"` } func generateEntry(project entities.Project, scope scopes.Result) Entry { return Entry{ ID: project.ID, OwnerID: project.OwnerID, CreatedTime: project.CreatedTime, Name: project.Name, Status: project.Status, Tags: project.Tags, StatusName: scope.StatusName(project.Status), OwnerName: scope.MemberName(project.OwnerID), } } type Result struct { entities.Project OwnerName string `json:"ownerName"` StatusName string `json:"statusName"` TotalAcquired float64 `json:"totalAcquired"` TotalRequired float64 `json:"totalRequired"` TotalPlanned float64 `json:"totalPlanned"` Requirements []RequirementResult `json:"requirements"` } func (r *Result) Requirement(id int) *RequirementResult { for _, requirement := range r.Requirements { if id == requirement.ID { return &requirement } } return nil } type RequirementResult struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` Status models.Status `json:"status"` StatusName string `json:"statusName"` TotalAcquired float64 `json:"totalAcquired"` TotalRequired float64 `json:"totalRequired"` TotalPlanned float64 `json:"totalPlanned"` IsCoarse bool `json:"isCoarse"` AggregateRequired int `json:"aggregateRequired"` Stats []RequirementResultStat `json:"stats"` Items []items.Result `json:"items,omitempty"` Tags []string `json:"tags"` Requirement entities.Requirement `json:"-"` } func (r *RequirementResult) Stat(id int) *RequirementResultStat { for _, stat := range r.Stats { if stat.ID == id { return &stat } } return nil } func (r *RequirementResult) refresh(scope scopes.Result) { r.Name = r.Requirement.Name r.Description = r.Requirement.Description r.Status = r.Requirement.Status r.StatusName = scope.StatusName(r.Requirement.Status) } func (r *RequirementResult) updateStat(stat entities.RequirementStat) bool { if stat.Required < 0 { for i, stat2 := range r.Stats { if stat2.ID == stat.StatID { r.Stats = append(r.Stats[:i], r.Stats[i+1:]...) return true } } } else { for i, stat2 := range r.Stats { if stat2.ID == stat.StatID { r.Stats[i].Required = stat.Required return true } } } return false } type RequirementResultStat struct { ID int `json:"id"` Name string `json:"name"` Weight float64 `json:"weight"` Acquired int `json:"acquired"` Required int `json:"required"` Planned int `json:"planned"` } func generateResult( project entities.Project, scope scopes.Result, requirement []entities.Requirement, requirementStats []entities.RequirementStat, projectItems []items.Result, ) *Result { res := Result{ Project: project, OwnerName: scope.MemberName(project.OwnerID), StatusName: scope.StatusName(project.Status), Requirements: make([]RequirementResult, 0, 8), } for _, req := range requirement { if req.ProjectID != project.ID { continue } resReq := generateRequirementResult(req, scope, requirementStats, projectItems) res.TotalRequired += resReq.TotalRequired res.TotalPlanned += resReq.TotalPlanned if req.Status.Ended() { res.TotalAcquired += resReq.TotalRequired } else { res.TotalAcquired += resReq.TotalAcquired } res.Requirements = append(res.Requirements, resReq) } sort.Slice(res.Requirements, func(i, j int) bool { ri := res.Requirements[i] rj := res.Requirements[j] if ri.Status != rj.Status { return ri.Status.Less(rj.Status) } return ri.ID < rj.ID }) return &res } func generateRequirementResult(req entities.Requirement, scope scopes.Result, requirementStats []entities.RequirementStat, projectItems []items.Result) RequirementResult { resReq := RequirementResult{ ID: req.ID, Name: req.Name, Description: req.Description, Status: req.Status, StatusName: scope.StatusName(req.Status), IsCoarse: req.IsCoarse, AggregateRequired: req.AggregateRequired, Stats: make([]RequirementResultStat, 0, 8), Items: make([]items.Result, 0, 8), Tags: req.Tags, Requirement: req, } resStats := make(map[int]*RequirementResultStat) for _, reqStat := range requirementStats { if reqStat.RequirementID != req.ID { continue } resStat := RequirementResultStat{ ID: reqStat.StatID, Required: reqStat.Required, } for _, stat := range scope.Stats { if stat.ID == resStat.ID { resStat.Name = stat.Name resStat.Weight = stat.Weight break } } resStats[resStat.ID] = &resStat } for _, item := range projectItems { if item.RequirementID == nil || *item.RequirementID != req.ID { continue } for _, stat := range item.Stats { if resStats[stat.ID] != nil { if item.AcquiredTime != nil { resStats[stat.ID].Acquired += stat.Acquired } resStats[stat.ID].Planned += stat.Required } } item.Project = nil item.Requirement = nil resReq.Items = append(resReq.Items, item) } // Sort items so that they're in order of created, with acquired items being in order of acquired below. sort.Slice(resReq.Items, func(i, j int) bool { ii := resReq.Items[i] ij := resReq.Items[j] if ii.AcquiredTime != nil && ij.AcquiredTime != nil { return ii.AcquiredTime.Before(*ij.AcquiredTime) } else if ii.AcquiredTime != nil && ij.AcquiredTime == nil { return false } else if ii.AcquiredTime == nil && ij.AcquiredTime != nil { return true } return ii.CreatedTime.Before(ij.CreatedTime) }) for _, stat := range scope.Stats { if rs := resStats[stat.ID]; rs != nil && rs.Required > 0 { resReq.Stats = append(resReq.Stats, *rs) } } for _, stat := range scope.Stats { if rs := resStats[stat.ID]; rs != nil && rs.Required == 0 { resReq.Stats = append(resReq.Stats, *rs) } } hasRequiredStats := false for _, stat := range resReq.Stats { if stat.Required > 0 { hasRequiredStats = true break } } if hasRequiredStats { totalAcquired := 0.0 totalRequired := 0.0 totalPlanned := 0.0 for _, stat := range resReq.Stats { if stat.Required > 0 { totalAcquired += math.Min(float64(stat.Acquired), float64(stat.Required)) * stat.Weight totalRequired += float64(stat.Required) * stat.Weight totalPlanned += math.Min(float64(stat.Planned), float64(stat.Required)) * stat.Weight } } resReq.TotalRequired += totalRequired resReq.TotalAcquired += math.Min(totalAcquired, totalRequired) resReq.TotalPlanned += math.Min(totalPlanned, totalRequired) } else { for _, item := range resReq.Items { if item.AcquiredTime != nil { resReq.TotalAcquired += item.WeightedRequired } resReq.TotalRequired += item.WeightedRequired } resReq.TotalPlanned = resReq.TotalRequired if req.AggregateRequired > 0 { resReq.TotalRequired = float64(req.AggregateRequired) } } if projectItems == nil { resReq.Items = nil } return resReq }