Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396b87eea1 | ||
|
|
1d59182d43 | ||
|
|
951be980c0 | ||
|
|
3a8bb9709f | ||
|
|
015c0c18b9 | ||
|
|
ed828b0227 |
@@ -64,6 +64,7 @@ func main() {
|
||||
Stats: statsService,
|
||||
Repository: db.Items(),
|
||||
RequirementFetcher: db.Projects(),
|
||||
ProjectFetcher: db.Projects(),
|
||||
}
|
||||
projectsService := &projects.Service{
|
||||
Auth: authService,
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user