You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
7.8 KiB
291 lines
7.8 KiB
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
|
|
}
|