Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbfbd7c3e7 | ||
|
|
344f743dc6 | ||
|
|
1b73c83b3b | ||
|
|
3a1142a965 | ||
|
|
3737a80c46 | ||
|
|
0d2e027816 | ||
|
|
cac4ac0bf6 | ||
|
|
f5dee09e40 | ||
|
|
53be464d7e | ||
|
|
b381e2cbed | ||
|
|
d7c1fbb21c | ||
|
|
d4141f5eb3 | ||
|
|
1232721921 | ||
|
|
a70bb4bb23 | ||
|
|
0ba65038e9 | ||
|
|
6c3fdad79c | ||
|
|
e307ed6021 | ||
|
|
e4c41e1d47 | ||
|
|
ae47f1a0e5 | ||
|
|
b9ffe256e9 |
+11
-7
@@ -31,13 +31,14 @@ func (project *Project) Update(update models.ProjectUpdate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Requirement struct {
|
type Requirement struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
ScopeID int `json:"scopeId"`
|
ScopeID int `json:"scopeId"`
|
||||||
ProjectID int `json:"projectId"`
|
ProjectID int `json:"projectId"`
|
||||||
Name string `json:"name"`
|
AggregateRequired int `json:"aggregateRequired"`
|
||||||
Description string `json:"description"`
|
Name string `json:"name"`
|
||||||
IsCoarse bool `json:"isCoarse"`
|
Description string `json:"description"`
|
||||||
Status models.Status `json:"status"`
|
IsCoarse bool `json:"isCoarse"`
|
||||||
|
Status models.Status `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (requirement *Requirement) Update(update models.RequirementUpdate) {
|
func (requirement *Requirement) Update(update models.RequirementUpdate) {
|
||||||
@@ -53,6 +54,9 @@ func (requirement *Requirement) Update(update models.RequirementUpdate) {
|
|||||||
if update.IsCoarse != nil {
|
if update.IsCoarse != nil {
|
||||||
requirement.IsCoarse = *update.IsCoarse
|
requirement.IsCoarse = *update.IsCoarse
|
||||||
}
|
}
|
||||||
|
if update.AggregateRequired != nil {
|
||||||
|
requirement.AggregateRequired = *update.AggregateRequired
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequirementStat struct {
|
type RequirementStat struct {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-amplify/auth": "^4.5.6",
|
"@aws-amplify/auth": "^4.5.6",
|
||||||
"@aws-amplify/core": "^4.5.6",
|
"@aws-amplify/core": "^4.5.6",
|
||||||
"@djverrall/sveltekit-lambda-adapter": "^0.0.14",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||||
"@nikso/adapter-serverless": "^0.1.0",
|
"@nikso/adapter-serverless": "^0.1.0",
|
||||||
"@sveltejs/adapter-auto": "next",
|
"@sveltejs/adapter-auto": "next",
|
||||||
|
|||||||
Vendored
+1
@@ -18,6 +18,7 @@ declare namespace App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Stuff {
|
interface Stuff {
|
||||||
|
title?: string,
|
||||||
scope?: import("$lib/models/scope").default
|
scope?: import("$lib/models/scope").default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import shortestDecimal from "$lib/utils/numbers";
|
import { significantDecimals } from "$lib/utils/numbers";
|
||||||
|
|
||||||
import PlusMinus from "../layout/PlusMinus.svelte"
|
import PlusMinus from "../layout/PlusMinus.svelte"
|
||||||
|
|
||||||
export let label: string
|
export let label: string
|
||||||
@@ -8,25 +7,17 @@ import shortestDecimal from "$lib/utils/numbers";
|
|||||||
export let target: number = null
|
export let target: number = null
|
||||||
export let right: boolean = false;
|
export let right: boolean = false;
|
||||||
export let dark: boolean = false;
|
export let dark: boolean = false;
|
||||||
|
export let weighted: boolean = false;
|
||||||
|
|
||||||
let valueRounded: number;
|
|
||||||
let targetRounded: number;
|
|
||||||
let valueString: string;
|
let valueString: string;
|
||||||
let targetString: string;
|
$: valueString = !weighted ? Math.round(value).toFixed() : significantDecimals(value);
|
||||||
$: {
|
|
||||||
valueRounded = Math.round(value * 1000) / 1000;
|
|
||||||
|
|
||||||
if (target != null) {
|
|
||||||
targetRounded = Math.round(target * 1000) / 1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class:dark class="value">{shortestDecimal(valueRounded)}×</span>
|
<div class="amount" class:right>
|
||||||
<span class:dark class="value">{valueString}×</span>
|
<span class:dark class="value">{valueString}×</span>
|
||||||
{ #if target != null && targetRounded != valueRounded }
|
<span class="label">{label}</span>
|
||||||
<span class="target">(<PlusMinus offset={valueRounded - targetRounded} />)</span>
|
{ #if target != null && target != value }
|
||||||
<span class="target">(<PlusMinus integersOnly={!weighted} offset={value - target} />)</span>
|
<span class="target">(<PlusMinus integersOnly={!weighted} offset={value - target} />)</span>
|
||||||
{ /if }
|
{ /if }
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { significantDecimals } from "$lib/utils/numbers";
|
||||||
|
|
||||||
export let target = 1;
|
export let target = 1;
|
||||||
export let count = 0;
|
export let count = 0;
|
||||||
export let green = false;
|
export let green = false;
|
||||||
export let alwaysSmooth = false;
|
export let alwaysSmooth = false;
|
||||||
export let percentageOnly = false;
|
export let percentageOnly = false;
|
||||||
|
export let weighted = false;
|
||||||
export let compact = false;
|
export let compact = false;
|
||||||
|
|
||||||
export let fullwidth = false;
|
export let fullwidth = false;
|
||||||
@@ -38,7 +41,11 @@
|
|||||||
{#if percentageOnly}
|
{#if percentageOnly}
|
||||||
<span class="ackreq">({((count / target) * 100).toFixed(0)}%)</span>
|
<span class="ackreq">({((count / target) * 100).toFixed(0)}%)</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="ackreq">({Math.floor(count)} / {Math.ceil(target)})</span>
|
{#if weighted}
|
||||||
|
<span class="ackreq">({significantDecimals(count)} / {significantDecimals(target)})</span>
|
||||||
|
{:else}
|
||||||
|
<span class="ackreq">({Math.round(count)} / {Math.round(target)})</span>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,14 @@
|
|||||||
|
|
||||||
div.markdown :global(p) {
|
div.markdown :global(p) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0.25em 0;
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.markdown :global(code) {
|
div.markdown :global(code) {
|
||||||
|
|||||||
@@ -94,6 +94,10 @@
|
|||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
@media screen and (max-width: 749px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.title {
|
div.title {
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
padding-left: 0
|
padding-left: 0
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
div.modal-body:first-of-type
|
@media screen and (max-width: 749px)
|
||||||
padding-left: 1em
|
padding-left: 1em
|
||||||
|
padding: 0 1em
|
||||||
|
|
||||||
|
&:first-of-type
|
||||||
|
padding-left: 1em
|
||||||
</style>
|
</style>
|
||||||
@@ -67,6 +67,11 @@
|
|||||||
// Mark it non-segmented if the target is too high. This prevents a clunky progress bar,
|
// Mark it non-segmented if the target is too high. This prevents a clunky progress bar,
|
||||||
// or a browser freeze if you set the target very high.
|
// or a browser freeze if you set the target very high.
|
||||||
nonSegmented = alwaysSmooth || (target >= 60);
|
nonSegmented = alwaysSmooth || (target >= 60);
|
||||||
|
|
||||||
|
if (!nonSegmented) {
|
||||||
|
ons = Math.round(ons);
|
||||||
|
offs = Math.round(offs);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
offClass = "none";
|
offClass = "none";
|
||||||
nonSegmented = alwaysSmooth;
|
nonSegmented = alwaysSmooth;
|
||||||
|
|||||||
@@ -16,6 +16,6 @@
|
|||||||
3: "spinner",
|
3: "spinner",
|
||||||
4: "check",
|
4: "check",
|
||||||
5: "times",
|
5: "times",
|
||||||
6: "times",
|
6: "trash",
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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);
|
itemsWritable.set(res.items);
|
||||||
groupsWritable.set(res.groups);
|
groupsWritable.set(res.groups);
|
||||||
} catch(_) {}
|
} catch(_) {}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Requirement } from "$lib/models/project";
|
||||||
|
|
||||||
|
export let value: number;
|
||||||
|
export let requirements: Requirement[];
|
||||||
|
export let disabled: boolean = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select disabled={disabled} bind:value={value}>
|
||||||
|
{#each requirements as req (req.id)}
|
||||||
|
<option value={req.id}>{req.name} ({req.statusName})</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
@@ -57,8 +57,6 @@
|
|||||||
required: requiredMap[s.id],
|
required: requiredMap[s.id],
|
||||||
acquired: acquiredMap[s.id],
|
acquired: acquiredMap[s.id],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$: console.log(value);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="stat-input">
|
<div class="stat-input">
|
||||||
|
|||||||
@@ -1,34 +1,68 @@
|
|||||||
<script lang="ts">
|
<script lang="ts" context="module">
|
||||||
import Status from "$lib/models/status";
|
const STATUS_ENTRIES: [Status, IconName][] = Object.entries(STATUS_ICONS).map(e => [parseInt(e[0]) as Status, e[1]])
|
||||||
|
|
||||||
export let status: Status;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<select class={`sc-${status}`} bind:value={status}>
|
<script lang="ts">
|
||||||
<option class="sc-0" value={Status.Blocked}>Blocked</option>
|
import type Status from "$lib/models/status";
|
||||||
<option class="sc-1" value={Status.Available}>Available</option>
|
import { STATUS_NAMES } from "$lib/models/status";
|
||||||
<option class="sc-2" value={Status.Background}>Background</option>
|
import { STATUS_ICONS } from "../common/StatusIcon.svelte";
|
||||||
<option class="sc-3" value={Status.Active}>Active</option>
|
import Icon, { type IconName } from "../layout/Icon.svelte";
|
||||||
<option class="sc-4" value={Status.Completed}>Completed</option>
|
|
||||||
<option class="sc-5" value={Status.Failed}>Failed</option>
|
export let status: Status;
|
||||||
<option class="sc-6" value={Status.Dropped}>Dropped</option>
|
|
||||||
</select>
|
console.log(STATUS_ENTRIES, status)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="status-select">
|
||||||
|
{#each STATUS_ENTRIES as [id, iconName] (id)}
|
||||||
|
<div on:click={() => {status = id}} class="option sc-{id}" class:selected={status === id}>
|
||||||
|
<Icon name={iconName} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="status-name">{STATUS_NAMES[status]}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
@import "../../css/colors"
|
@import "../../css/colors"
|
||||||
|
|
||||||
.sc-0
|
div.status-select
|
||||||
color: $sc-0 !important
|
background-color: $color-entryhalf
|
||||||
.sc-1
|
display: flex
|
||||||
color: $sc-1 !important
|
flex-direction: row
|
||||||
.sc-2
|
margin-bottom: 0.8em
|
||||||
color: $sc-2 !important
|
margin-top: 0.2em
|
||||||
.sc-3
|
|
||||||
color: $sc-3 !important
|
div.status-name
|
||||||
.sc-4
|
margin-left: auto
|
||||||
color: $sc-4 !important
|
margin-top: 0.275em
|
||||||
.sc-5
|
margin-right: 1ch
|
||||||
color: $sc-5 !important
|
color: $color-entry7
|
||||||
.sc-6
|
|
||||||
color: $sc-6 !important
|
div.option
|
||||||
|
padding: 0.2em 0.5ch
|
||||||
|
font-size: 1.175em
|
||||||
|
color: $color-entry2
|
||||||
|
cursor: pointer
|
||||||
|
line-height: 1em
|
||||||
|
|
||||||
|
&.sc-4, &.sc-2
|
||||||
|
border-left: 1px solid $color-entry1
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: $color-entry3
|
||||||
|
|
||||||
|
&.selected.sc-0
|
||||||
|
color: $sc-0 !important
|
||||||
|
&.selected.sc-1
|
||||||
|
color: $sc-1 !important
|
||||||
|
&.selected.sc-2
|
||||||
|
color: $sc-2 !important
|
||||||
|
&.selected.sc-3
|
||||||
|
color: $sc-3 !important
|
||||||
|
&.selected.sc-4
|
||||||
|
color: $sc-4 !important
|
||||||
|
&.selected.sc-5
|
||||||
|
color: $sc-5 !important
|
||||||
|
&.selected.sc-6
|
||||||
|
color: $sc-6 !important
|
||||||
</style>
|
</style>
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { TimeInterval } from "$lib/models/common";
|
||||||
|
import parseInterval, { datesOf } from "$lib/utils/timeinterval";
|
||||||
import { getItemListContext } from "../contexts/ItemListContext.svelte";
|
import { getItemListContext } from "../contexts/ItemListContext.svelte";
|
||||||
|
import { getTimeContext } from "../contexts/TimeContext.svelte";
|
||||||
import HistoryGroupSection from "./HistoryGroupSection.svelte";
|
import HistoryGroupSection from "./HistoryGroupSection.svelte";
|
||||||
|
|
||||||
|
export let intervalString: string;
|
||||||
|
|
||||||
const {groups, items} = getItemListContext();
|
const {groups, items} = getItemListContext();
|
||||||
|
const {now} = getTimeContext();
|
||||||
|
|
||||||
|
let interval: TimeInterval<string>
|
||||||
|
$: interval = datesOf(parseInterval(intervalString, $now));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each $groups as group (group.label)}
|
{#each $groups as group (group.label)}
|
||||||
<HistoryGroupSection group={group} items={$items} />
|
{#if (group.label >= interval.min && group.label <= interval.max)}
|
||||||
|
<HistoryGroupSection group={group} items={$items} />
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -2,14 +2,19 @@
|
|||||||
import type Item from "$lib/models/item";
|
import type Item from "$lib/models/item";
|
||||||
import type { ItemGroup } from "$lib/models/item";
|
import type { ItemGroup } from "$lib/models/item";
|
||||||
import Section from "../layout/Section.svelte";
|
import Section from "../layout/Section.svelte";
|
||||||
|
import AggregateAmountRow from "../project/AggregateAmountRow.svelte";
|
||||||
import ItemSubSection from "../project/ItemSubSection.svelte";
|
import ItemSubSection from "../project/ItemSubSection.svelte";
|
||||||
|
|
||||||
export let group: ItemGroup;
|
export let group: ItemGroup;
|
||||||
export let items: Item[];
|
export let items: Item[];
|
||||||
|
|
||||||
|
let acquiredItems: Item[];
|
||||||
|
$: acquiredItems = group.list.filter(ref => ref.event === "acquired").map(ref => items[ref.idx]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Section title={group.label}>
|
<Section title={group.label}>
|
||||||
{#each group.list as ref (ref.idx)}
|
{#each group.list as ref (ref.idx)}
|
||||||
<ItemSubSection item={items[ref.idx]} event={ref.event} />
|
<ItemSubSection item={items[ref.idx]} event={ref.event} />
|
||||||
{/each}
|
{/each}
|
||||||
|
<AggregateAmountRow items={acquiredItems} />
|
||||||
</Section>
|
</Section>
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import shortestDecimal from "$lib/utils/numbers";
|
import { significantDecimals } from "$lib/utils/numbers";
|
||||||
|
|
||||||
|
|
||||||
export let offset: number;
|
export let offset: number;
|
||||||
|
export let integersOnly: boolean = false;
|
||||||
|
|
||||||
let value: string;
|
let value: string;
|
||||||
let negative: boolean
|
let negative: boolean;
|
||||||
$: value = (offset > 0) ? `+${shortestDecimal(offset)}` : shortestDecimal(offset);
|
$: {
|
||||||
|
if (integersOnly) {
|
||||||
|
value = (offset > 0) ? `+${Math.round(offset).toFixed()}` : Math.round(offset).toFixed();
|
||||||
|
} else {
|
||||||
|
value = (offset > 0) ? `+${significantDecimals(offset)}` : significantDecimals(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
$: negative = offset < 0
|
$: negative = offset < 0
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<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 = null;
|
||||||
|
export let aggregateRequired: number = null;
|
||||||
|
|
||||||
|
const {scope} = getScopeContext();
|
||||||
|
|
||||||
|
let amounts: {name: string, amount: number}[] = [];
|
||||||
|
let total;
|
||||||
|
let required;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const statMap: Record<number, number> = {};
|
||||||
|
|
||||||
|
total = 0;
|
||||||
|
required = 0;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
for (const stat of item.stats) {
|
||||||
|
statMap[stat.id] = (statMap[stat.id] || 0) + stat.acquired;
|
||||||
|
required += stat.required * stat.weight;
|
||||||
|
total += stat.acquired * stat.weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalAcquired != null) {
|
||||||
|
total = totalAcquired;
|
||||||
|
}
|
||||||
|
if (aggregateRequired != null) {
|
||||||
|
required = aggregateRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 weighted right label="" dark value={total} target={required} />
|
||||||
|
</AmountRow>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
import Icon from "../layout/Icon.svelte";
|
import Icon from "../layout/Icon.svelte";
|
||||||
import { formatTime } from "$lib/utils/date";
|
import { formatTime } from "$lib/utils/date";
|
||||||
import RequirementReference from "./RequirementReference.svelte";
|
import RequirementReference from "./RequirementReference.svelte";
|
||||||
|
import ItemTimeInfo from "./ItemTimeInfo.svelte";
|
||||||
|
|
||||||
export let item: Item;
|
export let item: Item;
|
||||||
export let event: "created" | "scheduled" | "acquired" | "none" = "none";
|
export let event: "created" | "scheduled" | "acquired" | "none" = "none";
|
||||||
@@ -57,6 +58,8 @@
|
|||||||
</OptionsRow>
|
</OptionsRow>
|
||||||
{#if item.project != null}
|
{#if item.project != null}
|
||||||
<RequirementReference project={item.project} requirement={item.requirement} scopeId={item.scopeId} />
|
<RequirementReference project={item.project} requirement={item.requirement} scopeId={item.scopeId} />
|
||||||
|
{:else}
|
||||||
|
<ItemTimeInfo item={item} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if !compact}
|
{#if !compact}
|
||||||
<Markdown source={item.description} />
|
<Markdown source={item.description} />
|
||||||
@@ -70,7 +73,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if item.weightedRequired}
|
{#if item.weightedRequired}
|
||||||
<Amount dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
|
<Amount weighted dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
{#each item.stats as stat (stat.id)}
|
{#each item.stats as stat (stat.id)}
|
||||||
@@ -79,7 +82,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if item.weightedRequired}
|
{#if item.weightedRequired}
|
||||||
<Amount dark right label="" value={item.weightedRequired} />
|
<Amount weighted dark right label="" value={item.weightedRequired} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type Item from "$lib/models/item";
|
||||||
|
import { formatPrettyTime } from "$lib/utils/date";
|
||||||
|
import { getTimeContext } from "../contexts/TimeContext.svelte";
|
||||||
|
|
||||||
|
export let item: Item
|
||||||
|
|
||||||
|
const {now} = getTimeContext();
|
||||||
|
|
||||||
|
let action = "create";
|
||||||
|
let colorClass = "";
|
||||||
|
let timeStr = "";
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const ct = new Date(item.createdTime);
|
||||||
|
let t = ct;
|
||||||
|
|
||||||
|
if (item.acquiredTime != null) {
|
||||||
|
const at = new Date(item.acquiredTime);
|
||||||
|
t = at;
|
||||||
|
|
||||||
|
if (Math.abs(at.getTime() - ct.getTime()) < 300000) {
|
||||||
|
action = "Created/Acquired"
|
||||||
|
} else {
|
||||||
|
action = "Acquired"
|
||||||
|
}
|
||||||
|
colorClass = "acquired"
|
||||||
|
} else if (item.scheduledDate != null) {
|
||||||
|
t = new Date(item.scheduledDate);
|
||||||
|
action = "Scheduled for"
|
||||||
|
colorClass = "scheduled"
|
||||||
|
} else {
|
||||||
|
action = "Created"
|
||||||
|
colorClass = "created"
|
||||||
|
}
|
||||||
|
|
||||||
|
timeStr = formatPrettyTime(t, $now);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="item-time {colorClass}">
|
||||||
|
<span class="action">{action}</span>
|
||||||
|
<span class="timeStr">{timeStr}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@import "../../css/colors"
|
||||||
|
|
||||||
|
div.item-time
|
||||||
|
font-size: 0.8em
|
||||||
|
|
||||||
|
color: $color-entry3
|
||||||
|
|
||||||
|
&.created
|
||||||
|
color: $sc-1-dim
|
||||||
|
&.acquired
|
||||||
|
color: $sc-4-dim
|
||||||
|
&.scheduled
|
||||||
|
color: $sc-0
|
||||||
|
|
||||||
|
> span.timeStr
|
||||||
|
font-weight: 800
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
import ItemEntry from "./ItemSubSection.svelte";
|
import ItemEntry from "./ItemSubSection.svelte";
|
||||||
import Icon from "../layout/Icon.svelte";
|
import Icon from "../layout/Icon.svelte";
|
||||||
import { projectPrettyId } from "$lib/utils/prettyIds";
|
import { projectPrettyId } from "$lib/utils/prettyIds";
|
||||||
|
import AmountRow from "../common/AmountRow.svelte";
|
||||||
|
import AggregateAmountRow from "./AggregateAmountRow.svelte";
|
||||||
|
|
||||||
export let requirement: Requirement;
|
export let requirement: Requirement;
|
||||||
</script>
|
</script>
|
||||||
@@ -35,4 +37,7 @@
|
|||||||
{#each requirement.items as item (item.id)}
|
{#each requirement.items as item (item.id)}
|
||||||
<ItemEntry item={item} />
|
<ItemEntry item={item} />
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if requirement.aggregateRequired > 0}
|
||||||
|
<AggregateAmountRow items={requirement.items} totalAcquired={requirement.totalAcquired} aggregateRequired={requirement.aggregateRequired} />
|
||||||
|
{/if}
|
||||||
</Section>
|
</Section>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
{#if sprint.kind === SprintKind.Items}
|
{#if sprint.kind === SprintKind.Items}
|
||||||
<LabeledProgress timeRange={timeRange} green name={sprint.aggregateName || "Items"} count={sprint.itemsAcquired} target={sprint.itemsRequired} />
|
<LabeledProgress timeRange={timeRange} green name={sprint.aggregateName || "Items"} count={sprint.itemsAcquired} target={sprint.itemsRequired} />
|
||||||
{:else}
|
{:else}
|
||||||
<LabeledProgress timeRange={timeRange} name={sprint.aggregateName} count={sprint.aggregateAcquired} target={sprint.aggregateRequired} />
|
<LabeledProgress timeRange={timeRange} name={sprint.aggregateName} count={sprint.aggregateAcquired} target={sprint.aggregateRequired} weighted={!sprint.isUnweighted} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if !sprint.isCoarse}
|
{#if !sprint.isCoarse}
|
||||||
<LabeledProgressRow>
|
<LabeledProgressRow>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<Markdown source={stat.description} />
|
<Markdown source={stat.description} />
|
||||||
{#if stat.weight !== 1}
|
{#if stat.weight !== 1}
|
||||||
<AmountRow>
|
<AmountRow>
|
||||||
<Amount label="Weight" value={stat.weight} />
|
<Amount weighted label="Weight" value={stat.weight} />
|
||||||
</AmountRow>
|
</AmountRow>
|
||||||
{/if}
|
{/if}
|
||||||
</SubSection>
|
</SubSection>
|
||||||
@@ -25,9 +25,11 @@ $opacity-entry6: 0.80
|
|||||||
$sc-0: #78c9ff
|
$sc-0: #78c9ff
|
||||||
$sc-0-dark: #34424b
|
$sc-0-dark: #34424b
|
||||||
$sc-1: #e7e55e
|
$sc-1: #e7e55e
|
||||||
|
$sc-1-dim: #a3a352
|
||||||
$sc-2: #e7a75e
|
$sc-2: #e7a75e
|
||||||
$sc-3: $color-entry12
|
$sc-3: $color-entry12
|
||||||
$sc-4: #78ff78
|
$sc-4: #78ff78
|
||||||
|
$sc-4-dim: #549454
|
||||||
$sc-4-dark: #2f422f
|
$sc-4-dark: #2f422f
|
||||||
$sc-5: #c94339
|
$sc-5: #c94339
|
||||||
$sc-6: #e75ed0
|
$sc-6: #e75ed0
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import { getScopeListContext } from "$lib/components/contexts/ScopeListContext.svelte";
|
import { getScopeListContext } from "$lib/components/contexts/ScopeListContext.svelte";
|
||||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||||
import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte";
|
import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte";
|
||||||
|
import RequirementSelect from "$lib/components/controls/RequirementSelect.svelte";
|
||||||
import StatInput from "$lib/components/controls/StatInput.svelte";
|
import StatInput from "$lib/components/controls/StatInput.svelte";
|
||||||
import type Item from "$lib/models/item";
|
import type Item from "$lib/models/item";
|
||||||
import type { ItemInput } from "$lib/models/item";
|
import type { ItemInput } from "$lib/models/item";
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
|
|
||||||
op = "Edit"
|
op = "Edit"
|
||||||
openedDate = new Date();
|
openedDate = new Date();
|
||||||
requirementName = req?.name;
|
requirementName = current.requirement?.name;
|
||||||
show = true;
|
show = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +186,11 @@
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
{#if requirementName != null}
|
{#if requirementName != null}
|
||||||
<label for="req">Project Requirement</label>
|
<label for="req">Project Requirement</label>
|
||||||
<input disabled name="req" type="text" value={requirementName} />
|
{#if $project.id != null}
|
||||||
|
<RequirementSelect disabled={op === "Create"} requirements={$project.requirements} bind:value={item.requirementId} />
|
||||||
|
{:else}
|
||||||
|
<input disabled name="req" type="text" value={requirementName} />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<label for="stats">Stats</label>
|
<label for="stats">Stats</label>
|
||||||
<StatInput
|
<StatInput
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { statDiff } from "$lib/utils/stat";
|
|||||||
let projectId: number
|
let projectId: number
|
||||||
let requirementId: number
|
let requirementId: number
|
||||||
let oldStats: RequirementInput["stats"]
|
let oldStats: RequirementInput["stats"]
|
||||||
|
let showAggregateRequired: boolean
|
||||||
|
|
||||||
let error: string
|
let error: string
|
||||||
let loading: boolean
|
let loading: boolean
|
||||||
@@ -44,6 +45,8 @@ import { statDiff } from "$lib/utils/stat";
|
|||||||
show = false;
|
show = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: showAggregateRequired = requirement?.stats?.find(s => s.required > 0) == null;
|
||||||
|
|
||||||
function initCreate(project: ProjectEntry) {
|
function initCreate(project: ProjectEntry) {
|
||||||
requirement = {
|
requirement = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -51,6 +54,7 @@ import { statDiff } from "$lib/utils/stat";
|
|||||||
stats: [],
|
stats: [],
|
||||||
status: Status.Available,
|
status: Status.Available,
|
||||||
isCoarse: false,
|
isCoarse: false,
|
||||||
|
aggregateRequired: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
projectId = project.id;
|
projectId = project.id;
|
||||||
@@ -64,6 +68,7 @@ import { statDiff } from "$lib/utils/stat";
|
|||||||
status: current.status,
|
status: current.status,
|
||||||
stats: current.stats.map(s => ({acquired: s.acquired, required: s.required, statId: s.id})),
|
stats: current.stats.map(s => ({acquired: s.acquired, required: s.required, statId: s.id})),
|
||||||
isCoarse: current.isCoarse,
|
isCoarse: current.isCoarse,
|
||||||
|
aggregateRequired: current.aggregateRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
oldStats = [...requirement.stats];
|
oldStats = [...requirement.stats];
|
||||||
@@ -76,6 +81,10 @@ import { statDiff } from "$lib/utils/stat";
|
|||||||
error = null;
|
error = null;
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
|
if (requirement.stats.find(s => s.required > 0)) {
|
||||||
|
requirement.aggregateRequired = 0;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case "Create":
|
case "Create":
|
||||||
@@ -115,6 +124,10 @@ import { statDiff } from "$lib/utils/stat";
|
|||||||
<input name="name" type="text" bind:value={requirement.name} />
|
<input name="name" type="text" bind:value={requirement.name} />
|
||||||
<label for="description">Description</label>
|
<label for="description">Description</label>
|
||||||
<textarea name="description" bind:value={requirement.description} />
|
<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>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<label for="stats">Status</label>
|
<label for="stats">Status</label>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface Requirement {
|
|||||||
totalRequired: number
|
totalRequired: number
|
||||||
totalPlanned: number
|
totalPlanned: number
|
||||||
isCoarse: boolean
|
isCoarse: boolean
|
||||||
|
aggregateRequired: number
|
||||||
stats: StatProgressWithPlanned[]
|
stats: StatProgressWithPlanned[]
|
||||||
items: Item[]
|
items: Item[]
|
||||||
}
|
}
|
||||||
@@ -47,5 +48,6 @@ export interface RequirementInput {
|
|||||||
description: string
|
description: string
|
||||||
status: Status
|
status: Status
|
||||||
isCoarse: boolean
|
isCoarse: boolean
|
||||||
|
aggregateRequired: number
|
||||||
stats: StatValueInput[]
|
stats: StatValueInput[]
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,14 @@ enum Status {
|
|||||||
Dropped,
|
Dropped,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const STATUS_NAMES: Record<Status, string> = {
|
||||||
|
[Status.Blocked]: "Blocked",
|
||||||
|
[Status.Available]: "Available",
|
||||||
|
[Status.Background]: "Background",
|
||||||
|
[Status.Active]: "Active",
|
||||||
|
[Status.Completed]: "Completed",
|
||||||
|
[Status.Failed]: "Failed",
|
||||||
|
[Status.Dropped]: "Dropped",
|
||||||
|
}
|
||||||
|
|
||||||
export default Status;
|
export default Status;
|
||||||
@@ -114,7 +114,7 @@ export function endOfDay(now: Date): Date {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function monthName(date: Date): string {
|
export function monthName(date: Date): string {
|
||||||
return monthNames[date.getMonth()]
|
return monthNames[date.getMonth() - 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addDays(date: Date, days: number): Date {
|
export function addDays(date: Date, days: number): Date {
|
||||||
@@ -133,4 +133,24 @@ export function addYears(date: Date, years: number): Date {
|
|||||||
const newDate = new Date(date.getTime());
|
const newDate = new Date(date.getTime());
|
||||||
newDate.setFullYear(newDate.getFullYear() + years);
|
newDate.setFullYear(newDate.getFullYear() + years);
|
||||||
return newDate;
|
return newDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPrettyTime(date: Date, now: Date) {
|
||||||
|
const d = startOfDay(date);
|
||||||
|
const n = startOfDay(now);
|
||||||
|
|
||||||
|
const diff = (d.getTime() - n.getTime()) / (86400000);
|
||||||
|
|
||||||
|
const hhmm = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
|
if (diff === 0) {
|
||||||
|
return `today at ${hhmm}`;
|
||||||
|
} else if (diff === 1) {
|
||||||
|
return `tomorrow at ${hhmm}`;
|
||||||
|
} else if (diff === -1) {
|
||||||
|
return `yesterday at ${hhmm}`;
|
||||||
|
} else if (diff > -3 && diff < 6) {
|
||||||
|
return `${weekDay[date.getDay()]} at ${hhmm}`;
|
||||||
|
} else {
|
||||||
|
return `${monthName(date)} ${date.getDate()} at ${hhmm}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,20 @@ export default function shortestDecimal(n: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function significantDecimals(n: number): string {
|
||||||
|
if (n >= 99.95) {
|
||||||
|
return n.toFixed();
|
||||||
|
} else if (n >= 9.95) {
|
||||||
|
return n.toFixed(1);
|
||||||
|
} else if (n >= 0.01) {
|
||||||
|
return n.toFixed(2);
|
||||||
|
} else if (n >= 0.001) {
|
||||||
|
return n.toFixed(3);
|
||||||
|
} else if (n >= 0) {
|
||||||
|
return n.toFixed(2);
|
||||||
|
} else {
|
||||||
|
return "-" + significantDecimals(-n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
$: hideMobile = $page.url.pathname !== `/${scopePrettyId(scope)}`
|
$: hideMobile = $page.url.pathname !== `/${scopePrettyId(scope)}`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head><title>{scope.abbreviation}: {$page.stuff.title} – Stufflog 3</title></svelte:head>
|
||||||
|
|
||||||
<ScopeContext scope={scope}>
|
<ScopeContext scope={scope}>
|
||||||
<ProjectListContext projects={projects}>
|
<ProjectListContext projects={projects}>
|
||||||
<Columns wide>
|
<Columns wide>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { Load } from "@sveltejs/kit/types/internal";
|
import type { Load } from "@sveltejs/kit/types/internal";
|
||||||
|
|
||||||
import { sl3 } from "$lib/clients/sl3";
|
import { sl3 } from "$lib/clients/sl3";
|
||||||
import parseInterval, { datesOf, intervalLabel } from "$lib/utils/timeinterval";
|
import parseInterval, { datesOf } from "$lib/utils/timeinterval";
|
||||||
import type Item from "$lib/models/item";
|
import type Item from "$lib/models/item";
|
||||||
import type { ItemFilter, ItemGroup } from "$lib/models/item";
|
import type { ItemFilter, ItemGroup } from "$lib/models/item";
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
const {items, groups} = await sl3(fetch).listItems(scopeId, filter, true, session.groupOptions);
|
const {items, groups} = await sl3(fetch).listItems(scopeId, filter, true, session.groupOptions);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
stuff: { title: "History" },
|
||||||
props: {
|
props: {
|
||||||
items, groups, filter,
|
items, groups, filter,
|
||||||
intervalString: params.interval,
|
intervalString: params.interval,
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
<ToggleIcon inverse name="calendar" bind:value={$session.groupOptions.hideScheduled} on:click={triggerReload} />
|
<ToggleIcon inverse name="calendar" bind:value={$session.groupOptions.hideScheduled} on:click={triggerReload} />
|
||||||
{/if}
|
{/if}
|
||||||
</HistorySelector>
|
</HistorySelector>
|
||||||
<HistoryGroupList />
|
<HistoryGroupList intervalString={intervalString} />
|
||||||
</Main>
|
</Main>
|
||||||
|
|
||||||
<ItemCreateModal />
|
<ItemCreateModal />
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
const sprints = await sl3(fetch).listSprints(scopeId);
|
const sprints = await sl3(fetch).listSprints(scopeId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
stuff: { title: "Overview" },
|
||||||
props: {scheduledItems, acquiredItems, looseItems, sprints}
|
props: {scheduledItems, acquiredItems, looseItems, sprints}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
const project = await sl3(fetch).findProject(scopeId, projectId);
|
const project = await sl3(fetch).findProject(scopeId, projectId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
stuff: { title: project.name },
|
||||||
props: {project}
|
props: {project}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
const sprints = await sl3(fetch).listSprints(scopeId, interval);
|
const sprints = await sl3(fetch).listSprints(scopeId, interval);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
stuff: {
|
||||||
|
title: "Sprints",
|
||||||
|
},
|
||||||
|
props: {
|
||||||
sprints,
|
sprints,
|
||||||
intervalString: params.interval,
|
intervalString: params.interval,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,6 +71,10 @@
|
|||||||
|
|
||||||
<Header fullwidth wide subtitle="Logging your stuff">Stufflog</Header>
|
<Header fullwidth wide subtitle="Logging your stuff">Stufflog</Header>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Stufflog 3</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<ItemMultiListContext global lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
|
<ItemMultiListContext global lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
|
||||||
<ScopeListContext scopes={scopes}>
|
<ScopeListContext scopes={scopes}>
|
||||||
<SprintListContext global sprints={sprints} intervalString="">
|
<SprintListContext global sprints={sprints} intervalString="">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ require (
|
|||||||
github.com/awslabs/aws-lambda-go-api-proxy v0.13.3
|
github.com/awslabs/aws-lambda-go-api-proxy v0.13.3
|
||||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
|
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
|
||||||
github.com/gin-gonic/gin v1.7.7
|
github.com/gin-gonic/gin v1.7.7
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/lestrrat-go/jwx v1.2.25
|
github.com/lestrrat-go/jwx v1.2.25
|
||||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
|
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
|
||||||
|
|||||||
+5
-4
@@ -8,8 +8,9 @@ type ProjectUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RequirementUpdate struct {
|
type RequirementUpdate struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Status *Status `json:"status"`
|
Status *Status `json:"status"`
|
||||||
IsCoarse *bool `json:"isCoarse"`
|
IsCoarse *bool `json:"isCoarse"`
|
||||||
|
AggregateRequired *int `json:"aggregateRequired"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,13 +41,14 @@ type Project struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProjectRequirement struct {
|
type ProjectRequirement struct {
|
||||||
ID int
|
ID int
|
||||||
ScopeID int
|
ScopeID int
|
||||||
ProjectID int
|
ProjectID int
|
||||||
Name string
|
Name string
|
||||||
Status int
|
Status int
|
||||||
Description string
|
Description string
|
||||||
IsCoarse bool
|
IsCoarse bool
|
||||||
|
AggregateRequired int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectRequirementStat struct {
|
type ProjectRequirementStat struct {
|
||||||
|
|||||||
@@ -156,17 +156,18 @@ func (q *Queries) InsertProject(ctx context.Context, arg InsertProjectParams) (s
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertProjectRequirement = `-- name: InsertProjectRequirement :execresult
|
const insertProjectRequirement = `-- name: InsertProjectRequirement :execresult
|
||||||
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse)
|
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse, aggregate_required)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertProjectRequirementParams struct {
|
type InsertProjectRequirementParams struct {
|
||||||
ScopeID int
|
ScopeID int
|
||||||
ProjectID int
|
ProjectID int
|
||||||
Name string
|
Name string
|
||||||
Status int
|
Status int
|
||||||
Description string
|
Description string
|
||||||
IsCoarse bool
|
IsCoarse bool
|
||||||
|
AggregateRequired int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertProjectRequirement(ctx context.Context, arg InsertProjectRequirementParams) (sql.Result, error) {
|
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.Status,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
arg.IsCoarse,
|
arg.IsCoarse,
|
||||||
|
arg.AggregateRequired,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const listProjectRequirements = `-- name: ListProjectRequirements :many
|
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) {
|
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.Status,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.IsCoarse,
|
&i.IsCoarse,
|
||||||
|
&i.AggregateRequired,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -330,17 +333,19 @@ UPDATE project_requirement
|
|||||||
SET name = ?,
|
SET name = ?,
|
||||||
status = ?,
|
status = ?,
|
||||||
description = ?,
|
description = ?,
|
||||||
is_coarse = ?
|
is_coarse = ?,
|
||||||
|
aggregate_required = ?
|
||||||
WHERE id = ? AND scope_id = ?
|
WHERE id = ? AND scope_id = ?
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateProjectRequirementParams struct {
|
type UpdateProjectRequirementParams struct {
|
||||||
Name string
|
Name string
|
||||||
Status int
|
Status int
|
||||||
Description string
|
Description string
|
||||||
IsCoarse bool
|
IsCoarse bool
|
||||||
ID int
|
AggregateRequired int
|
||||||
ScopeID int
|
ID int
|
||||||
|
ScopeID int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateProjectRequirement(ctx context.Context, arg UpdateProjectRequirementParams) error {
|
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.Status,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
arg.IsCoarse,
|
arg.IsCoarse,
|
||||||
|
arg.AggregateRequired,
|
||||||
arg.ID,
|
arg.ID,
|
||||||
arg.ScopeID,
|
arg.ScopeID,
|
||||||
)
|
)
|
||||||
|
|||||||
+24
-20
@@ -194,7 +194,7 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
|
|||||||
return []entities.Requirement{}, []entities.RequirementStat{}, nil
|
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").
|
From("project_requirement").
|
||||||
Where(squirrel.Eq{"id": requirementIDs})
|
Where(squirrel.Eq{"id": requirementIDs})
|
||||||
if scopeID != -1 {
|
if scopeID != -1 {
|
||||||
@@ -221,6 +221,7 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
|
|||||||
&requirement.Status,
|
&requirement.Status,
|
||||||
&requirement.Description,
|
&requirement.Description,
|
||||||
&requirement.IsCoarse,
|
&requirement.IsCoarse,
|
||||||
|
&requirement.AggregateRequired,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -271,13 +272,14 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int)
|
|||||||
requirements := make([]entities.Requirement, 0, len(reqRows))
|
requirements := make([]entities.Requirement, 0, len(reqRows))
|
||||||
for _, row := range reqRows {
|
for _, row := range reqRows {
|
||||||
requirements = append(requirements, entities.Requirement{
|
requirements = append(requirements, entities.Requirement{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
ScopeID: row.ScopeID,
|
ScopeID: row.ScopeID,
|
||||||
ProjectID: row.ProjectID,
|
ProjectID: row.ProjectID,
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
Description: row.Description,
|
Description: row.Description,
|
||||||
IsCoarse: row.IsCoarse,
|
IsCoarse: row.IsCoarse,
|
||||||
Status: models.Status(row.Status),
|
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) {
|
func (r *projectRepository) CreateRequirement(ctx context.Context, requirement entities.Requirement) (*entities.Requirement, error) {
|
||||||
res, err := r.q.InsertProjectRequirement(ctx, mysqlcore.InsertProjectRequirementParams{
|
res, err := r.q.InsertProjectRequirement(ctx, mysqlcore.InsertProjectRequirementParams{
|
||||||
ScopeID: requirement.ScopeID,
|
ScopeID: requirement.ScopeID,
|
||||||
ProjectID: requirement.ProjectID,
|
ProjectID: requirement.ProjectID,
|
||||||
Name: requirement.Name,
|
Name: requirement.Name,
|
||||||
Status: int(requirement.Status),
|
Status: int(requirement.Status),
|
||||||
Description: requirement.Description,
|
Description: requirement.Description,
|
||||||
IsCoarse: requirement.IsCoarse,
|
IsCoarse: requirement.IsCoarse,
|
||||||
|
AggregateRequired: requirement.AggregateRequired,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -319,12 +322,13 @@ func (r *projectRepository) UpdateRequirement(ctx context.Context, requirement e
|
|||||||
requirement.Update(update)
|
requirement.Update(update)
|
||||||
|
|
||||||
return r.q.UpdateProjectRequirement(ctx, mysqlcore.UpdateProjectRequirementParams{
|
return r.q.UpdateProjectRequirement(ctx, mysqlcore.UpdateProjectRequirementParams{
|
||||||
Name: requirement.Name,
|
Name: requirement.Name,
|
||||||
Status: int(requirement.Status),
|
Status: int(requirement.Status),
|
||||||
Description: requirement.Description,
|
Description: requirement.Description,
|
||||||
IsCoarse: requirement.IsCoarse,
|
IsCoarse: requirement.IsCoarse,
|
||||||
ID: requirement.ID,
|
ID: requirement.ID,
|
||||||
ScopeID: requirement.ScopeID,
|
ScopeID: requirement.ScopeID,
|
||||||
|
AggregateRequired: requirement.AggregateRequired,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,15 +23,16 @@ DELETE FROM project WHERE id = ? AND scope_id = ?;
|
|||||||
SELECT * FROM project_requirement WHERE project_id = ?;
|
SELECT * FROM project_requirement WHERE project_id = ?;
|
||||||
|
|
||||||
-- name: InsertProjectRequirement :execresult
|
-- name: InsertProjectRequirement :execresult
|
||||||
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse)
|
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse, aggregate_required)
|
||||||
VALUES (?, ?, ?, ?, ?, ?);
|
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
-- name: UpdateProjectRequirement :exec
|
-- name: UpdateProjectRequirement :exec
|
||||||
UPDATE project_requirement
|
UPDATE project_requirement
|
||||||
SET name = ?,
|
SET name = ?,
|
||||||
status = ?,
|
status = ?,
|
||||||
description = ?,
|
description = ?,
|
||||||
is_coarse = ?
|
is_coarse = ?,
|
||||||
|
aggregate_required = ?
|
||||||
WHERE id = ? AND scope_id = ?;
|
WHERE id = ? AND scope_id = ?;
|
||||||
|
|
||||||
-- name: DeleteProjectRequirement :exec
|
-- name: DeleteProjectRequirement :exec
|
||||||
|
|||||||
@@ -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
|
||||||
+25
-19
@@ -53,17 +53,18 @@ func (r *Result) Requirement(id int) *RequirementResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RequirementResult struct {
|
type RequirementResult struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Status models.Status `json:"status"`
|
Status models.Status `json:"status"`
|
||||||
StatusName string `json:"statusName"`
|
StatusName string `json:"statusName"`
|
||||||
TotalAcquired float64 `json:"totalAcquired"`
|
TotalAcquired float64 `json:"totalAcquired"`
|
||||||
TotalRequired float64 `json:"totalRequired"`
|
TotalRequired float64 `json:"totalRequired"`
|
||||||
TotalPlanned float64 `json:"totalPlanned"`
|
TotalPlanned float64 `json:"totalPlanned"`
|
||||||
IsCoarse bool `json:"isCoarse"`
|
IsCoarse bool `json:"isCoarse"`
|
||||||
Stats []RequirementResultStat `json:"stats"`
|
AggregateRequired int `json:"aggregateRequired"`
|
||||||
Items []items.Result `json:"items"`
|
Stats []RequirementResultStat `json:"stats"`
|
||||||
|
Items []items.Result `json:"items"`
|
||||||
|
|
||||||
Requirement entities.Requirement `json:"-"`
|
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 {
|
func generateRequirementResult(req entities.Requirement, scope scopes.Result, requirementStats []entities.RequirementStat, projectItems []items.Result) RequirementResult {
|
||||||
resReq := RequirementResult{
|
resReq := RequirementResult{
|
||||||
ID: req.ID,
|
ID: req.ID,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
StatusName: scope.StatusName(req.Status),
|
StatusName: scope.StatusName(req.Status),
|
||||||
IsCoarse: req.IsCoarse,
|
IsCoarse: req.IsCoarse,
|
||||||
Stats: make([]RequirementResultStat, 0, 8),
|
AggregateRequired: req.AggregateRequired,
|
||||||
Items: make([]items.Result, 0, 8),
|
Stats: make([]RequirementResultStat, 0, 8),
|
||||||
|
Items: make([]items.Result, 0, 8),
|
||||||
|
|
||||||
Requirement: req,
|
Requirement: req,
|
||||||
}
|
}
|
||||||
@@ -271,6 +273,10 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
|
|||||||
resReq.TotalRequired += item.WeightedRequired
|
resReq.TotalRequired += item.WeightedRequired
|
||||||
}
|
}
|
||||||
resReq.TotalPlanned = resReq.TotalRequired
|
resReq.TotalPlanned = resReq.TotalRequired
|
||||||
|
|
||||||
|
if req.AggregateRequired > 0 {
|
||||||
|
resReq.TotalRequired = float64(req.AggregateRequired)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resReq
|
return resReq
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package sprints
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BurndownDataPoint struct {
|
||||||
|
Date models.Date `json:"date"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type burndownGenerator struct {
|
||||||
|
Points []BurndownDataPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *burndownGenerator) Add(t time.Time, v float64) {
|
||||||
|
t = t.In(tz).Add(-time.Minute * 300)
|
||||||
|
yyyy, mm, dd := t.Date()
|
||||||
|
date := models.Date{yyyy, int(mm), dd}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
insertIndex := -1
|
||||||
|
for i, p := range g.Points {
|
||||||
|
if date == p.Date {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
} else if date.Before(p.Date) {
|
||||||
|
insertIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
if insertIndex == -1 {
|
||||||
|
value := 0.0
|
||||||
|
if len(g.Points) > 0 {
|
||||||
|
value = g.Points[len(g.Points)-1].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Points = append(g.Points, BurndownDataPoint{
|
||||||
|
Date: date,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
g.Points = append(g.Points[:insertIndex+1], g.Points[insertIndex:]...)
|
||||||
|
g.Points[insertIndex].Date = date
|
||||||
|
if insertIndex > 0 {
|
||||||
|
g.Points[insertIndex].Value = g.Points[insertIndex-1].Value
|
||||||
|
} else {
|
||||||
|
g.Points[insertIndex].Value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range g.Points {
|
||||||
|
if !p.Date.Before(date) {
|
||||||
|
g.Points[i].Value += v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tz *time.Location
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
tz, err = time.LoadLocation("Europe/Oslo")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package sprints
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||||
|
"github.com/go-playground/assert/v2"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBurndownGenerator_Add(t *testing.T) {
|
||||||
|
bg := burndownGenerator{}
|
||||||
|
|
||||||
|
// This is too early on the second, and counts as the first.
|
||||||
|
bg.Add(td(3, 2, 20), 14)
|
||||||
|
assert.Equal(t, bg.Points, []BurndownDataPoint{
|
||||||
|
{Date: d(2), Value: 14},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add some more data points in order
|
||||||
|
bg.Add(td(3, 14, 11), 4)
|
||||||
|
bg.Add(td(4, 6, 44), 3)
|
||||||
|
bg.Add(td(21, 11, 24), 20)
|
||||||
|
assert.Equal(t, bg.Points, []BurndownDataPoint{
|
||||||
|
{Date: d(2), Value: 14},
|
||||||
|
{Date: d(3), Value: 18},
|
||||||
|
{Date: d(4), Value: 21},
|
||||||
|
{Date: d(21), Value: 41},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add ones out of order.
|
||||||
|
bg.Add(td(7, 21, 12), 2)
|
||||||
|
bg.Add(td(2, 15, 22), 1)
|
||||||
|
assert.Equal(t, bg.Points, []BurndownDataPoint{
|
||||||
|
{Date: d(2), Value: 15},
|
||||||
|
{Date: d(3), Value: 19},
|
||||||
|
{Date: d(4), Value: 22},
|
||||||
|
{Date: d(7), Value: 24},
|
||||||
|
{Date: d(21), Value: 44},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add one at the start
|
||||||
|
bg.Add(td(1, 11, 16), 1)
|
||||||
|
assert.Equal(t, bg.Points, []BurndownDataPoint{
|
||||||
|
{Date: d(1), Value: 1},
|
||||||
|
{Date: d(2), Value: 16},
|
||||||
|
{Date: d(3), Value: 20},
|
||||||
|
{Date: d(4), Value: 23},
|
||||||
|
{Date: d(7), Value: 25},
|
||||||
|
{Date: d(21), Value: 45},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add one more in the middle for good measure
|
||||||
|
bg.Add(td(5, 14, 33), 10)
|
||||||
|
assert.Equal(t, bg.Points, []BurndownDataPoint{
|
||||||
|
{Date: d(1), Value: 1},
|
||||||
|
{Date: d(2), Value: 16},
|
||||||
|
{Date: d(3), Value: 20},
|
||||||
|
{Date: d(4), Value: 23},
|
||||||
|
{Date: d(5), Value: 33},
|
||||||
|
{Date: d(7), Value: 35},
|
||||||
|
{Date: d(21), Value: 55},
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func d(dd int) models.Date {
|
||||||
|
return models.Date{2022, 7, dd}
|
||||||
|
}
|
||||||
|
|
||||||
|
func td(dd, hh, mm int) time.Time {
|
||||||
|
return time.Date(2022, time.July, dd, hh, mm, 17, 0, time.UTC)
|
||||||
|
}
|
||||||
@@ -9,18 +9,20 @@ import (
|
|||||||
type Result struct {
|
type Result struct {
|
||||||
entities.Sprint
|
entities.Sprint
|
||||||
|
|
||||||
AggregateAcquired int `json:"aggregateAcquired"`
|
AggregateAcquired float64 `json:"aggregateAcquired"`
|
||||||
AggregatePlanned int `json:"aggregatePlanned"`
|
AggregatePlanned float64 `json:"aggregatePlanned"`
|
||||||
|
|
||||||
ItemsAcquired *int `json:"itemsAcquired,omitempty"`
|
ItemsAcquired *int `json:"itemsAcquired,omitempty"`
|
||||||
ItemsRequired *int `json:"itemsRequired,omitempty"`
|
ItemsRequired *int `json:"itemsRequired,omitempty"`
|
||||||
|
|
||||||
PartIDs []int `json:"partIds"` // Just for simplifying code.
|
PartIDs []int `json:"partIds"` // Just for simplifying code.
|
||||||
|
|
||||||
Items []items.Result `json:"items,omitempty"`
|
Items []items.Result `json:"items,omitempty"`
|
||||||
Stats []entities.Stat `json:"stats,omitempty"`
|
Stats []entities.Stat `json:"stats,omitempty"`
|
||||||
Requirements []projects.RequirementResult `json:"requirements,omitempty"`
|
Requirements []projects.RequirementResult `json:"requirements,omitempty"`
|
||||||
Progress []ResultProgress `json:"progress,omitempty"`
|
Progress []ResultProgress `json:"progress,omitempty"`
|
||||||
|
ItemBurndown []BurndownDataPoint `json:"itemBurndown,omitempty"`
|
||||||
|
AggregateBurndown []BurndownDataPoint `json:"aggregateBurndown,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResultProgress struct {
|
type ResultProgress struct {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"git.aiterp.net/stufflog3/stufflog3/usecases/projects"
|
"git.aiterp.net/stufflog3/stufflog3/usecases/projects"
|
||||||
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"math"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -85,6 +84,7 @@ func (s *Service) ListScopedBetween(ctx context.Context, from, to time.Time) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) fillMany(ctx context.Context, sprints []entities.Sprint) ([]Result, error) {
|
func (s *Service) fillMany(ctx context.Context, sprints []entities.Sprint) ([]Result, error) {
|
||||||
|
now := time.Now()
|
||||||
sort.Slice(sprints, func(i, j int) bool {
|
sort.Slice(sprints, func(i, j int) bool {
|
||||||
si := sprints[i]
|
si := sprints[i]
|
||||||
sj := sprints[j]
|
sj := sprints[j]
|
||||||
@@ -93,6 +93,12 @@ func (s *Service) fillMany(ctx context.Context, sprints []entities.Sprint) ([]Re
|
|||||||
return si.IsTimed
|
return si.IsTimed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
siActive := si.ToTime.After(now)
|
||||||
|
sjActive := sj.ToTime.After(now)
|
||||||
|
if sjActive != siActive {
|
||||||
|
return siActive
|
||||||
|
}
|
||||||
|
|
||||||
if !si.FromTime.Equal(sj.FromTime) {
|
if !si.FromTime.Equal(sj.FromTime) {
|
||||||
return si.FromTime.Before(sj.FromTime)
|
return si.FromTime.Before(sj.FromTime)
|
||||||
}
|
}
|
||||||
@@ -550,17 +556,32 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Measure progress
|
// Measure progress
|
||||||
|
itemBurndown := burndownGenerator{}
|
||||||
|
aggregateBurndown := burndownGenerator{}
|
||||||
for _, item := range res.Items {
|
for _, item := range res.Items {
|
||||||
if !item.AcquiredBetween(sprint.FromTime, sprint.ToTime) {
|
if !item.AcquiredBetween(sprint.FromTime, sprint.ToTime) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemBurndown.Add(*item.AcquiredTime, 1)
|
||||||
|
|
||||||
|
itemAggregate := 0.0
|
||||||
for _, stat := range item.Stats {
|
for _, stat := range item.Stats {
|
||||||
if pi, ok := progressMap[stat.ID]; ok {
|
if pi, ok := progressMap[stat.ID]; ok {
|
||||||
res.Progress[pi].Acquired += stat.Acquired
|
res.Progress[pi].Acquired += stat.Acquired
|
||||||
|
|
||||||
|
if res.IsUnweighted {
|
||||||
|
itemAggregate += float64(stat.Acquired)
|
||||||
|
} else {
|
||||||
|
itemAggregate += float64(stat.Acquired) * stat.Weight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aggregateBurndown.Add(*item.AcquiredTime, itemAggregate)
|
||||||
}
|
}
|
||||||
|
res.AggregateBurndown = aggregateBurndown.Points
|
||||||
|
res.ItemBurndown = itemBurndown.Points
|
||||||
|
|
||||||
// For requirement sprints, remove items array after enumeration
|
// For requirement sprints, remove items array after enumeration
|
||||||
if res.Kind == models.SprintKindRequirements {
|
if res.Kind == models.SprintKindRequirements {
|
||||||
@@ -583,11 +604,11 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.AggregateAcquired = int(math.Round(aggregateAcquired))
|
res.AggregateAcquired = aggregateAcquired
|
||||||
res.AggregatePlanned = int(math.Round(aggregatePlanned))
|
res.AggregatePlanned = aggregatePlanned
|
||||||
|
|
||||||
if res.AggregateRequired < 0 {
|
if res.AggregateRequired < 0 {
|
||||||
res.AggregateRequired = res.AggregatePlanned
|
res.AggregateRequired = int(res.AggregatePlanned)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user