Compare commits

...
6 Commits
Author SHA1 Message Date
gisle 396b87eea1 add option to show only active items.
continuous-integration/drone/push Build is passing
2022-07-06 22:13:21 +02:00
gisle 1d59182d43 make overview item sprints even more compact by hiding stats.
continuous-integration/drone/push Build is passing
2022-07-06 22:04:56 +02:00
gisle 951be980c0 change item sprint picker to sort projects and reqs by id instead of name. 2022-07-06 22:01:44 +02:00
gisle 3a8bb9709f improve sprint item picker.
continuous-integration/drone/push Build is passing
2022-07-06 19:56:10 +02:00
gisle 015c0c18b9 add item list on sprint page.
continuous-integration/drone/push Build is passing
2022-07-05 18:36:00 +02:00
gisle ed828b0227 add item-based progress bar on requirements with only 0-required stats.
continuous-integration/drone/push Build is passing
2022-07-04 19:59:38 +02:00
16 changed files with 383 additions and 78 deletions
+1
View File
@@ -64,6 +64,7 @@ func main() {
Stats: statsService,
Repository: db.Items(),
RequirementFetcher: db.Projects(),
ProjectFetcher: db.Projects(),
}
projectsService := &projects.Service{
Auth: authService,
+1
View File
@@ -62,6 +62,7 @@ func main() {
Stats: statsService,
Repository: db.Items(),
RequirementFetcher: db.Projects(),
ProjectFetcher: db.Projects(),
}
projectsService := &projects.Service{
Auth: authService,
@@ -1,10 +1,30 @@
<script lang="ts" context="module">
export interface PartOptionGroup {
id: number
label: string
subs: PartOptionGroup[]
options: PartOption[]
}
export interface PartOption {
partId: number
name: string
enabled: boolean
required: number
}
const SO = [
Status.Active, Status.Background, Status.Available, Status.Blocked,
Status.Completed, Status.Failed, Status.Dropped,
]
function compareStatus<T extends {status: Status, id: number}>(a: T, b: T): number {
if (a.status === b.status) {
return a.id - b.id;
}
return SO.indexOf(a.status) - SO.indexOf(b.status)
}
</script>
<script lang="ts">
@@ -12,6 +32,8 @@
import { sl3 } from "$lib/clients/sl3";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import { SprintKind, type SprintInputPart } from "$lib/models/sprint";
import Status from "$lib/models/status";
import shortestDecimal from "$lib/utils/numbers";
import Checkbox from "../layout/Checkbox.svelte";
export let value: SprintInputPart[] = [];
@@ -20,50 +42,96 @@
const {scope} = getScopeContext();
let error = "";
let options: PartOption[] = [];
let groups: PartOptionGroup[] = [];
let showRequired = false;
let loading = false;
let onlyActive = true;
let loaded: SprintKind = SprintKind.Invalid
function forceReload() {
loaded = SprintKind.Invalid;
}
async function load(kind: SprintKind) {
loading = true;
try {
switch (kind) {
case SprintKind.Items: {
const items = await sl3(fetch).listItems($scope.id, {});
options = items.map(i => ({
enabled: false,
name: `${i.name}${i.acquiredTime?" ✓":""}`,
partId: i.id,
required: 0,
const items = await sl3(fetch).listItems($scope.id, {unAcquired: onlyActive});
const projects = items.filter(i => i.project != null).map(i => i.project).sort(compareStatus).filter((p, i, a) => p.id != a[i-1]?.id);
const requirements = items.filter(i => i.requirement != null).map(i => i.requirement).sort(compareStatus).filter((r, i, a) => r.id != a[i-1]?.id);
const loose = items.filter(i => i.requirementId == null);
const reqProjectIdMap = items.filter(i => i.projectId != null && i.requirementId != null).reduce((p, c) => ({
...p,
[c.requirementId]: c.projectId,
}), {} as Record<number, string>)
groups = projects.filter(p => !onlyActive || p.status === Status.Active || p.status === Status.Background).map(p => ({
id: p.id,
label: `${p.name} (${p.statusName})`,
options: [],
subs: requirements.filter(r => !onlyActive || r.status === Status.Active || r.status === Status.Background).filter(r => reqProjectIdMap[r.id] === p.id).map(r => ({
id: r.id,
label: `${r.name} (${r.statusName})`,
options: items.filter(i => i.requirementId === r.id).map(i => ({
enabled: false,
partId: i.id,
name: `${i.name} (${shortestDecimal(i.weightedRequired)})${!!i.acquiredTime?" ✓":""}`,
required: 0,
})).sort((a,b) => a.partId - b.partId),
subs: [],
}))
}))
groups.push({id: -1, label: "Loose items", subs: [], options: loose.map(i => ({
enabled: false,
partId: i.id,
name: `${i.name} (${shortestDecimal(i.weightedRequired)})${!!i.acquiredTime?" ✓":""}`,
required: 0,
})).sort((a,b) => a.partId - b.partId)})
showRequired = false;
break;
}
case SprintKind.Requirements: {
options = [];
groups = [];
throw "Requirement selection is not yet supported the UI."
}
case SprintKind.Stats: {
options = $scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0}));
groups = [{
id: -1,
label: "",
subs: [],
options: $scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0})),
}];
showRequired = true;
break;
}
case SprintKind.Scope: {
options = [];
groups = [];
showRequired = false;
break;
}
}
error = "";
for (const opt of options) {
const val = value.find(v => v.partId === opt.partId);
if (val != null) {
opt.enabled = true;
opt.required = val.required;
for (const group of groups) {
for (const opt of group.options) {
const val = value.find(v => v.partId === opt.partId);
if (val != null) {
opt.enabled = true;
opt.required = val.required;
}
}
for (const subGroup of group.subs) {
for (const opt of subGroup.options) {
const val = value.find(v => v.partId === opt.partId);
if (val != null) {
opt.enabled = true;
opt.required = val.required;
}
}
}
}
} catch(err) {
@@ -74,8 +142,13 @@
loading = false;
}
// Hax to make it reload if onlyActive changed.
$: if (onlyActive != null) {
forceReload();
}
$: if (loaded === kind && error === "") {
value = options.filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required}))
value = groups.flatMap(g => [...g.subs.flatMap(g => g.options), ...g.options]).filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required}))
}
$: if (browser && !loading && loaded !== kind) {
@@ -85,15 +158,34 @@
<div class="part-input">
<div class="error">{error}</div>
{#each options as option (option.partId)}
<div class="part">
<Checkbox noLabel bind:checked={option.enabled} />
<div class="name" class:disabled={!option.enabled}>{option.name}</div>
{#if showRequired && option.enabled}
<input type="number" class="required" bind:value={option.required} />
{/if}
</div>
{#each groups as group (group.id)}
<div class="label">{group.label}</div>
{#each group.subs as group (group.id)}
<div class="label label-2">{group.label}</div>
{#each group.options as option (option.partId)}
<div class="part">
<Checkbox noLabel bind:checked={option.enabled} />
<div class="name" class:disabled={!option.enabled}>{option.name}</div>
{#if showRequired && option.enabled}
<input type="number" class="required" bind:value={option.required} />
{/if}
</div>
{/each}
{/each}
{#each group.options as option (option.partId)}
<div class="part">
<Checkbox noLabel bind:checked={option.enabled} />
<div class="name" class:disabled={!option.enabled}>{option.name}</div>
{#if showRequired && option.enabled}
<input type="number" class="required" bind:value={option.required} />
{/if}
</div>
{/each}
{/each}
{#if kind === SprintKind.Items}
<hr />
<Checkbox label="Show only active items" bind:checked={onlyActive} />
{/if}
</div>
<style lang="scss">
@@ -103,7 +195,6 @@
display: flex;
margin: 0;
padding: 0;
border-top: 1px solid $color-entry1;
&:first-of-type {
border-top: none;
@@ -123,7 +214,7 @@
}
div {
padding: 0.3em 0.5ch;
padding: 0.25em 0.5ch;
&.disabled {
color: $color-entry2;
@@ -131,11 +222,37 @@
}
}
hr {
margin-top: 2em;
border: 0;
border-bottom: 1px solid $color-entry2;
}
div.label {
margin-top: 0.5em;
border-bottom: 1px solid $color-entry2;
&:empty {
margin-top: 0;
border-bottom: none;
}
&.label-2 {
margin-top: 1em;
font-size: 0.75em;
border-bottom: none;
}
&+div.label-2 {
margin-top: 0.5em;
}
}
div.part-input {
width: calc(100% - 2ch);
margin-bottom: 1em;
margin-top: 0.20em;
height: 12em;
height: 26em;
overflow-y: auto;
background: $color-entryhalf;
@@ -15,6 +15,7 @@
export let event: "created" | "scheduled" | "acquired" | "none" = "none";
export let compact = false;
export let showAcquiredTime = false;
export let hideStats = false;
let icon: IconName
let status: Status
@@ -57,23 +58,25 @@
<Markdown source={item.description} />
{/if}
<AmountRow>
{#if item.acquiredTime != null}
{#each item.stats as stat (stat.id)}
{#if stat.required > 0}
<Amount label={stat.name} value={stat.acquired} target={stat.required} />
{#if !hideStats}
{#if item.acquiredTime != null}
{#each item.stats as stat (stat.id)}
{#if stat.required > 0}
<Amount label={stat.name} value={stat.acquired} target={stat.required} />
{/if}
{/each}
{#if item.weightedRequired}
<Amount dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
{/if}
{/each}
{#if item.weightedRequired}
<Amount dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
{/if}
{:else}
{#each item.stats as stat (stat.id)}
{#if stat.required > 0}
<Amount label={stat.name} value={stat.required} />
{:else}
{#each item.stats as stat (stat.id)}
{#if stat.required > 0}
<Amount label={stat.name} value={stat.required} />
{/if}
{/each}
{#if item.weightedRequired}
<Amount dark right label="" value={item.weightedRequired} />
{/if}
{/each}
{#if item.weightedRequired}
<Amount dark right label="" value={item.weightedRequired} />
{/if}
{/if}
</AmountRow>
@@ -9,7 +9,7 @@
import LabeledProgressRow from "../common/LabeledProgressRow.svelte";
import { STATUS_ICONS } from "../common/StatusIcon.svelte";
import ItemEntry from "./ItemSubSection.svelte";
import Icon from "../layout/Icon.svelte";
import Icon from "../layout/Icon.svelte";
export let requirement: Requirement;
</script>
@@ -36,11 +36,17 @@
{#if sprint.kind === SprintKind.Items}
{#each (sprint.items||[]) as item (item.id)}
{#if !compact || !item.acquiredTime}
<ItemSubSection compact={compact} item={item} />
<ItemSubSection compact={compact} hideStats={compact} item={item} />
{/if}
{/each}
{:else}
<AmountRow>
<Amount label="Acquired Items" value={(sprint.items||[]).length} />
</AmountRow>
{#if compact}
<AmountRow>
<Amount label="Acquired Items" value={(sprint.items||[]).length} />
</AmountRow>
{:else}
{#each (sprint.items||[]) as item (item.id)}
<ItemSubSection compact item={item} />
{/each}
{/if}
{/if}
@@ -79,7 +79,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
function initEdit(current: Item, requirement?: Requirement, scope?: Scope) {
const req = $project.requirements?.find(r => r.id === current.requirementId);
let ctxStats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
if (ctxStats == null) {
if (ctxStats == null || ctxStats.length === 0) {
ctxStats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0}))
}
const inputStats = current.stats.map(s => ({statId: s.id, acquired: s.acquired, required: s.required}));
@@ -153,15 +153,15 @@ import { partsDiff } from "$lib/utils/sprint";
<label for="aggregateValue">Aggregate Goal</label>
<input name="aggregateValue" type="number" bind:value={sprint.aggregateRequired} />
{/if}
<Checkbox bind:checked={sprint.isTimed} label="Sprint is timed" />
<Checkbox bind:checked={sprint.isCoarse} label="Show only aggregate" />
<Checkbox bind:checked={sprint.isUnweighted} label="Aggregate is unweighted" />
</ModalBody>
<ModalBody>
<label for="kind">Kind</label>
<SprintKindSelect disabled={op === "Edit"} bind:kind={sprint.kind} />
<label for="parts">Parts</label>
<PartInput bind:value={sprint.parts} kind={sprint.kind} />
<Checkbox bind:checked={sprint.isTimed} label="Sprint is timed" />
<Checkbox bind:checked={sprint.isCoarse} label="Show only aggregate" />
<Checkbox bind:checked={sprint.isUnweighted} label="Aggregate is unweighted" />
</ModalBody>
</Modal>
</form>
+18
View File
@@ -1,5 +1,6 @@
import type { TimeInterval } from "./common";
import type { StatValueInput, StatProgress } from "./stat";
import type Status from "./status";
export default interface Item {
id: number
@@ -15,6 +16,23 @@ export default interface Item {
stats: StatProgress[]
weightedAcquired: number
weightedRequired: number
project: ItemProject
requirement: ItemRequirement
}
interface ItemProject {
id: number
name: string
status: Status
statusName: string
createdTime: string
}
interface ItemRequirement {
id: number
name: string
status: Status
statusName: string
}
export interface ItemInput {
+2 -6
View File
@@ -1,7 +1,6 @@
package mysql
import (
"context"
"database/sql"
"encoding/json"
"fmt"
@@ -75,11 +74,8 @@ func Connect(host string, port int, username, password, database string) (*Datab
return nil, err
}
q, err := mysqlcore.Prepare(context.Background(), db)
if err != nil {
return nil, err
}
q := mysqlcore.New(db)
return &Database{db: db, q: q}, nil
}
+55
View File
@@ -35,6 +35,61 @@ func (r *projectRepository) Find(ctx context.Context, scopeID, projectID int) (*
}, nil
}
func (r *projectRepository) FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error) {
if len(ids) == 0 {
return []entities.Project{}, nil
} else if len(ids) == 1 && scopeID != -1 {
project, err := r.Find(ctx, scopeID, ids[0])
if err != nil {
return nil, err
}
return []entities.Project{*project}, nil
}
sq := squirrel.Select("id,scope_id,owner_id,name,status,description,created_time").
From("project").
Where(squirrel.Eq{"id": ids})
if scopeID != -1 {
sq = sq.Where(squirrel.Eq{"scope_id": scopeID})
}
query, args, err := sq.ToSql()
if err != nil {
return nil, err
}
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
projects := make([]entities.Project, 0, len(ids))
for rows.Next() {
project := entities.Project{}
if err := rows.Scan(
&project.ID,
&project.ScopeID,
&project.OwnerID,
&project.Name,
&project.Status,
&project.Description,
&project.CreatedTime,
); err != nil {
return nil, err
}
projects = append(projects, project)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return projects, nil
}
func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.Project, error) {
rows, err := r.q.ListProjects(ctx, scopeID)
if err != nil {
+4
View File
@@ -16,6 +16,10 @@ type Repository interface {
UpdateStat(ctx context.Context, progress entities.ItemStat) error
}
type ProjectFetcher interface {
FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error)
}
type RequirementFetcher interface {
FetchRequirements(ctx context.Context, scopeID int, requirementIDs ...int) ([]entities.Requirement, []entities.RequirementStat, error)
}
+45 -6
View File
@@ -2,16 +2,34 @@ package items
import (
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"time"
)
type Result struct {
entities.Item
Requirement *entities.Requirement `json:"requirement,omitempty"`
Stats []ResultStat `json:"stats"`
WeightedAcquired float64 `json:"weightedAcquired"`
WeightedRequired float64 `json:"weightedRequired"`
Requirement *ResultRequirement `json:"requirement,omitempty"`
Project *ResultProject `json:"project,omitempty"`
Stats []ResultStat `json:"stats"`
WeightedAcquired float64 `json:"weightedAcquired"`
WeightedRequired float64 `json:"weightedRequired"`
}
type ResultRequirement struct {
ID int `json:"id"`
Name string `json:"name"`
Status models.Status `json:"status"`
StatusName string `json:"statusName"`
}
type ResultProject struct {
ID int `json:"id"`
CreatedTime time.Time `json:"createdTime"`
Name string `json:"name"`
Status models.Status `json:"status"`
StatusName string `json:"statusName"`
}
func (r *Result) Stat(statID int) *ResultStat {
@@ -101,7 +119,7 @@ type ResultStat struct {
Required int `json:"required"`
}
func generateResult(item entities.Item, scope scopes.Result, progresses []entities.ItemStat, requirements []entities.Requirement) Result {
func generateResult(item entities.Item, scope scopes.Result, progresses []entities.ItemStat, requirements []entities.Requirement, projects []entities.Project) Result {
res := Result{
Item: item,
Stats: make([]ResultStat, 0, 8),
@@ -110,7 +128,28 @@ func generateResult(item entities.Item, scope scopes.Result, progresses []entiti
if item.RequirementID != nil {
for _, req := range requirements {
if req.ID == *item.RequirementID {
res.Requirement = &req
res.Requirement = &ResultRequirement{
ID: req.ID,
Name: req.Name,
Status: req.Status,
StatusName: scope.StatusName(req.Status),
}
break
}
}
}
if item.ProjectID != nil {
for _, project := range projects {
if project.ID == *item.ProjectID {
res.Project = &ResultProject{
ID: project.ID,
CreatedTime: project.CreatedTime,
Name: project.Name,
Status: project.Status,
StatusName: scope.StatusName(project.Status),
}
break
}
}
}
+47 -5
View File
@@ -17,6 +17,7 @@ type Service struct {
Scopes *scopes.Service
Stats *stats.Service
Repository Repository
ProjectFetcher ProjectFetcher
RequirementFetcher RequirementFetcher
}
@@ -44,7 +45,15 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
}
}
result := generateResult(*item, sc.Scope, itemStats, requirements)
var projects []entities.Project
if item.ProjectID != nil {
projects, err = s.ProjectFetcher.FetchProjects(ctx, item.ScopeID, *item.ProjectID)
if err != nil {
return nil, err
}
}
result := generateResult(*item, sc.Scope, itemStats, requirements, projects)
return &result, nil
}
@@ -88,10 +97,14 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
}
reqIds := genutils.Set[int]{}
projIds := genutils.Set[int]{}
for _, item := range items {
if item.RequirementID != nil {
reqIds.Add(*item.RequirementID)
}
if item.ProjectID != nil {
projIds.Add(*item.ProjectID)
}
}
var requirements []entities.Requirement
if reqIds.Len() > 0 {
@@ -100,10 +113,17 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
return nil, err
}
}
var projects []entities.Project
if projIds.Len() > 0 {
projects, err = s.ProjectFetcher.FetchProjects(ctx, sc.ID, projIds.Values()...)
if err != nil {
return nil, err
}
}
res := make([]Result, 0, len(items))
for _, item := range items {
res = append(res, generateResult(item, sc.Scope, progresses, requirements))
res = append(res, generateResult(item, sc.Scope, progresses, requirements, projects))
}
return res, nil
@@ -122,6 +142,7 @@ func (s *Service) Create(ctx context.Context, item entities.Item, stats []entiti
}
}
var requirements []entities.Requirement
var projects []entities.Project
if item.RequirementID != nil {
reqs, _, err := s.RequirementFetcher.FetchRequirements(ctx, scope.ID, *item.RequirementID)
if err != nil {
@@ -133,6 +154,8 @@ func (s *Service) Create(ctx context.Context, item entities.Item, stats []entiti
item.ProjectID = &reqs[0].ProjectID
requirements = reqs
projects, _ = s.ProjectFetcher.FetchProjects(ctx, scope.ID, *item.ProjectID)
}
item.ScopeID = scope.ID
item.OwnerID = user.ID
@@ -161,7 +184,7 @@ func (s *Service) Create(ctx context.Context, item entities.Item, stats []entiti
goodStats = append(goodStats, stat)
}
result := generateResult(*newItem, scope, goodStats, requirements)
result := generateResult(*newItem, scope, goodStats, requirements, projects)
return &result, nil
}
@@ -197,7 +220,26 @@ func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate,
}
item.ProjectID = genutils.Ptr(req[0].ProjectID)
item.Requirement = &req[0]
item.Requirement = &ResultRequirement{
ID: req[0].ID,
Name: req[0].Name,
Status: req[0].Status,
StatusName: scope.StatusName(req[0].Status),
}
projects, err := s.ProjectFetcher.FetchProjects(ctx, scope.ID, *item.ProjectID)
if err != nil {
return nil, err
}
if len(projects) == 1 {
item.Project = &ResultProject{
ID: projects[0].ID,
CreatedTime: projects[0].CreatedTime,
Name: projects[0].Name,
Status: projects[0].Status,
StatusName: scope.StatusName(projects[0].Status),
}
}
} else {
item.ProjectID = nil
}
@@ -211,7 +253,7 @@ func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate,
for _, stat := range stats {
stat.ItemID = item.ID
err = s.Repository.UpdateStat(ctx, stat)
if err == nil {
item.AddStat(scope, stat)
+1
View File
@@ -9,6 +9,7 @@ import (
type Repository interface {
Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error)
List(ctx context.Context, scopeID int) ([]entities.Project, error)
FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error)
Insert(ctx context.Context, project entities.Project) (*entities.Project, error)
Update(ctx context.Context, project entities.Project, update models.ProjectUpdate) error
Delete(ctx context.Context, project entities.Project) error
+31 -9
View File
@@ -204,6 +204,9 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
}
}
item.Project = nil
item.Requirement = nil
resReq.Items = append(resReq.Items, item)
}
@@ -233,19 +236,38 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
}
}
totalAcquired := 0.0
totalRequired := 0.0
totalPlanned := 0.0
hasRequiredStats := false
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
hasRequiredStats = true
break
}
}
resReq.TotalRequired += totalRequired
resReq.TotalAcquired += math.Min(totalAcquired, totalRequired)
resReq.TotalPlanned += math.Min(totalPlanned, totalRequired)
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
}
return resReq
}