Compare commits

..
11 Commits
Author SHA1 Message Date
gisle d083c9b1e8 fix sprint viewing.
continuous-integration/drone/push Build is passing
2022-07-03 19:16:10 +02:00
gisle 0e9b1a4930 tweaked header icons and such to better accomadate japanese text.
continuous-integration/drone/push Build is passing
2022-07-03 19:07:09 +02:00
gisle 6bbbf33c83 make weighted totals render better. 2022-07-03 18:57:02 +02:00
gisle ad53ec5c7e add total weighted for items. 2022-07-03 18:50:44 +02:00
gisle 1195f6ab03 add sorting to project requirements.
continuous-integration/drone/push Build is passing
2022-07-03 17:36:46 +02:00
gisle 2bd68930fe ...and make it somewhat persistent. 2022-07-03 17:28:45 +02:00
gisle 0ef4e38b8a add filtering on history item groups, though it is non-persistent.
continuous-integration/drone/push Build is passing
2022-07-03 17:24:13 +02:00
gisle 48b5c17166 change default stat behavior on item create to check all >0 req stats in requirement.
continuous-integration/drone/push Build is passing
2022-07-03 15:51:28 +02:00
gisle 2fcb6a5d1a allow checking scope/requirement stats on item edits. 2022-07-03 15:45:23 +02:00
gisle 0dfda26c09 fix project progress bars.
continuous-integration/drone/push Build is passing
2022-07-03 15:04:30 +02:00
gisle 701932546d geesed the utf8.
continuous-integration/drone/push Build is passing
2022-07-02 18:57:18 +02:00
30 changed files with 327 additions and 91 deletions
+1
View File
@@ -13,6 +13,7 @@ declare namespace App {
user?: import("$lib/models/user").default user?: import("$lib/models/user").default
idToken?: string idToken?: string
req?: any req?: any
groupOptions?: import("$lib/utils/items").GroupItemsOptions
amplifyConfig: any amplifyConfig: any
} }
@@ -1,16 +1,32 @@
<script lang="ts"> <script lang="ts">
import PlusMinus from "../layout/PlusMinus.svelte" import shortestDecimal from "$lib/utils/numbers";
import PlusMinus from "../layout/PlusMinus.svelte"
export let label: string export let label: string
export let value: number export let value: number
export let target: number = null export let target: number = null
export let right: boolean = false;
export let dark: boolean = false;
let valueRounded: number;
let targetRounded: number;
let valueString: string;
let targetString: string;
$: {
valueRounded = Math.round(value * 1000) / 1000;
if (target != null) {
targetRounded = Math.round(target * 1000) / 1000;
}
}
</script> </script>
<div class="amount"> <div class="amount" class:right>
<span class="value">{value}&times;</span> <span class:dark class="value">{shortestDecimal(valueRounded)}&times;</span>
<span class="label">{label}</span> <span class="label">{label}</span>
{ #if target != null && target != value } { #if target != null && targetRounded != valueRounded }
<span class="target">(<PlusMinus offset={value - target} />)</span> <span class="target">(<PlusMinus offset={valueRounded - targetRounded} />)</span>
{ /if } { /if }
</div> </div>
@@ -19,9 +35,14 @@ import PlusMinus from "../layout/PlusMinus.svelte"
div.amount { div.amount {
padding-right: 1ch; padding-right: 1ch;
&.right {
padding-right: 0;
margin-left: auto;
}
} }
span.label { span.label, span.dark {
opacity: $opacity-entry4; opacity: $opacity-entry4;
} }
@@ -8,24 +8,26 @@
<div class="par">(</div> <div class="par">(</div>
<div class="selector"> <div class="selector">
<select value={value} on:change> <select value={value} on:change>
<option value="last:7d">Last 7 days</option> <option selected={value === "last:7d" } value="last:7d">Last 7 days</option>
<option value="last:14d">Last 14 days</option> <option selected={value === "last:14d" } value="last:14d">Last 14 days</option>
<option value="last:30d">Last 30 days</option> <option selected={value === "last:30d" } value="last:30d">Last 30 days</option>
<option value="last:90d">Last 90 days</option> <option selected={value === "last:90d" } value="last:90d">Last 90 days</option>
<option value="last:1m">Last 1 month</option> <option selected={value === "last:1m" } value="last:1m">Last 1 month</option>
<option value="last:3m">Last 3 months</option> <option selected={value === "last:3m" } value="last:3m">Last 3 months</option>
<option value="last:6m">Last 6 months</option> <option selected={value === "last:6m" } value="last:6m">Last 6 months</option>
<option value="last:1y">Last 1 year</option> <option selected={value === "last:1y" } value="last:1y">Last 1 year</option>
<option value="this_week">This week</option> <option selected={value === "this_week" } value="this_week">This week</option>
<option value="next_week">Next week</option> <option selected={value === "next_week" } value="next_week">Next week</option>
<option value="last_week">Last week</option> <option selected={value === "last_week" } value="last_week">Last week</option>
<option value="this_month">This month</option> <option selected={value === "this_month" } value="this_month">This month</option>
<option value="next_month">Next month</option> <option selected={value === "next_month" } value="next_month">Next month</option>
<option value="last_month">Last month</option> <option selected={value === "last_month" } value="last_month">Last month</option>
<option value="all_time">All time</option> <option selected={value === "all_time" } value="all_time">All time</option>
</select> </select>
</div> </div>
<div class="par">)</div> <div class="par">)
<slot></slot>
</div>
</div> </div>
<style lang="sass"> <style lang="sass">
@@ -7,7 +7,7 @@
}; };
const fallback: ProjectContextData = { const fallback: ProjectContextData = {
project: writable({} as Project), project: readable({requirements: []} as Project),
reloadProject: () => Promise.resolve() reloadProject: () => Promise.resolve()
}; };
@@ -18,7 +18,7 @@
<script lang="ts"> <script lang="ts">
import type Project from "$lib/models/project"; import type Project from "$lib/models/project";
import { writable, type Readable } from "svelte/store"; import { readable, writable, type Readable } from "svelte/store";
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
import { sl3 } from "$lib/clients/sl3"; import { sl3 } from "$lib/clients/sl3";
import { getScopeContext } from "./ScopeContext.svelte"; import { getScopeContext } from "./ScopeContext.svelte";
@@ -9,6 +9,7 @@
export let hideUnseen = false; export let hideUnseen = false;
export let zeroDeletes = false; export let zeroDeletes = false;
export let noToggle = false; export let noToggle = false;
export let initOnList: number[] = [];
let acquiredMap: Record<number, number>; let acquiredMap: Record<number, number>;
let requiredMap: Record<number, number>; let requiredMap: Record<number, number>;
@@ -21,7 +22,7 @@
if (hideUnseen) { if (hideUnseen) {
for (const v of value) { for (const v of value) {
enabledMap[v.statId] = zeroDeletes ? v.required > 0 : v.required >= 0; enabledMap[v.statId] = initOnList.includes(v.statId) || (zeroDeletes ? v.required > 0 : v.required >= 0);
requiredMap[v.statId] = v.required; requiredMap[v.statId] = v.required;
acquiredMap[v.statId] = v.acquired; acquiredMap[v.statId] = v.acquired;
} }
@@ -0,0 +1,33 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import Icon, { type IconName } from "../layout/Icon.svelte";
export let name: IconName;
export let value: boolean;
export let inverse: boolean = false;
const dispatcher = createEventDispatcher();
function onClick() {
value = !value;
dispatcher("click");
}
</script>
<span on:click={onClick} class="toggle-icon" class:checked={value !== inverse}>
<Icon name={name} />
</span>
<style lang="sass">
@import "../../css/colors"
.toggle-icon
font-size: 0.666em
color: $color-entry1
user-select: none
-moz-user-select: none
-webkit-user-select: none
&.checked
color: $color-entry7
</style>
@@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import shortestDecimal from "$lib/utils/numbers";
export let offset: number; export let offset: number;
let value: string; let value: string;
let negative: boolean let negative: boolean
$: value = (offset > 0) ? `+${offset.toFixed()}` : offset.toFixed(); $: value = (offset > 0) ? `+${shortestDecimal(offset)}` : shortestDecimal(offset);
$: negative = offset < 0 $: negative = offset < 0
</script> </script>
@@ -11,9 +11,10 @@
export let noProgress = false; export let noProgress = false;
export let icon: IconName = null; export let icon: IconName = null;
export let status: Status = null; export let status: Status = null;
export let anchor: string = "";
</script> </script>
<div class="sl3-entry"> <div id={anchor} class="sl3-entry">
<div class="header"> <div class="header">
<div class="entry-icon"> <div class="entry-icon">
{#if icon != null} {#if icon != null}
@@ -44,33 +45,20 @@
opacity: 0.6 opacity: 0.6
margin-right: 0.5ch margin-right: 0.5ch
font-size: 1em font-size: 1em
margin-top: 0.225em margin-top: 0.15em
&:empty &:empty
display: none display: none
div.header div.header
display: flex display: flex
line-height: 1.25em
h2
font-size: 1.25em
margin: 0
color: $color-entry10
span.sub
color: $color-entry5
&.big
margin-top: 1em
font-size: 2em
&.small
font-size: 1em
div.right div.right
color: $color-entry2-transparent color: $color-entry2-transparent
margin-left: auto margin-left: auto
margin-top: auto margin-top: auto
margin-bottom: 0
&.noProgress &.noProgress
margin-top: -0.25em margin-top: -0.25em
@@ -43,6 +43,7 @@
div.sub-section div.sub-section
margin: 0.5em 0.4ch margin: 0.5em 0.4ch
padding: 0.33em 0.75ch padding: 0.33em 0.75ch
padding-bottom: 0.25em
background: $color-entry1-transparent background: $color-entry1-transparent
border-radius: 0.125em border-radius: 0.125em
border-bottom-right-radius: 0.5em border-bottom-right-radius: 0.5em
@@ -21,6 +21,8 @@
let eventText: string let eventText: string
$: { $: {
icon = void(0);
if (event === "none" || event === "acquired") { if (event === "none" || event === "acquired") {
if (item.acquiredTime) { if (item.acquiredTime) {
icon = "check"; icon = "check";
@@ -34,9 +36,9 @@
eventText = "scheduled" eventText = "scheduled"
} }
} else if (event === "created") { } else if (event === "created") {
icon = "lightbulb"; icon = "lightbulb";
status = Status.Available; status = Status.Available;
eventText = formatTime(item.createdTime) eventText = formatTime(item.createdTime)
} }
} }
</script> </script>
@@ -61,12 +63,18 @@
<Amount label={stat.name} value={stat.acquired} target={stat.required} /> <Amount label={stat.name} value={stat.acquired} target={stat.required} />
{/if} {/if}
{/each} {/each}
{#if item.weightedRequired}
<Amount dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
{/if}
{:else} {:else}
{#each item.stats as stat (stat.id)} {#each item.stats as stat (stat.id)}
{#if stat.required > 0} {#if stat.required > 0}
<Amount label={stat.name} value={stat.required} /> <Amount label={stat.name} value={stat.required} />
{/if} {/if}
{/each} {/each}
{#if item.weightedRequired}
<Amount dark right label="" value={item.weightedRequired} />
{/if}
{/if} {/if}
</AmountRow> </AmountRow>
</SubSection> </SubSection>
@@ -5,6 +5,7 @@
import Option from "$lib/components/layout/Option.svelte"; import Option from "$lib/components/layout/Option.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.svelte"; import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
import { getProjectContext } from "../contexts/ProjectContext.svelte"; import { getProjectContext } from "../contexts/ProjectContext.svelte";
import Icon from "../layout/Icon.svelte";
import RequirementEntry from "./RequirementSection.svelte"; import RequirementEntry from "./RequirementSection.svelte";
const {project} = getProjectContext(); const {project} = getProjectContext();
@@ -15,9 +16,9 @@
<Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} /> <Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} />
<Markdown source={$project.description} /> <Markdown source={$project.description} />
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option open={{name: "requirement.create", project: $project}}>Add Requirement</Option> <Option open={{name: "requirement.create", project: $project}}><Icon name="plus" /></Option>
<Option open={{name: "project.edit", project: $project}}>Edit</Option> <Option open={{name: "project.edit", project: $project}}><Icon name="pen" /></Option>
<Option open={{name: "project.delete", project: $project}} color="red">Delete</Option> <Option open={{name: "project.delete", project: $project}} color="red"><Icon name="trash" /></Option>
</OptionsRow> </OptionsRow>
{#each $project.requirements as requirement (requirement.id)} {#each $project.requirements as requirement (requirement.id)}
<RequirementEntry requirement={requirement} /> <RequirementEntry requirement={requirement} />
@@ -9,6 +9,7 @@
import LabeledProgressRow from "../common/LabeledProgressRow.svelte"; import LabeledProgressRow from "../common/LabeledProgressRow.svelte";
import { STATUS_ICONS } from "../common/StatusIcon.svelte"; import { STATUS_ICONS } from "../common/StatusIcon.svelte";
import ItemEntry from "./ItemSubSection.svelte"; import ItemEntry from "./ItemSubSection.svelte";
import Icon from "../layout/Icon.svelte";
export let requirement: Requirement; export let requirement: Requirement;
</script> </script>
@@ -18,9 +19,9 @@
<Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} /> <Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} />
<Markdown source={requirement.description} /> <Markdown source={requirement.description} />
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option open={{name: "item.create", requirement}}>Add Item</Option> <Option open={{name: "item.create", requirement}}><Icon name="plus" /></Option>
<Option open={{name: "requirement.edit", requirement}}>Edit</Option> <Option open={{name: "requirement.edit", requirement}}><Icon name="pen" /></Option>
<Option open={{name: "requirement.delete", requirement}} color="red">Delete</Option> <Option open={{name: "requirement.delete", requirement}} color="red"><Icon name="trash" /></Option>
</OptionsRow> </OptionsRow>
<LabeledProgressRow> <LabeledProgressRow>
{#each requirement.stats as stat (stat.id)} {#each requirement.stats as stat (stat.id)}
@@ -9,6 +9,7 @@
import ItemSubSection from "../project/ItemSubSection.svelte"; import ItemSubSection from "../project/ItemSubSection.svelte";
export let sprint: Sprint export let sprint: Sprint
export let compact: boolean = false;
let timeRange: [Date, Date]; let timeRange: [Date, Date];
@@ -34,8 +35,8 @@
{/if} {/if}
{#if sprint.kind === SprintKind.Items} {#if sprint.kind === SprintKind.Items}
{#each (sprint.items||[]) as item (item.id)} {#each (sprint.items||[]) as item (item.id)}
{#if !item.acquiredTime} {#if !compact || !item.acquiredTime}
<ItemSubSection compact item={item} /> <ItemSubSection compact={compact} item={item} />
{/if} {/if}
{/each} {/each}
{:else} {:else}
@@ -1,25 +1,17 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import type Sprint from "$lib/models/sprint"; import type Sprint from "$lib/models/sprint";
import Option from "../layout/Option.svelte"; import Option from "../layout/Option.svelte";
import OptionsRow from "../layout/OptionsRow.svelte"; import OptionsRow from "../layout/OptionsRow.svelte";
import SprintBody from "./SprintBody.svelte"; import SprintBody from "./SprintBody.svelte";
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds"; import { projectPrettyId } from "$lib/utils/prettyIds";
import { getScopeContext } from "../contexts/ScopeContext.svelte";
import Section from "../layout/Section.svelte"; import Section from "../layout/Section.svelte";
export let sprint: Sprint; export let sprint: Sprint;
const {scope} = getScopeContext();
function gotoSprint() {
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`)
}
</script> </script>
<Section noProgress title={sprint.name}> <Section anchor={projectPrettyId(sprint)} noProgress title={sprint.name}>
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option on:click={gotoSprint}>View</Option>
<Option open={{name: "sprint.edit", sprint}}>Edit</Option> <Option open={{name: "sprint.edit", sprint}}>Edit</Option>
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option> <Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
</OptionsRow> </OptionsRow>
@@ -10,10 +10,10 @@
import { getScopeContext } from "../contexts/ScopeContext.svelte"; import { getScopeContext } from "../contexts/ScopeContext.svelte";
export let sprint: Sprint; export let sprint: Sprint;
const {scope} = getScopeContext(); const {scope, lastHistoryRange} = getScopeContext();
function gotoSprint() { function gotoSprint() {
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`) goto(`/${scopePrettyId($scope)}/sprints/${$lastHistoryRange}#${projectPrettyId(sprint)}`)
} }
</script> </script>
@@ -23,5 +23,5 @@
<Option open={{name: "sprint.edit", sprint}}>Edit</Option> <Option open={{name: "sprint.edit", sprint}}>Edit</Option>
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option> <Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
</OptionsRow> </OptionsRow>
<SprintBody sprint={sprint} /> <SprintBody compact sprint={sprint} />
</SubSection> </SubSection>
+21 -5
View File
@@ -29,6 +29,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
let openedDate: Date let openedDate: Date
let requirementName: string let requirementName: string
let op: string let op: string
let initOnList: number[] = [];
let error: string let error: string
let loading: boolean let loading: boolean
@@ -38,9 +39,11 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
case "item.create": case "item.create":
initCreate($scope, $currentModal.requirement) initCreate($scope, $currentModal.requirement)
break; break;
case "item.edit": case "item.edit": {
initEdit($currentModal.item) const reqId = $currentModal.item.requirementId;
initEdit($currentModal.item, $project.requirements.find(r => r.id === reqId), $scope)
break; break;
}
default: default:
loading = false; loading = false;
@@ -52,6 +55,8 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
let stats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0})); let stats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
if (stats == null) { if (stats == null) {
stats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0})) stats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0}))
} else {
initOnList = requirement.stats.filter(s => s.required > 0).map(s => s.id);
} }
item = { item = {
@@ -69,14 +74,19 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
show = true; show = true;
} }
function initEdit(current: Item) { function initEdit(current: Item, requirement?: Requirement, scope?: Scope) {
const req = $project.requirements?.find(r => r.id === current.requirementId); 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) {
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}));
item = { item = {
name: current.name, name: current.name,
description: current.description, description: current.description,
requirementId: current.requirementId, requirementId: current.requirementId,
stats: current.stats.map(s => ({statId: s.id, acquired: s.acquired, required: s.required})), stats: ctxStats.map(s => inputStats.find(s2 => s2.statId === s.statId) || s),
acquiredTime: formatFormTime(current.acquiredTime), acquiredTime: formatFormTime(current.acquiredTime),
scheduledDate: current.scheduledDate, scheduledDate: current.scheduledDate,
}; };
@@ -165,7 +175,13 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
<input disabled name="req" type="text" value={requirementName} /> <input disabled name="req" type="text" value={requirementName} />
{/if} {/if}
<label for="stats">Stats</label> <label for="stats">Stats</label>
<StatInput showAcquired={op === "Edit" && !!item.acquiredTime} showRequired hideUnseen={!!item.requirementId} bind:value={item.stats} /> <StatInput
zeroDeletes showRequired
initOnList={initOnList}
showAcquired={op === "Edit" && !!item.acquiredTime}
hideUnseen={!!item.requirementId}
bind:value={item.stats}
/>
</ModalBody> </ModalBody>
</Modal> </Modal>
</form> </form>
+2
View File
@@ -13,6 +13,8 @@ export default interface Item {
acquiredTime?: string acquiredTime?: string
scheduledDate?: string scheduledDate?: string
stats: StatProgress[] stats: StatProgress[]
weightedAcquired: number
weightedRequired: number
} }
export interface ItemInput { export interface ItemInput {
+23 -7
View File
@@ -2,7 +2,17 @@ import type { ItemGroup, ItemGroupReference } from "$lib/models/item";
import type Item from "$lib/models/item"; import type Item from "$lib/models/item";
import { formatDate, formatDateTime, formatTime, formatWeekdayDate } from "./date"; import { formatDate, formatDateTime, formatTime, formatWeekdayDate } from "./date";
export function groupItems(items: Item[], minDate?: string): ItemGroup[] { export interface GroupItemsOptions {
hideCreated?: boolean
hideScheduled?: boolean
hideAcquired?: boolean
}
export function groupItemOptionsQuery(opts: GroupItemsOptions): string {
return `hideCreated=${opts.hideCreated},hideScheduled=${opts.hideScheduled},hideAcquired=${opts.hideAcquired}`;
}
export function groupItems(items: Item[], minDate?: string, opts: GroupItemsOptions = {}): ItemGroup[] {
let groups: Record<string, ItemGroup> = {}; let groups: Record<string, ItemGroup> = {};
for (let i = 0; i < items.length; ++i) { for (let i = 0; i < items.length; ++i) {
@@ -10,16 +20,22 @@ export function groupItems(items: Item[], minDate?: string): ItemGroup[] {
const createKey = formatDate(item.createdTime) const createKey = formatDate(item.createdTime)
const createSortKey = `${createKey} ${formatTime(item.createdTime)}` const createSortKey = `${createKey} ${formatTime(item.createdTime)}`
if (!item.acquiredTime || item.acquiredTime >= item.createdTime) { if (!opts.hideCreated) {
addItem(groups, createKey, i, createSortKey, item, "created"); if (!item.acquiredTime || item.acquiredTime >= item.createdTime) {
addItem(groups, createKey, i, createSortKey, item, "created");
}
} }
if (item.scheduledDate) { if (!opts.hideScheduled) {
addItem(groups, item.scheduledDate, i, `${item.scheduledDate} 23:59:59`, item, "scheduled"); if (item.scheduledDate) {
addItem(groups, item.scheduledDate, i, `${item.scheduledDate} 23:59:59`, item, "scheduled");
}
} }
if (item.acquiredTime) { if (!opts.hideAcquired) {
addItem(groups, formatDate(item.acquiredTime), i, formatDateTime(item.acquiredTime), item, "acquired"); if (item.acquiredTime) {
addItem(groups, formatDate(item.acquiredTime), i, formatDateTime(item.acquiredTime), item, "acquired");
}
} }
} }
+11
View File
@@ -0,0 +1,11 @@
export default function shortestDecimal(n: number) {
let s = n.toFixed(3);
while (s.endsWith("0")) {
s = s.slice(0, -1)
}
if (s.endsWith(".")) {
s = s.slice(0, -1)
}
return s;
}
+1 -1
View File
@@ -2,7 +2,7 @@ import type { TimeInterval } from "../models/common";
import { addDays, addMonths, addYears, formatDate, startOfDay, startOfMonth, startOfWeek, startOfYear } from "./date"; import { addDays, addMonths, addYears, formatDate, startOfDay, startOfMonth, startOfWeek, startOfYear } from "./date";
export default function parseInterval(s: string, date: Date): TimeInterval<Date> { export default function parseInterval(s: string, date: Date): TimeInterval<Date> {
const [verb, args] = s.split(":") const [verb, args] = s.split(";")[0].split(":")
switch (verb) { switch (verb) {
case "": case "":
@@ -3,11 +3,11 @@
import { sl3 } from "$lib/clients/sl3"; import { sl3 } from "$lib/clients/sl3";
import parseInterval, { datesOf, intervalLabel } from "$lib/utils/timeinterval"; import parseInterval, { datesOf, intervalLabel } from "$lib/utils/timeinterval";
import { groupItems } from "$lib/utils/items"; import { groupItemOptionsQuery, groupItems, type GroupItemsOptions } from "$lib/utils/items";
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";
export const load: Load = async({ fetch, params }) => { export const load: Load = async({ fetch, params, session }) => {
const scopeId = parseInt(params.scope.split("-")[0]); const scopeId = parseInt(params.scope.split("-")[0]);
const interval = parseInterval(params.interval, new Date()); const interval = parseInterval(params.interval, new Date());
@@ -22,22 +22,26 @@
props: { props: {
items, filter, items, filter,
intervalString: params.interval, intervalString: params.interval,
groups: groupItems(items, datesOf(interval)?.min), groups: groupItems(items, datesOf(interval)?.min, session.groupOptions || {}),
}, },
}; };
} }
</script> </script>
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import { session } from "$app/stores";
import { browser } from "$app/env";
import Main from "$lib/components/layout/Main.svelte"; import Main from "$lib/components/layout/Main.svelte";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import DeletionModal from "$lib/modals/DeletionModal.svelte"; import DeletionModal from "$lib/modals/DeletionModal.svelte";
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte"; import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte";
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte"; import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
import { goto } from "$app/navigation";
import HistorySelector from "$lib/components/common/HistorySelector.svelte"; import HistorySelector from "$lib/components/common/HistorySelector.svelte";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import ItemListContext from "$lib/components/contexts/ItemListContext.svelte"; import ItemListContext from "$lib/components/contexts/ItemListContext.svelte";
import HistoryGroupList from "$lib/components/history/HistoryGroupList.svelte"; import HistoryGroupList from "$lib/components/history/HistoryGroupList.svelte";
import ToggleIcon from "$lib/components/history/ToggleIcon.svelte";
import Status from "$lib/models/status";
export let intervalString: string export let intervalString: string
export let items: Item[] export let items: Item[]
@@ -51,13 +55,38 @@
lastHistoryRange.set(target.value); lastHistoryRange.set(target.value);
goto(`./${target.value}`); goto(`./${target.value}`, {replaceState: true});
}
function triggerReload() {
goto(`./${intervalString}`, {replaceState: true});
}
$: {
if (browser) {
if ($session.groupOptions == null) {
try {
$session.groupOptions = JSON.parse(localStorage.getItem("sl3.history.group_options")||"{}");
triggerReload();
} catch(_) {
$session.groupOptions = {};
}
} else {
localStorage.setItem("sl3.history.group_options", JSON.stringify($session.groupOptions));
}
}
} }
</script> </script>
<ItemListContext filter={filter} groups={groups} items={items}> <ItemListContext filter={filter} groups={groups} items={items}>
<Main big> <Main big>
<HistorySelector value={intervalString} on:change={changeInterval} /> <HistorySelector value={intervalString} on:change={changeInterval} >
{#if $session.groupOptions != null}
<ToggleIcon inverse name="lightbulb" bind:value={$session.groupOptions.hideCreated} on:click={triggerReload} />
<ToggleIcon inverse name="check_slot" bind:value={$session.groupOptions.hideAcquired} on:click={triggerReload} />
<ToggleIcon inverse name="calendar" bind:value={$session.groupOptions.hideScheduled} on:click={triggerReload} />
{/if}
</HistorySelector>
<HistoryGroupList /> <HistoryGroupList />
</Main> </Main>
+29
View File
@@ -14,6 +14,35 @@ func (s Status) Name() string {
return StatusLabels[s] return StatusLabels[s]
} }
func (s Status) Less(s2 Status) bool {
if s == Active && s2 != Active {
return true
}
if s != Active && s2 == Active {
return false
}
if s == Background && s2 != Background {
return true
}
if s != Background && s2 == Background {
return false
}
if s == Available && s2 != Available {
return true
}
if s != Available && s2 == Available {
return false
}
if s == Blocked && s2 != Blocked {
return true
}
if s != Blocked && s2 == Blocked {
return false
}
return s < s2
}
const ( const (
Blocked Status = iota Blocked Status = iota
Available Available
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE item CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE project CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE project_requirement CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE scope CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE sprint CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE stat CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
+18 -2
View File
@@ -8,8 +8,10 @@ import (
type Result struct { type Result struct {
entities.Item entities.Item
Requirement *entities.Requirement `json:"requirement,omitempty"` Requirement *entities.Requirement `json:"requirement,omitempty"`
Stats []ResultStat `json:"stats"` Stats []ResultStat `json:"stats"`
WeightedAcquired float64 `json:"weightedAcquired"`
WeightedRequired float64 `json:"weightedRequired"`
} }
func (r *Result) Stat(statID int) *ResultStat { func (r *Result) Stat(statID int) *ResultStat {
@@ -53,6 +55,7 @@ func (r *Result) AddStat(scope scopes.Result, stat entities.ItemStat) {
Required: stat.Required, Required: stat.Required,
}) })
r.SortStats(scope) r.SortStats(scope)
r.RecalculateWeighted()
} }
func (r *Result) SortStats(scope scopes.Result) { func (r *Result) SortStats(scope scopes.Result) {
@@ -76,6 +79,18 @@ func (r *Result) RemoveStat(id int) {
r.Stats = append(r.Stats[:i], r.Stats[i+1:]...) r.Stats = append(r.Stats[:i], r.Stats[i+1:]...)
} }
} }
r.RecalculateWeighted()
}
func (r *Result) RecalculateWeighted() {
r.WeightedAcquired = 0
r.WeightedRequired = 0
for _, stat := range r.Stats {
r.WeightedAcquired += float64(stat.Acquired) * stat.Weight
r.WeightedRequired += float64(stat.Required) * stat.Weight
}
} }
type ResultStat struct { type ResultStat struct {
@@ -121,6 +136,7 @@ func generateResult(item entities.Item, scope scopes.Result, progresses []entiti
} }
res.SortStats(scope) res.SortStats(scope)
res.RecalculateWeighted()
return res return res
} }
+12 -2
View File
@@ -142,6 +142,16 @@ func generateResult(
res.Requirements = append(res.Requirements, resReq) res.Requirements = append(res.Requirements, resReq)
} }
sort.Slice(res.Requirements, func(i, j int) bool {
ri := res.Requirements[i]
rj := res.Requirements[j]
if ri.Status != rj.Status {
return ri.Status.Less(rj.Status)
}
return ri.ID < rj.ID
})
return &res return &res
} }
@@ -228,9 +238,9 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
totalPlanned := 0.0 totalPlanned := 0.0
for _, stat := range resReq.Stats { for _, stat := range resReq.Stats {
if stat.Required > 0 { if stat.Required > 0 {
totalAcquired += float64(stat.Acquired) * stat.Weight totalAcquired += math.Min(float64(stat.Acquired), float64(stat.Required)) * stat.Weight
totalRequired += float64(stat.Required) * stat.Weight totalRequired += float64(stat.Required) * stat.Weight
totalPlanned += float64(stat.Planned) * stat.Weight totalPlanned += math.Min(float64(stat.Planned), float64(stat.Required)) * stat.Weight
} }
} }
resReq.TotalRequired += int(totalRequired) resReq.TotalRequired += int(totalRequired)