Browse Source

add aggregate requirement for requirements that estimate on weights instead.

master
Gisle Aune 2 years ago
parent
commit
e4c41e1d47
  1. 18
      entities/project.go
  2. 2
      frontend/src/lib/components/contexts/ItemListContext.svelte
  3. 38
      frontend/src/lib/components/project/AggregateAmountRow.svelte
  4. 5
      frontend/src/lib/components/project/RequirementSection.svelte
  5. 13
      frontend/src/lib/modals/RequirementCreateModal.svelte
  6. 2
      frontend/src/lib/models/project.ts
  7. 9
      models/project.go
  8. 15
      ports/mysql/mysqlcore/models.go
  9. 38
      ports/mysql/mysqlcore/projects.sql.go
  10. 44
      ports/mysql/projects.go
  11. 7
      ports/mysql/queries/projects.sql
  12. 9
      scripts/goose-mysql/20220727173716_requirement_aggregate_required.sql
  13. 44
      usecases/projects/result.go

18
entities/project.go

@ -31,13 +31,14 @@ func (project *Project) Update(update models.ProjectUpdate) {
}
type Requirement struct {
ID int `json:"id"`
ScopeID int `json:"scopeId"`
ProjectID int `json:"projectId"`
Name string `json:"name"`
Description string `json:"description"`
IsCoarse bool `json:"isCoarse"`
Status models.Status `json:"status"`
ID int `json:"id"`
ScopeID int `json:"scopeId"`
ProjectID int `json:"projectId"`
AggregateRequired int `json:"aggregateRequired"`
Name string `json:"name"`
Description string `json:"description"`
IsCoarse bool `json:"isCoarse"`
Status models.Status `json:"status"`
}
func (requirement *Requirement) Update(update models.RequirementUpdate) {
@ -53,6 +54,9 @@ func (requirement *Requirement) Update(update models.RequirementUpdate) {
if update.IsCoarse != nil {
requirement.IsCoarse = *update.IsCoarse
}
if update.AggregateRequired != nil {
requirement.AggregateRequired = *update.AggregateRequired
}
}
type RequirementStat struct {

2
frontend/src/lib/components/contexts/ItemListContext.svelte

@ -49,7 +49,7 @@
}
try {
const res = await sl3(fetch).listItems($scope.id, filter, true)
const res = await sl3(fetch).listItems($scope.id, filter, true, {});
itemsWritable.set(res.items);
groupsWritable.set(res.groups);
} catch(_) {}

38
frontend/src/lib/components/project/AggregateAmountRow.svelte

@ -0,0 +1,38 @@
<script lang="ts">
import type Item from "$lib/models/item";
import Amount from "../common/Amount.svelte";
import AmountRow from "../common/AmountRow.svelte";
import { getScopeContext } from "../contexts/ScopeContext.svelte";
export let items: Item[];
export let totalAcquired: number;
export let aggregateRequired: number;
const {scope} = getScopeContext();
let amounts: {name: string, amount: number}[] = [];
$: {
const statMap: Record<number, number> = {};
for (const item of items) {
for (const stat of item.stats) {
statMap[stat.id] = (statMap[stat.id] || 0) + stat.acquired
}
}
amounts = [];
for (const stat of $scope.stats) {
if (!!statMap[stat.id]) {
amounts.push({name: stat.name, amount: statMap[stat.id]});
}
}
}
</script>
<AmountRow>
{#each amounts as {name, amount} (name)}
<Amount label={name} value={amount} />
{/each}
<Amount right label="" dark value={totalAcquired} target={aggregateRequired} />
</AmountRow>

5
frontend/src/lib/components/project/RequirementSection.svelte

@ -11,6 +11,8 @@
import ItemEntry from "./ItemSubSection.svelte";
import Icon from "../layout/Icon.svelte";
import { projectPrettyId } from "$lib/utils/prettyIds";
import AmountRow from "../common/AmountRow.svelte";
import AggregateAmountRow from "./AggregateAmountRow.svelte";
export let requirement: Requirement;
</script>
@ -19,6 +21,9 @@
<Section title={requirement.name} icon={STATUS_ICONS[requirement.status]} status={requirement.status}>
<Progress alwaysSmooth titlePercentageOnly thin green status={requirement.status} count={requirement.totalAcquired} target={requirement.totalRequired} />
<Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} />
{#if requirement.aggregateRequired > 0}
<AggregateAmountRow items={requirement.items} totalAcquired={requirement.totalAcquired} aggregateRequired={requirement.aggregateRequired} />
{/if}
<Markdown source={requirement.description} />
<OptionsRow slot="right">
<Option open={{name: "item.create", requirement}}><Icon name="plus" /></Option>

13
frontend/src/lib/modals/RequirementCreateModal.svelte

@ -21,6 +21,7 @@ import { statDiff } from "$lib/utils/stat";
let projectId: number
let requirementId: number
let oldStats: RequirementInput["stats"]
let showAggregateRequired: boolean
let error: string
let loading: boolean
@ -44,6 +45,8 @@ import { statDiff } from "$lib/utils/stat";
show = false;
}
$: showAggregateRequired = requirement?.stats?.find(s => s.required > 0) == null;
function initCreate(project: ProjectEntry) {
requirement = {
name: "",
@ -51,6 +54,7 @@ import { statDiff } from "$lib/utils/stat";
stats: [],
status: Status.Available,
isCoarse: false,
aggregateRequired: 0,
}
projectId = project.id;
@ -64,6 +68,7 @@ import { statDiff } from "$lib/utils/stat";
status: current.status,
stats: current.stats.map(s => ({acquired: s.acquired, required: s.required, statId: s.id})),
isCoarse: current.isCoarse,
aggregateRequired: current.aggregateRequired,
}
oldStats = [...requirement.stats];
@ -76,6 +81,10 @@ import { statDiff } from "$lib/utils/stat";
error = null;
loading = true;
if (requirement.stats.find(s => s.required > 0)) {
requirement.aggregateRequired = 0;
}
try {
switch (op) {
case "Create":
@ -115,6 +124,10 @@ import { statDiff } from "$lib/utils/stat";
<input name="name" type="text" bind:value={requirement.name} />
<label for="description">Description</label>
<textarea name="description" bind:value={requirement.description} />
{#if showAggregateRequired}
<label for="aggregateRequired">Aggregate Required (Zero for old behavior)</label>
<input name="aggregateRequired" type="number" bind:value={requirement.aggregateRequired} />
{/if}
</ModalBody>
<ModalBody>
<label for="stats">Status</label>

2
frontend/src/lib/models/project.ts

@ -38,6 +38,7 @@ export interface Requirement {
totalRequired: number
totalPlanned: number
isCoarse: boolean
aggregateRequired: number
stats: StatProgressWithPlanned[]
items: Item[]
}
@ -47,5 +48,6 @@ export interface RequirementInput {
description: string
status: Status
isCoarse: boolean
aggregateRequired: number
stats: StatValueInput[]
}

9
models/project.go

@ -8,8 +8,9 @@ type ProjectUpdate struct {
}
type RequirementUpdate struct {
Name *string `json:"name"`
Description *string `json:"description"`
Status *Status `json:"status"`
IsCoarse *bool `json:"isCoarse"`
Name *string `json:"name"`
Description *string `json:"description"`
Status *Status `json:"status"`
IsCoarse *bool `json:"isCoarse"`
AggregateRequired *int `json:"aggregateRequired"`
}

15
ports/mysql/mysqlcore/models.go

@ -41,13 +41,14 @@ type Project struct {
}
type ProjectRequirement struct {
ID int
ScopeID int
ProjectID int
Name string
Status int
Description string
IsCoarse bool
ID int
ScopeID int
ProjectID int
Name string
Status int
Description string
IsCoarse bool
AggregateRequired int
}
type ProjectRequirementStat struct {

38
ports/mysql/mysqlcore/projects.sql.go

@ -156,17 +156,18 @@ func (q *Queries) InsertProject(ctx context.Context, arg InsertProjectParams) (s
}
const insertProjectRequirement = `-- name: InsertProjectRequirement :execresult
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse, aggregate_required)
VALUES (?, ?, ?, ?, ?, ?, ?)
`
type InsertProjectRequirementParams struct {
ScopeID int
ProjectID int
Name string
Status int
Description string
IsCoarse bool
ScopeID int
ProjectID int
Name string
Status int
Description string
IsCoarse bool
AggregateRequired int
}
func (q *Queries) InsertProjectRequirement(ctx context.Context, arg InsertProjectRequirementParams) (sql.Result, error) {
@ -177,11 +178,12 @@ func (q *Queries) InsertProjectRequirement(ctx context.Context, arg InsertProjec
arg.Status,
arg.Description,
arg.IsCoarse,
arg.AggregateRequired,
)
}
const listProjectRequirements = `-- name: ListProjectRequirements :many
SELECT id, scope_id, project_id, name, status, description, is_coarse FROM project_requirement WHERE project_id = ?
SELECT id, scope_id, project_id, name, status, description, is_coarse, aggregate_required FROM project_requirement WHERE project_id = ?
`
func (q *Queries) ListProjectRequirements(ctx context.Context, projectID int) ([]ProjectRequirement, error) {
@ -201,6 +203,7 @@ func (q *Queries) ListProjectRequirements(ctx context.Context, projectID int) ([
&i.Status,
&i.Description,
&i.IsCoarse,
&i.AggregateRequired,
); err != nil {
return nil, err
}
@ -330,17 +333,19 @@ UPDATE project_requirement
SET name = ?,
status = ?,
description = ?,
is_coarse = ?
is_coarse = ?,
aggregate_required = ?
WHERE id = ? AND scope_id = ?
`
type UpdateProjectRequirementParams struct {
Name string
Status int
Description string
IsCoarse bool
ID int
ScopeID int
Name string
Status int
Description string
IsCoarse bool
AggregateRequired int
ID int
ScopeID int
}
func (q *Queries) UpdateProjectRequirement(ctx context.Context, arg UpdateProjectRequirementParams) error {
@ -349,6 +354,7 @@ func (q *Queries) UpdateProjectRequirement(ctx context.Context, arg UpdateProjec
arg.Status,
arg.Description,
arg.IsCoarse,
arg.AggregateRequired,
arg.ID,
arg.ScopeID,
)

44
ports/mysql/projects.go

@ -194,7 +194,7 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
return []entities.Requirement{}, []entities.RequirementStat{}, nil
}
sq := squirrel.Select("id, scope_id, project_id, name, status, description, is_coarse").
sq := squirrel.Select("id, scope_id, project_id, name, status, description, is_coarse, aggregate_required").
From("project_requirement").
Where(squirrel.Eq{"id": requirementIDs})
if scopeID != -1 {
@ -221,6 +221,7 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
&requirement.Status,
&requirement.Description,
&requirement.IsCoarse,
&requirement.AggregateRequired,
); err != nil {
return nil, nil, err
}
@ -271,13 +272,14 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int)
requirements := make([]entities.Requirement, 0, len(reqRows))
for _, row := range reqRows {
requirements = append(requirements, entities.Requirement{
ID: row.ID,
ScopeID: row.ScopeID,
ProjectID: row.ProjectID,
Name: row.Name,
Description: row.Description,
IsCoarse: row.IsCoarse,
Status: models.Status(row.Status),
ID: row.ID,
ScopeID: row.ScopeID,
ProjectID: row.ProjectID,
Name: row.Name,
Description: row.Description,
IsCoarse: row.IsCoarse,
AggregateRequired: row.AggregateRequired,
Status: models.Status(row.Status),
})
}
@ -295,12 +297,13 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int)
func (r *projectRepository) CreateRequirement(ctx context.Context, requirement entities.Requirement) (*entities.Requirement, error) {
res, err := r.q.InsertProjectRequirement(ctx, mysqlcore.InsertProjectRequirementParams{
ScopeID: requirement.ScopeID,
ProjectID: requirement.ProjectID,
Name: requirement.Name,
Status: int(requirement.Status),
Description: requirement.Description,
IsCoarse: requirement.IsCoarse,
ScopeID: requirement.ScopeID,
ProjectID: requirement.ProjectID,
Name: requirement.Name,
Status: int(requirement.Status),
Description: requirement.Description,
IsCoarse: requirement.IsCoarse,
AggregateRequired: requirement.AggregateRequired,
})
if err != nil {
return nil, err
@ -319,12 +322,13 @@ func (r *projectRepository) UpdateRequirement(ctx context.Context, requirement e
requirement.Update(update)
return r.q.UpdateProjectRequirement(ctx, mysqlcore.UpdateProjectRequirementParams{
Name: requirement.Name,
Status: int(requirement.Status),
Description: requirement.Description,
IsCoarse: requirement.IsCoarse,
ID: requirement.ID,
ScopeID: requirement.ScopeID,
Name: requirement.Name,
Status: int(requirement.Status),
Description: requirement.Description,
IsCoarse: requirement.IsCoarse,
ID: requirement.ID,
ScopeID: requirement.ScopeID,
AggregateRequired: requirement.AggregateRequired,
})
}

7
ports/mysql/queries/projects.sql

@ -23,15 +23,16 @@ DELETE FROM project WHERE id = ? AND scope_id = ?;
SELECT * FROM project_requirement WHERE project_id = ?;
-- name: InsertProjectRequirement :execresult
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse)
VALUES (?, ?, ?, ?, ?, ?);
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse, aggregate_required)
VALUES (?, ?, ?, ?, ?, ?, ?);
-- name: UpdateProjectRequirement :exec
UPDATE project_requirement
SET name = ?,
status = ?,
description = ?,
is_coarse = ?
is_coarse = ?,
aggregate_required = ?
WHERE id = ? AND scope_id = ?;
-- name: DeleteProjectRequirement :exec

9
scripts/goose-mysql/20220727173716_requirement_aggregate_required.sql

@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE project_requirement ADD COLUMN aggregate_required INT NOT NULL DEFAULT 0;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE project_requirement DROP COLUMN aggregate_required;
-- +goose StatementEnd

44
usecases/projects/result.go

@ -53,17 +53,18 @@ func (r *Result) Requirement(id int) *RequirementResult {
}
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"`
Stats []RequirementResultStat `json:"stats"`
Items []items.Result `json:"items"`
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"`
Requirement entities.Requirement `json:"-"`
}
@ -161,14 +162,15 @@ func generateResult(
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,
Stats: make([]RequirementResultStat, 0, 8),
Items: make([]items.Result, 0, 8),
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),
Requirement: req,
}
@ -271,6 +273,10 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
resReq.TotalRequired += item.WeightedRequired
}
resReq.TotalPlanned = resReq.TotalRequired
if req.AggregateRequired > 0 {
resReq.TotalRequired = float64(req.AggregateRequired)
}
}
return resReq

Loading…
Cancel
Save