Gisle Aune
2 years ago
69 changed files with 1810 additions and 238 deletions
-
3entities/sprint.go
-
3entities/stat.go
-
53frontend/src/lib/clients/sl3.ts
-
22frontend/src/lib/components/common/Card.svelte
-
61frontend/src/lib/components/common/HistorySelector.svelte
-
20frontend/src/lib/components/common/LabeledProgress.svelte
-
5frontend/src/lib/components/common/Progress.svelte
-
70frontend/src/lib/components/contexts/ItemListContext.svelte
-
14frontend/src/lib/components/contexts/ModalContext.svelte
-
5frontend/src/lib/components/contexts/ProjectContext.svelte
-
37frontend/src/lib/components/contexts/ScopeContext.svelte
-
58frontend/src/lib/components/contexts/ScopeListContext.svelte
-
64frontend/src/lib/components/contexts/SprintListContext.svelte
-
153frontend/src/lib/components/controls/PartInput.svelte
-
13frontend/src/lib/components/controls/SprintKindSelect.svelte
-
46frontend/src/lib/components/controls/TimeRangeInput.svelte
-
23frontend/src/lib/components/frontpage/ScopeLink.svelte
-
10frontend/src/lib/components/frontpage/ScopeLinkList.svelte
-
10frontend/src/lib/components/history/HistoryGroupList.svelte
-
4frontend/src/lib/components/layout/Checkbox.svelte
-
7frontend/src/lib/components/layout/Column.svelte
-
4frontend/src/lib/components/layout/Main.svelte
-
22frontend/src/lib/components/layout/SubSection.svelte
-
11frontend/src/lib/components/project/ItemSubSection.svelte
-
2frontend/src/lib/components/project/ProjectMain.svelte
-
8frontend/src/lib/components/scope/ScopeHeader.svelte
-
15frontend/src/lib/components/scope/ScopeMenu.svelte
-
45frontend/src/lib/components/scope/SprintBody.svelte
-
14frontend/src/lib/components/scope/SprintList.svelte
-
27frontend/src/lib/components/scope/SprintSection.svelte
-
10frontend/src/lib/components/scope/SprintSectionList.svelte
-
27frontend/src/lib/components/scope/SprintSubSection.svelte
-
33frontend/src/lib/modals/DeletionModal.svelte
-
14frontend/src/lib/modals/ItemAcquireModal.svelte
-
34frontend/src/lib/modals/ItemCreateModal.svelte
-
117frontend/src/lib/modals/ProjectCreateEditModal.svelte
-
105frontend/src/lib/modals/ScopeCreateUpdateModal.svelte
-
167frontend/src/lib/modals/SprintCreateUpdateModal.svelte
-
6frontend/src/lib/modals/StatCreateEditModal.svelte
-
1frontend/src/lib/models/item.ts
-
2frontend/src/lib/models/project.ts
-
7frontend/src/lib/models/scope.ts
-
57frontend/src/lib/models/sprint.ts
-
2frontend/src/lib/models/stat.ts
-
4frontend/src/lib/utils/items.ts
-
13frontend/src/lib/utils/sprint.ts
-
44frontend/src/lib/utils/timeinterval.ts
-
22frontend/src/routes/[scope=prettyid]/__layout.svelte
-
2frontend/src/routes/[scope=prettyid]/history.svelte
-
62frontend/src/routes/[scope=prettyid]/history/[interval].svelte
-
79frontend/src/routes/[scope=prettyid]/index.svelte
-
125frontend/src/routes/[scope=prettyid]/overview.svelte
-
2frontend/src/routes/[scope=prettyid]/projects/[project=prettyid].svelte
-
62frontend/src/routes/[scope=prettyid]/sprints/[interval].svelte
-
14frontend/src/routes/__layout.svelte
-
2frontend/src/routes/api/[...any].ts
-
42frontend/src/routes/index.svelte
-
55frontend/src/routes/login.svelte
-
1models/sprint.go
-
1models/stat.go
-
27ports/httpapi/scopes.go
-
2ports/httpapi/sprints.go
-
5ports/mysql/mysqlcore/sprint.sql.go
-
3ports/mysql/queries/sprint.sql
-
1ports/mysql/sprint.go
-
2usecases/scopes/result.go
-
61usecases/scopes/service.go
-
2usecases/sprints/result.go
-
4usecases/sprints/service.go
@ -0,0 +1,61 @@ |
|||
<script lang="ts"> |
|||
export let label: string = "History" |
|||
export let value: string |
|||
</script> |
|||
|
|||
<div class="selector"> |
|||
<div class="label">{label}</div> |
|||
<div class="par">(</div> |
|||
<div class="selector"> |
|||
<select value={value} on:change> |
|||
<option value="last:7d">Last 7 days</option> |
|||
<option value="last:14d">Last 14 days</option> |
|||
<option value="last:30d">Last 30 days</option> |
|||
<option value="last:90d">Last 90 days</option> |
|||
<option value="last:1m">Last 1 month</option> |
|||
<option value="last:3m">Last 3 months</option> |
|||
<option value="last:6m">Last 6 months</option> |
|||
<option value="last:1y">Last 1 year</option> |
|||
<option value="this_week">This week</option> |
|||
<option value="next_week">Next week</option> |
|||
<option value="last_week">Last week</option> |
|||
<option value="this_month">This month</option> |
|||
<option value="next_month">Next month</option> |
|||
<option value="last_month">Last month</option> |
|||
<option value="all_time">All time</option> |
|||
</select> |
|||
</div> |
|||
<div class="par">)</div> |
|||
</div> |
|||
|
|||
<style lang="sass"> |
|||
@import "../../css/colors" |
|||
|
|||
div.selector |
|||
margin-top: 1em |
|||
font-size: 2em |
|||
margin: 0 |
|||
color: $color-entry10 |
|||
|
|||
display: flex |
|||
flex-direction: row |
|||
|
|||
div.label |
|||
margin-right: 0.5ch |
|||
|
|||
select |
|||
color: $color-entry5 |
|||
font-size: 0.5em |
|||
background: none |
|||
outline: none |
|||
border: none |
|||
line-height: 1em |
|||
margin-top: 0.025em |
|||
|
|||
> option |
|||
background: #000 |
|||
font-size: 0.666em |
|||
|
|||
div.par |
|||
color: $color-entry5 |
|||
</style> |
@ -0,0 +1,70 @@ |
|||
<script lang="ts" context="module"> |
|||
const contextKey = {ctx: "projectListCtx"}; |
|||
|
|||
interface ItemListContextData { |
|||
items: Readable<Item[]>, |
|||
groups: Readable<ItemGroup[]>, |
|||
reloadItemList(): Promise<void>, |
|||
}; |
|||
|
|||
const fallback: ItemListContextData = { |
|||
items: readable([]), |
|||
groups: readable([]), |
|||
reloadItemList: () => Promise.resolve() |
|||
}; |
|||
|
|||
export function getItemListContext() { |
|||
return getContext(contextKey) as ItemListContextData || fallback |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { readable, writable, type Readable } from "svelte/store"; |
|||
import { getContext, setContext } from "svelte"; |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
import { getScopeContext } from "./ScopeContext.svelte"; |
|||
import type Item from "$lib/models/item"; |
|||
import type { ItemFilter, ItemGroup } from "$lib/models/item"; |
|||
import { groupItems } from "$lib/utils/items"; |
|||
|
|||
export let items: Item[]; |
|||
export let groups: ItemGroup[]; |
|||
export let filter: ItemFilter; |
|||
|
|||
const {scope} = getScopeContext(); |
|||
|
|||
let itemsWritable = writable<Item[]>(items); |
|||
let groupsWritable = writable<ItemGroup[]>(groups); |
|||
let loading = false; |
|||
let lastSet = {items, groups}; |
|||
|
|||
setContext<ItemListContextData>(contextKey, { |
|||
items: {subscribe: itemsWritable.subscribe}, |
|||
groups: {subscribe: groupsWritable.subscribe}, |
|||
reloadItemList, |
|||
}); |
|||
|
|||
async function reloadItemList() { |
|||
if (loading) { |
|||
return |
|||
} |
|||
|
|||
try { |
|||
const newItems = await sl3(fetch).listItems($scope.id, filter) |
|||
itemsWritable.set(newItems); |
|||
groupsWritable.set(groupItems(newItems, filter.scheduledDate.min)) |
|||
} catch(_) {} |
|||
|
|||
loading = false; |
|||
} |
|||
|
|||
$: { |
|||
if (lastSet.items !== items || lastSet.groups !== groups) { |
|||
itemsWritable.set(items); |
|||
groupsWritable.set(groups); |
|||
lastSet = {items, groups}; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<slot></slot> |
@ -0,0 +1,58 @@ |
|||
<script lang="ts" context="module"> |
|||
const contextKey = {ctx: "scopeListCtx"}; |
|||
|
|||
interface ScopeListContextData { |
|||
scopes: Readable<Scope[]>, |
|||
reloadScopeList(): Promise<void>, |
|||
}; |
|||
|
|||
const fallback: ScopeListContextData = { |
|||
scopes: readable([]), |
|||
reloadScopeList: () => Promise.resolve() |
|||
}; |
|||
|
|||
export function getScopeListContext() { |
|||
return getContext(contextKey) as ScopeListContextData || fallback |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { readable, writable, type Readable } from "svelte/store"; |
|||
import { getContext, setContext } from "svelte"; |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
import { getScopeContext } from "./ScopeContext.svelte"; |
|||
import type Scope from "$lib/models/scope"; |
|||
|
|||
export let scopes: Scope[]; |
|||
|
|||
let scopesWritable = writable<Scope[]>(scopes); |
|||
let loading = false; |
|||
let lastSet = scopes; |
|||
|
|||
setContext<ScopeListContextData>(contextKey, { |
|||
scopes: {subscribe: scopesWritable.subscribe}, |
|||
reloadScopeList, |
|||
}); |
|||
|
|||
async function reloadScopeList() { |
|||
if (loading) { |
|||
return |
|||
} |
|||
|
|||
try { |
|||
const newScopes = await sl3(fetch).listScopes() |
|||
scopesWritable.set(newScopes); |
|||
} catch(_) {} |
|||
|
|||
loading = false; |
|||
} |
|||
|
|||
$: { |
|||
if (lastSet !== scopes) { |
|||
scopesWritable.set(scopes); |
|||
lastSet = scopes; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<slot></slot> |
@ -0,0 +1,64 @@ |
|||
<script lang="ts" context="module"> |
|||
const contextKey = {ctx: "projectListCtx"}; |
|||
|
|||
interface SprintListContextData { |
|||
sprints: Readable<Sprint[]>, |
|||
reloadSprintList(): Promise<void>, |
|||
}; |
|||
|
|||
const fallback: SprintListContextData = { |
|||
sprints: readable([]), |
|||
reloadSprintList: () => Promise.resolve() |
|||
}; |
|||
|
|||
export function getSprintListContext() { |
|||
return getContext(contextKey) as SprintListContextData || fallback |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { readable, writable, type Readable } from "svelte/store"; |
|||
import { getContext, setContext } from "svelte"; |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
import { getScopeContext } from "./ScopeContext.svelte"; |
|||
import type Sprint from "$lib/models/sprint"; |
|||
import parseInterval from "$lib/utils/timeinterval"; |
|||
import { getTimeContext } from "./TimeContext.svelte"; |
|||
|
|||
export let sprints: Sprint[]; |
|||
export let intervalString: string; |
|||
|
|||
const {scope} = getScopeContext(); |
|||
const {now} = getTimeContext(); |
|||
|
|||
let sprintsWritable = writable<Sprint[]>(sprints); |
|||
let loading = false; |
|||
let lastSet = sprints; |
|||
|
|||
setContext<SprintListContextData>(contextKey, { |
|||
sprints: {subscribe: sprintsWritable.subscribe}, |
|||
reloadSprintList, |
|||
}); |
|||
|
|||
async function reloadSprintList() { |
|||
if (loading) { |
|||
return |
|||
} |
|||
|
|||
try { |
|||
const newSprints = await sl3(fetch).listSprints($scope.id, parseInterval(intervalString, $now)); |
|||
sprintsWritable.set(newSprints); |
|||
} catch(_) {} |
|||
|
|||
loading = false; |
|||
} |
|||
|
|||
$: { |
|||
if (lastSet !== sprints) { |
|||
sprintsWritable.set(sprints); |
|||
lastSet = sprints; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<slot></slot> |
@ -0,0 +1,153 @@ |
|||
<script lang="ts" context="module"> |
|||
export interface PartOption { |
|||
partId: number |
|||
name: string |
|||
enabled: boolean |
|||
required: number |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { browser } from "$app/env"; |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; |
|||
import { SprintKind, type SprintInputPart } from "$lib/models/sprint"; |
|||
import Checkbox from "../layout/Checkbox.svelte"; |
|||
|
|||
export let value: SprintInputPart[] = []; |
|||
export let kind: SprintKind; |
|||
|
|||
const {scope} = getScopeContext(); |
|||
|
|||
let error = ""; |
|||
let options: PartOption[] = []; |
|||
let showRequired = false; |
|||
let loading = false; |
|||
let loaded: SprintKind = SprintKind.Invalid |
|||
|
|||
async function load(kind: SprintKind) { |
|||
loading = true; |
|||
|
|||
try { |
|||
switch (kind) { |
|||
case SprintKind.Items: { |
|||
const items = await sl3(fetch).listItems($scope.id, {}); |
|||
options = items.map(i => ({ |
|||
enabled: false, |
|||
name: `${i.name}${i.acquiredTime?" ✓":""}`, |
|||
partId: i.id, |
|||
required: 0, |
|||
})) |
|||
|
|||
showRequired = false; |
|||
break; |
|||
} |
|||
case SprintKind.Requirements: { |
|||
options = []; |
|||
throw "Requirement selection is not yet supported the UI." |
|||
} |
|||
case SprintKind.Stats: { |
|||
options = $scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0})); |
|||
showRequired = true; |
|||
break; |
|||
} |
|||
case SprintKind.Scope: { |
|||
options = []; |
|||
showRequired = false; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
error = ""; |
|||
for (const opt of options) { |
|||
const val = value.find(v => v.partId === opt.partId); |
|||
if (val != null) { |
|||
opt.enabled = true; |
|||
opt.required = val.required; |
|||
} |
|||
} |
|||
} catch(err) { |
|||
error = err?.toString() || err; |
|||
} |
|||
|
|||
loaded = kind; |
|||
loading = false; |
|||
} |
|||
|
|||
$: if (loaded === kind && error === "") { |
|||
value = options.filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required})) |
|||
} |
|||
|
|||
$: if (browser && !loading && loaded !== kind) { |
|||
load(kind); |
|||
} |
|||
</script> |
|||
|
|||
<div class="part-input"> |
|||
<div class="error">{error}</div> |
|||
{#each options as option (option.partId)} |
|||
<div class="part"> |
|||
<Checkbox noLabel bind:checked={option.enabled} /> |
|||
<div class="name" class:disabled={!option.enabled}>{option.name}</div> |
|||
{#if showRequired && option.enabled} |
|||
<input type="number" class="required" bind:value={option.required} /> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style lang="scss"> |
|||
@import "../../css/colors"; |
|||
|
|||
div.part { |
|||
display: flex; |
|||
margin: 0; |
|||
padding: 0; |
|||
border-top: 1px solid $color-entry1; |
|||
|
|||
&:first-of-type { |
|||
border-top: none; |
|||
} |
|||
|
|||
input:first-of-type { |
|||
margin-left: auto; |
|||
} |
|||
|
|||
input { |
|||
font-size: 1em; |
|||
width: fit-content; |
|||
max-width: 7ch; |
|||
padding: 0.3em 0; |
|||
margin: 0; |
|||
text-align: center; |
|||
} |
|||
|
|||
div { |
|||
padding: 0.3em 0.5ch; |
|||
|
|||
&.disabled { |
|||
color: $color-entry2; |
|||
} |
|||
} |
|||
} |
|||
|
|||
div.part-input { |
|||
width: calc(100% - 2ch); |
|||
margin-bottom: 1em; |
|||
margin-top: 0.20em; |
|||
height: 12em; |
|||
overflow-y: auto; |
|||
|
|||
background: $color-entryhalf; |
|||
color: $color-entry8; |
|||
border: none; |
|||
outline: none; |
|||
resize: vertical; |
|||
padding: 0.5em 1ch; |
|||
|
|||
div.error { |
|||
font-size: 0.9em; |
|||
color: $sc-5; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,13 @@ |
|||
<script lang="ts"> |
|||
import { SprintKind } from "$lib/models/sprint"; |
|||
|
|||
export let kind: SprintKind; |
|||
export let disabled: boolean = false; |
|||
</script> |
|||
|
|||
<select disabled={disabled} bind:value={kind}> |
|||
<option value={SprintKind.Items}>Items</option> |
|||
<option value={SprintKind.Requirements}>Requirements</option> |
|||
<option value={SprintKind.Stats}>Stats</option> |
|||
<option value={SprintKind.Scope}>Scope</option> |
|||
</select> |
@ -0,0 +1,46 @@ |
|||
<script lang="ts"> |
|||
import { formatFormTime } from "$lib/utils/date"; |
|||
import parseInterval from "$lib/utils/timeinterval"; |
|||
|
|||
export let from: string; |
|||
export let to: string; |
|||
export let openDate: Date; |
|||
export let intervalName: string; |
|||
|
|||
$: { |
|||
if (intervalName !== "specific_dates") { |
|||
const int = parseInterval(intervalName, openDate); |
|||
from = formatFormTime(int.min); |
|||
to = formatFormTime(int.max); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<select bind:value={intervalName}> |
|||
<option value="this_week">This week</option> |
|||
<option value="next_week">Next week</option> |
|||
<option value="last_week">Last week</option> |
|||
<option value="next:7d">7 days starting from today</option> |
|||
<option value="this_month">This month</option> |
|||
<option value="next_month">Next month</option> |
|||
<option value="last_month">Last month</option> |
|||
<option value="next:30d">30 days starting from today</option> |
|||
<option value="this_year">This year</option> |
|||
<option value="next_year">Next year</option> |
|||
<option value="last_year">Last year</option> |
|||
<option value="next:1y">One year starting from today</option> |
|||
<option value="specific_dates">Specific Date</option> |
|||
</select> |
|||
<input class="top-date" type="datetime-local" disabled={intervalName !== "specific_dates"} bind:value={from} /> |
|||
<input type="datetime-local" disabled={intervalName !== "specific_dates"} bind:value={to} /> |
|||
|
|||
<style> |
|||
select, input.top-date { |
|||
margin-bottom: 0.25em !important; |
|||
} |
|||
|
|||
input { |
|||
margin-top: 0 !important; |
|||
resize: none !important; |
|||
} |
|||
</style> |
@ -0,0 +1,10 @@ |
|||
<script lang="ts"> |
|||
import { getScopeListContext } from "../contexts/ScopeListContext.svelte"; |
|||
import ScopeLink from "./ScopeLink.svelte"; |
|||
|
|||
const {scopes} = getScopeListContext(); |
|||
</script> |
|||
|
|||
{#each $scopes as scope (scope.id)} |
|||
<ScopeLink scope={scope} /> |
|||
{/each} |
@ -0,0 +1,10 @@ |
|||
<script lang="ts"> |
|||
import { getItemListContext } from "../contexts/ItemListContext.svelte"; |
|||
import HistoryGroupSection from "./HistoryGroupSection.svelte"; |
|||
|
|||
const {groups, items} = getItemListContext(); |
|||
</script> |
|||
|
|||
{#each $groups as group (group.label)} |
|||
<HistoryGroupSection group={group} items={$items} /> |
|||
{/each} |
@ -0,0 +1,8 @@ |
|||
<script lang="ts"> |
|||
import { getScopeContext } from "../contexts/ScopeContext.svelte"; |
|||
import Header from "../layout/Header.svelte"; |
|||
|
|||
const {scope} = getScopeContext(); |
|||
</script> |
|||
|
|||
<Header subtitle={$scope.name}>{$scope.abbreviation}</Header> |
@ -0,0 +1,15 @@ |
|||
<script lang="ts"> |
|||
import { scopePrettyId } from "$lib/utils/prettyIds"; |
|||
import MenuCategory from "../common/MenuCategory.svelte"; |
|||
import MenuItem from "../common/MenuItem.svelte"; |
|||
import { getScopeContext } from "../contexts/ScopeContext.svelte"; |
|||
|
|||
const {scope, lastHistoryRange} = getScopeContext(); |
|||
</script> |
|||
|
|||
<MenuCategory> |
|||
<MenuItem href={`/`}>Home</MenuItem> |
|||
<MenuItem href={`/${scopePrettyId($scope)}/overview`}>Overview</MenuItem> |
|||
<MenuItem href={`/${scopePrettyId($scope)}/sprints/${$lastHistoryRange}`}>Sprints</MenuItem> |
|||
<MenuItem href={`/${scopePrettyId($scope)}/history/${$lastHistoryRange}`}>History</MenuItem> |
|||
</MenuCategory> |
@ -0,0 +1,45 @@ |
|||
<script lang="ts"> |
|||
import type Sprint from "$lib/models/sprint"; |
|||
import { SprintKind } from "$lib/models/sprint"; |
|||
import Amount from "../common/Amount.svelte"; |
|||
import AmountRow from "../common/AmountRow.svelte"; |
|||
import LabeledProgress from "../common/LabeledProgress.svelte"; |
|||
import LabeledProgressRow from "../common/LabeledProgressRow.svelte"; |
|||
import Markdown from "../common/Markdown.svelte"; |
|||
import ItemSubSection from "../project/ItemSubSection.svelte"; |
|||
|
|||
export let sprint: Sprint |
|||
|
|||
let timeRange: [Date, Date]; |
|||
|
|||
$: if (sprint.isTimed) { |
|||
timeRange = [new Date(sprint.fromTime), new Date(sprint.toTime)]; |
|||
} else { |
|||
timeRange = void(0); |
|||
} |
|||
</script> |
|||
|
|||
<Markdown source={sprint.description} /> |
|||
{#if sprint.kind === SprintKind.Items} |
|||
<LabeledProgress timeRange={timeRange} green name={sprint.aggregateName || "Items"} count={sprint.itemsAcquired} target={sprint.itemsRequired} /> |
|||
{:else} |
|||
<LabeledProgress timeRange={timeRange} name={sprint.aggregateName} count={sprint.aggregateAcquired} target={sprint.aggregateRequired} /> |
|||
{/if} |
|||
{#if !sprint.isCoarse} |
|||
<LabeledProgressRow> |
|||
{#each (sprint.progress||[]) as stat (stat.id)} |
|||
<LabeledProgress compact timeRange={timeRange} name={stat.name} count={stat.acquired} target={stat.required} /> |
|||
{/each} |
|||
</LabeledProgressRow> |
|||
{/if} |
|||
{#if sprint.kind === SprintKind.Items} |
|||
{#each sprint.items as item (item.id)} |
|||
{#if !item.acquiredTime} |
|||
<ItemSubSection compact item={item} /> |
|||
{/if} |
|||
{/each} |
|||
{:else} |
|||
<AmountRow> |
|||
<Amount label="Acquired Items" value={(sprint.items||[]).length} /> |
|||
</AmountRow> |
|||
{/if} |
@ -0,0 +1,14 @@ |
|||
<script lang="ts"> |
|||
import { getSprintListContext } from "../contexts/SprintListContext.svelte"; |
|||
import SprintSubSection from "./SprintSubSection.svelte"; |
|||
|
|||
export let sub = false; |
|||
|
|||
const {sprints} = getSprintListContext(); |
|||
</script> |
|||
|
|||
{#if sub} |
|||
{#each $sprints as sprint (sprint.id)} |
|||
<SprintSubSection sprint={sprint} /> |
|||
{/each} |
|||
{/if} |
@ -0,0 +1,27 @@ |
|||
<script lang="ts"> |
|||
import { goto } from "$app/navigation"; |
|||
import type Sprint from "$lib/models/sprint"; |
|||
|
|||
import Option from "../layout/Option.svelte"; |
|||
import OptionsRow from "../layout/OptionsRow.svelte"; |
|||
import SprintBody from "./SprintBody.svelte"; |
|||
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds"; |
|||
import { getScopeContext } from "../contexts/ScopeContext.svelte"; |
|||
import Section from "../layout/Section.svelte"; |
|||
|
|||
export let sprint: Sprint; |
|||
const {scope} = getScopeContext(); |
|||
|
|||
function gotoSprint() { |
|||
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`) |
|||
} |
|||
</script> |
|||
|
|||
<Section noProgress title={sprint.name}> |
|||
<OptionsRow slot="right"> |
|||
<Option on:click={gotoSprint}>View</Option> |
|||
<Option open={{name: "sprint.edit", sprint}}>Edit</Option> |
|||
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option> |
|||
</OptionsRow> |
|||
<SprintBody sprint={sprint} /> |
|||
</Section> |
@ -0,0 +1,10 @@ |
|||
<script lang="ts"> |
|||
import { getSprintListContext } from "../contexts/SprintListContext.svelte"; |
|||
import SprintSection from "./SprintSection.svelte"; |
|||
|
|||
const {sprints} = getSprintListContext(); |
|||
</script> |
|||
|
|||
{#each $sprints as sprint (sprint.id)} |
|||
<SprintSection sprint={sprint} /> |
|||
{/each} |
@ -0,0 +1,27 @@ |
|||
<script lang="ts"> |
|||
import { goto } from "$app/navigation"; |
|||
import type Sprint from "$lib/models/sprint"; |
|||
|
|||
import Option from "../layout/Option.svelte"; |
|||
import OptionsRow from "../layout/OptionsRow.svelte"; |
|||
import SubSection from "../layout/SubSection.svelte"; |
|||
import SprintBody from "./SprintBody.svelte"; |
|||
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds"; |
|||
import { getScopeContext } from "../contexts/ScopeContext.svelte"; |
|||
|
|||
export let sprint: Sprint; |
|||
const {scope} = getScopeContext(); |
|||
|
|||
function gotoSprint() { |
|||
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`) |
|||
} |
|||
</script> |
|||
|
|||
<SubSection noProgress title={sprint.name}> |
|||
<OptionsRow slot="right"> |
|||
<Option on:click={gotoSprint}>View</Option> |
|||
<Option open={{name: "sprint.edit", sprint}}>Edit</Option> |
|||
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option> |
|||
</OptionsRow> |
|||
<SprintBody sprint={sprint} /> |
|||
</SubSection> |
@ -0,0 +1,117 @@ |
|||
<script lang="ts"> |
|||
import { goto } from "$app/navigation"; |
|||
|
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
|
|||
import Modal from "$lib/components/common/Modal.svelte"; |
|||
import ModalBody from "$lib/components/common/ModalBody.svelte"; |
|||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte"; |
|||
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte"; |
|||
import { getProjectListContext } from "$lib/components/contexts/ProjectListContext.svelte"; |
|||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; |
|||
import StatusSelect from "$lib/components/controls/StatusSelect.svelte"; |
|||
import type Project from "$lib/models/project"; |
|||
import type { ProjectEntry, ProjectInput, Requirement, RequirementInput } from "$lib/models/project"; |
|||
import Status from "$lib/models/status"; |
|||
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds"; |
|||
|
|||
const {currentModal, closeModal} = getModalContext(); |
|||
const {scope} = getScopeContext(); |
|||
const {reloadProject} = getProjectContext(); |
|||
const {reloadProjectList} = getProjectListContext(); |
|||
|
|||
let project: ProjectInput |
|||
let projectId: number |
|||
let oldStats: RequirementInput["stats"] |
|||
|
|||
let error: string |
|||
let loading: boolean |
|||
let show: boolean |
|||
let op: "Create" | "Edit" | "Delete" |
|||
|
|||
$: switch ($currentModal.name) { |
|||
case "project.create": |
|||
initCreate(); |
|||
op = "Create"; |
|||
break; |
|||
|
|||
case "project.edit": |
|||
initEdit($currentModal.project); |
|||
op = "Edit"; |
|||
break; |
|||
|
|||
default: |
|||
loading = false; |
|||
error = null; |
|||
show = false; |
|||
} |
|||
|
|||
function initCreate() { |
|||
project = { |
|||
name: "", |
|||
description: "", |
|||
status: Status.Available, |
|||
createdTime: void(0), |
|||
} |
|||
|
|||
show = true; |
|||
} |
|||
|
|||
function initEdit(current: Project) { |
|||
project = { |
|||
name: current.name, |
|||
description: current.description, |
|||
status: current.status, |
|||
} |
|||
|
|||
projectId = current.id; |
|||
show = true; |
|||
} |
|||
|
|||
async function submit() { |
|||
error = null; |
|||
loading = true; |
|||
|
|||
try { |
|||
switch (op) { |
|||
case "Create": |
|||
const newProject = await sl3(fetch).createProject($scope.id, project); |
|||
projectId = newProject.id |
|||
break; |
|||
case "Edit": |
|||
await sl3(fetch).updateProject($scope.id, projectId, project); |
|||
break; |
|||
} |
|||
|
|||
// Wait for project to reload |
|||
await reloadProject(); |
|||
await reloadProjectList(); |
|||
|
|||
goto(`/${scopePrettyId($scope)}/projects/${projectPrettyId({id: projectId, name: project.name})}`, {replaceState: op === "Edit"}) |
|||
|
|||
closeModal(); |
|||
} catch(err) { |
|||
if (err.statusCode != null) { |
|||
error = err.statusMessage; |
|||
} else { |
|||
error = err |
|||
} |
|||
|
|||
} finally { |
|||
loading = false; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<form on:submit|preventDefault={submit}> |
|||
<Modal show={show} closable verb={op} noun="Project" disabled={loading} error={error}> |
|||
<ModalBody> |
|||
<label for="name">Name</label> |
|||
<input name="name" type="text" bind:value={project.name} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={project.description} /> |
|||
<label for="stats">Status</label> |
|||
<StatusSelect bind:status={project.status} /> |
|||
</ModalBody> |
|||
</Modal> |
|||
</form> |
@ -0,0 +1,105 @@ |
|||
<script lang="ts"> |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
|
|||
import Modal from "$lib/components/common/Modal.svelte"; |
|||
import ModalBody from "$lib/components/common/ModalBody.svelte"; |
|||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte"; |
|||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; |
|||
import { getScopeListContext } from "$lib/components/contexts/ScopeListContext.svelte"; |
|||
import type { ScopeInput } from "$lib/models/scope"; |
|||
import type Scope from "$lib/models/scope"; |
|||
|
|||
const {currentModal, closeModal} = getModalContext(); |
|||
const {updateScope} = getScopeContext(); |
|||
const {reloadScopeList} = getScopeListContext(); |
|||
|
|||
let scope: ScopeInput |
|||
let scopeId: number |
|||
|
|||
let op: string |
|||
let error: string |
|||
let loading: boolean |
|||
let show: boolean |
|||
|
|||
$: switch ($currentModal.name) { |
|||
case "scope.create": |
|||
initCreate() |
|||
break; |
|||
case "scope.edit": |
|||
initEdit($currentModal.scope) |
|||
break; |
|||
|
|||
default: |
|||
loading = false; |
|||
error = null; |
|||
show = false; |
|||
} |
|||
|
|||
function initCreate() { |
|||
scope = { |
|||
name: "", |
|||
abbreviation: "", |
|||
ownerName: "", |
|||
customLabels: {}, |
|||
} |
|||
|
|||
op = "Create" |
|||
show = true; |
|||
} |
|||
|
|||
function initEdit(current: Scope) { |
|||
scope = { |
|||
name: current.name, |
|||
abbreviation: current.abbreviation, |
|||
ownerName: "", |
|||
customLabels: current.customLabels, |
|||
}; |
|||
scopeId = current.id; |
|||
|
|||
op = "Edit" |
|||
show = true; |
|||
} |
|||
|
|||
async function submit() { |
|||
error = null; |
|||
loading = true; |
|||
|
|||
try { |
|||
switch (op) { |
|||
case "Create": |
|||
await sl3(fetch).createScope(scope); |
|||
await reloadScopeList(); |
|||
break; |
|||
case "Edit": |
|||
const res = await sl3(fetch).updateScope(scopeId, scope); |
|||
updateScope(res); |
|||
break; |
|||
} |
|||
|
|||
closeModal(); |
|||
} catch(err) { |
|||
if (err.statusCode != null) { |
|||
error = err.statusMessage; |
|||
} else { |
|||
error = err |
|||
} |
|||
} finally { |
|||
loading = false; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<form on:submit|preventDefault={submit}> |
|||
<Modal closable show={show} verb={op} noun="scope" disabled={loading} error={error}> |
|||
<ModalBody> |
|||
<label for="name">Name</label> |
|||
<input name="name" type="text" bind:value={scope.name} /> |
|||
<label for="abbreviation">Abbreviation</label> |
|||
<input name="abbreviation" type="text" bind:value={scope.abbreviation} /> |
|||
{#if op === "Create"} |
|||
<label for="ownerName">Owner Name</label> |
|||
<input name="ownerName" type="text" bind:value={scope.ownerName} /> |
|||
{/if} |
|||
</ModalBody> |
|||
</Modal> |
|||
</form> |
@ -0,0 +1,167 @@ |
|||
<script lang="ts"> |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
|
|||
import Modal from "$lib/components/common/Modal.svelte"; |
|||
import ModalBody from "$lib/components/common/ModalBody.svelte"; |
|||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte"; |
|||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; |
|||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte"; |
|||
import PartInput from "$lib/components/controls/PartInput.svelte"; |
|||
import SprintKindSelect from "$lib/components/controls/SprintKindSelect.svelte"; |
|||
import TimeRangeInput from "$lib/components/controls/TimeRangeInput.svelte"; |
|||
import Checkbox from "$lib/components/layout/Checkbox.svelte"; |
|||
import type Scope from "$lib/models/scope"; |
|||
import type Sprint from "$lib/models/sprint"; |
|||
import { SprintKind, type SprintInput, type SprintInputPart } from "$lib/models/sprint"; |
|||
import { formatFormTime } from "$lib/utils/date"; |
|||
import { partsDiff } from "$lib/utils/sprint"; |
|||
|
|||
const {currentModal, closeModal} = getModalContext(); |
|||
const {scope} = getScopeContext(); |
|||
const {reloadSprintList} = getSprintListContext(); |
|||
|
|||
let sprint: SprintInput |
|||
let sprintId: number |
|||
let oldParts: SprintInputPart[] |
|||
let intervalName: string; |
|||
|
|||
let openedDate: Date |
|||
let op: string |
|||
let error: string |
|||
let loading: boolean |
|||
let show: boolean |
|||
|
|||
$: switch ($currentModal.name) { |
|||
case "sprint.create": |
|||
initCreate($scope) |
|||
break; |
|||
case "sprint.edit": |
|||
initEdit($currentModal.sprint) |
|||
break; |
|||
|
|||
default: |
|||
loading = false; |
|||
error = null; |
|||
show = false; |
|||
} |
|||
|
|||
function initCreate(scope: Scope) { |
|||
sprint = { |
|||
name: "", |
|||
description: "", |
|||
fromTime: "", |
|||
toTime: "", |
|||
kind: SprintKind.Stats, |
|||
aggregateName: "", |
|||
aggregateRequired: 0, |
|||
isCoarse: false, |
|||
isTimed: false, |
|||
isUnweighted: false, |
|||
parts: [], |
|||
} |
|||
intervalName = "this_month"; |
|||
|
|||
op = "Create" |
|||
openedDate = new Date(); |
|||
show = true; |
|||
} |
|||
|
|||
function initEdit(current: Sprint) { |
|||
sprint = { |
|||
name: current.name, |
|||
description: current.description, |
|||
fromTime: formatFormTime(current.fromTime), |
|||
toTime: formatFormTime(current.toTime), |
|||
kind: current.kind, |
|||
aggregateName: current.aggregateName, |
|||
aggregateRequired: current.aggregateRequired, |
|||
isCoarse: current.isCoarse, |
|||
isTimed: current.isTimed, |
|||
isUnweighted: current.isUnweighted, |
|||
}; |
|||
sprintId = current.id; |
|||
intervalName = "specific_dates"; |
|||
|
|||
if (sprint.kind === SprintKind.Stats) { |
|||
sprint.parts = current.progress.map(p => ({partId: p.id, required: p.required})); |
|||
} else { |
|||
sprint.parts = current.partIds.map(p => ({partId: p, required: 0})); |
|||
} |
|||
oldParts = [...sprint.parts]; |
|||
|
|||
op = "Edit" |
|||
openedDate = new Date(); |
|||
show = true; |
|||
} |
|||
|
|||
async function submit() { |
|||
error = null; |
|||
loading = true; |
|||
|
|||
const submission: SprintInput = { |
|||
...sprint, |
|||
fromTime: new Date(sprint.fromTime).toISOString(), |
|||
toTime: new Date(sprint.toTime).toISOString(), |
|||
} |
|||
|
|||
try { |
|||
switch (op) { |
|||
case "Create": |
|||
await sl3(fetch).createSprint($scope.id, submission); |
|||
break; |
|||
case "Edit": |
|||
await sl3(fetch).updateSprint($scope.id, sprintId, submission); |
|||
const {added, removed} = partsDiff(oldParts, submission.parts) |
|||
for (const part of added) { |
|||
await sl3(fetch).upsertSprintPart($scope.id, sprintId, part).catch(() => {}); |
|||
} |
|||
for (const part of removed) { |
|||
await sl3(fetch).deleteSprintPart($scope.id, sprintId, part).catch(() => {}); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
await reloadSprintList(); |
|||
|
|||
closeModal(); |
|||
} catch(err) { |
|||
if (err.statusCode != null) { |
|||
error = err.statusMessage; |
|||
} else { |
|||
error = err |
|||
} |
|||
|
|||
} finally { |
|||
loading = false; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<form on:submit|preventDefault={submit}> |
|||
<Modal wide closable show={show} verb={op} noun="sprint" disabled={loading} error={error}> |
|||
<ModalBody> |
|||
<label for="name">Name</label> |
|||
<input name="name" type="text" bind:value={sprint.name} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={sprint.description} /> |
|||
<label for="time">Time</label> |
|||
<TimeRangeInput openDate={openedDate} bind:from={sprint.fromTime} bind:to={sprint.toTime} bind:intervalName={intervalName} /> |
|||
<label for="aggregateName">Aggregate Name</label> |
|||
<input name="aggregateName" type="text" bind:value={sprint.aggregateName} /> |
|||
{#if sprint.kind != SprintKind.Items} |
|||
<label for="aggregateValue">Aggregate Goal</label> |
|||
<input name="aggregateValue" type="number" bind:value={sprint.aggregateRequired} /> |
|||
{/if} |
|||
</ModalBody> |
|||
<ModalBody> |
|||
<label for="kind">Kind</label> |
|||
<SprintKindSelect disabled={op === "Edit"} bind:kind={sprint.kind} /> |
|||
<label for="parts">Parts</label> |
|||
<PartInput bind:value={sprint.parts} kind={sprint.kind} /> |
|||
<Checkbox bind:checked={sprint.isTimed} label="Sprint is timed" /> |
|||
<Checkbox bind:checked={sprint.isCoarse} label="Show only aggregate" /> |
|||
<Checkbox bind:checked={sprint.isUnweighted} label="Aggregate is unweighted" /> |
|||
</ModalBody> |
|||
</Modal> |
|||
</form> |
@ -0,0 +1,57 @@ |
|||
import type Item from "./item"; |
|||
import type { StatProgress } from "./stat"; |
|||
|
|||
export default interface Sprint { |
|||
id: number |
|||
scopeId: number |
|||
name: string |
|||
description: string |
|||
kind: SprintKind |
|||
|
|||
fromTime: string |
|||
toTime: string |
|||
|
|||
isTimed: boolean |
|||
isCoarse: boolean |
|||
isUnweighted: boolean |
|||
|
|||
aggregateName: string |
|||
aggregateRequired: number |
|||
aggregateAcquired: number |
|||
aggregatePlanned: number |
|||
|
|||
itemsAcquired?: number |
|||
itemsRequired?: number |
|||
|
|||
partIds: number[] |
|||
items: Item[] |
|||
progress: StatProgress[] |
|||
} |
|||
|
|||
export enum SprintKind { |
|||
Items = 0, |
|||
Requirements = 1, |
|||
Stats = 2, |
|||
Scope = 3, |
|||
|
|||
Invalid = -1, |
|||
} |
|||
|
|||
export interface SprintInput { |
|||
name: string |
|||
description: string |
|||
kind: SprintKind |
|||
fromTime: string |
|||
toTime: string |
|||
isTimed?: boolean |
|||
isCoarse?: boolean |
|||
isUnweighted?: boolean |
|||
aggregateName?: string |
|||
aggregateRequired?: number |
|||
parts?: SprintInputPart[] |
|||
} |
|||
|
|||
export interface SprintInputPart { |
|||
partId: number |
|||
required?: number |
|||
} |
@ -0,0 +1,13 @@ |
|||
import type { SprintInputPart } from "$lib/models/sprint"; |
|||
|
|||
interface PartDiff { |
|||
added: SprintInputPart[] |
|||
removed: SprintInputPart[] |
|||
} |
|||
|
|||
export function partsDiff(before: SprintInputPart[], after: SprintInputPart[]): PartDiff { |
|||
return { |
|||
added: after.filter(p => !before.find(p2 => p2.partId === p.partId && p2.required === p.required)), |
|||
removed: before.filter(p => !after.find(p2 => p2.partId === p.partId)), |
|||
} |
|||
} |
@ -1,79 +0,0 @@ |
|||
<script lang="ts" context="module"> |
|||
import type { Load } from "@sveltejs/kit/types/internal"; |
|||
|
|||
export const load: Load = async({params, url, fetch}) => { |
|||
const scopeId = parseInt(params.scope.split("-")[0]); |
|||
|
|||
console.log( |
|||
datesOf(parseInterval("next:7d", new Date)), |
|||
parseInterval("today", new Date), |
|||
) |
|||
|
|||
const scheduledItems = (await sl3(fetch).listItems(scopeId, { |
|||
scheduledDate: datesOf(parseInterval("next:7d", new Date)), |
|||
})) |
|||
const acquiredItems = (await sl3(fetch).listItems(scopeId, { |
|||
acquiredTime: parseInterval("today", new Date), |
|||
})) |
|||
return { |
|||
props: {scheduledItems, acquiredItems} |
|||
}; |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; |
|||
import Column from "$lib/components/layout/Column.svelte"; |
|||
import Columns from "$lib/components/layout/Columns.svelte"; |
|||
import Row from "$lib/components/layout/Row.svelte"; |
|||
import Option from "$lib/components/layout/Option.svelte"; |
|||
import OptionsRow from "$lib/components/layout/OptionsRow.svelte"; |
|||
import StatSubSection from "$lib/components/scope/StatSubSection.svelte"; |
|||
import DeletionModal from "$lib/modals/DeletionModal.svelte"; |
|||
import StatCreateEditModal from "$lib/modals/StatCreateEditModal.svelte"; |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
import parseInterval, { datesOf } from "$lib/utils/timeinterval"; |
|||
import type Item from "$lib/models/item"; |
|||
import ItemSubSection from "$lib/components/project/ItemSubSection.svelte"; |
|||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte"; |
|||
|
|||
const {scope} = getScopeContext(); |
|||
|
|||
export let acquiredItems: Item[]; |
|||
export let scheduledItems: Item[]; |
|||
</script> |
|||
|
|||
<Columns fullwidth> |
|||
<Column> |
|||
{#if scheduledItems.length > 0} |
|||
<Row title="Schedule"> |
|||
{#each scheduledItems as item (item.id)} |
|||
<ItemSubSection item={item} /> |
|||
{/each} |
|||
</Row> |
|||
{/if} |
|||
{#if acquiredItems.length > 0} |
|||
<Row title="Today"> |
|||
{#each acquiredItems as item (item.id)} |
|||
<ItemSubSection item={item} /> |
|||
{/each} |
|||
</Row> |
|||
{/if} |
|||
</Column> |
|||
<Column> |
|||
<Row title="Sprints"> |
|||
</Row> |
|||
<Row title="Stats"> |
|||
<OptionsRow slot="right"> |
|||
<Option open={{name: "stat.create"}}>Create</Option> |
|||
</OptionsRow> |
|||
{#each $scope.stats as stat (stat.id)} |
|||
<StatSubSection stat={stat} /> |
|||
{/each} |
|||
</Row> |
|||
</Column> |
|||
</Columns> |
|||
|
|||
<ItemCreateModal /> |
|||
<StatCreateEditModal /> |
|||
<DeletionModal /> |
@ -0,0 +1,125 @@ |
|||
<script lang="ts" context="module"> |
|||
import type { Load } from "@sveltejs/kit/types/internal"; |
|||
|
|||
export const load: Load = async({params, url, fetch}) => { |
|||
const scopeId = parseInt(params.scope.split("-")[0]); |
|||
|
|||
console.log( |
|||
datesOf(parseInterval("next:7d", new Date)), |
|||
parseInterval("today", new Date), |
|||
) |
|||
|
|||
const scheduledItems = (await sl3(fetch).listItems(scopeId, { |
|||
scheduledDate: datesOf(parseInterval("next:7d", new Date)), |
|||
})) |
|||
const acquiredItems = (await sl3(fetch).listItems(scopeId, { |
|||
acquiredTime: parseInterval("today", new Date), |
|||
})) |
|||
const sprints = await sl3(fetch).listSprints(scopeId); |
|||
|
|||
return { |
|||
props: {scheduledItems, acquiredItems, sprints} |
|||
}; |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; |
|||
import Column from "$lib/components/layout/Column.svelte"; |
|||
import Columns from "$lib/components/layout/Columns.svelte"; |
|||
import Row from "$lib/components/layout/Row.svelte"; |
|||
import Option from "$lib/components/layout/Option.svelte"; |
|||
import OptionsRow from "$lib/components/layout/OptionsRow.svelte"; |
|||
import StatSubSection from "$lib/components/scope/StatSubSection.svelte"; |
|||
import DeletionModal from "$lib/modals/DeletionModal.svelte"; |
|||
import StatCreateEditModal from "$lib/modals/StatCreateEditModal.svelte"; |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
import parseInterval, { datesOf } from "$lib/utils/timeinterval"; |
|||
import type Item from "$lib/models/item"; |
|||
import ItemSubSection from "$lib/components/project/ItemSubSection.svelte"; |
|||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte"; |
|||
import Card from "$lib/components/common/Card.svelte"; |
|||
import CardHeader from "$lib/components/common/CardHeader.svelte"; |
|||
import SprintCreateUpdateModal from "$lib/modals/SprintCreateUpdateModal.svelte"; |
|||
import type Sprint from "$lib/models/sprint"; |
|||
import SprintListContext from "$lib/components/contexts/SprintListContext.svelte"; |
|||
import SprintList from "$lib/components/scope/SprintList.svelte"; |
|||
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte"; |
|||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte"; |
|||
import ProjectCreateEditModal from "$lib/modals/ProjectCreateEditModal.svelte"; |
|||
import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte"; |
|||
|
|||
export let acquiredItems: Item[]; |
|||
export let scheduledItems: Item[]; |
|||
export let sprints: Sprint[]; |
|||
|
|||
const {scope} = getScopeContext(); |
|||
const {openModal} = getModalContext(); |
|||
|
|||
function openCreateProject() { |
|||
openModal({name: "project.create"}); |
|||
} |
|||
|
|||
function openPostItem() { |
|||
openModal({name: "item.create"}); |
|||
} |
|||
|
|||
function openEditScope() { |
|||
openModal({name: "scope.edit", scope: $scope}); |
|||
} |
|||
|
|||
function openDeleteScope() { |
|||
openModal({name: "scope.delete", scope: $scope}); |
|||
} |
|||
</script> |
|||
|
|||
<SprintListContext sprints={sprints} intervalString=""> |
|||
<Columns fullwidth> |
|||
<Column> |
|||
<Row title="Options"> |
|||
<Card on:click={openCreateProject} pointerCursor><CardHeader>Create Project</CardHeader></Card> |
|||
<Card on:click={openPostItem} pointerCursor><CardHeader>Post Item</CardHeader></Card> |
|||
<Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card> |
|||
<Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card> |
|||
</Row> |
|||
{#if scheduledItems.length > 0} |
|||
<Row title="Schedule"> |
|||
{#each scheduledItems as item (item.id)} |
|||
<ItemSubSection item={item} /> |
|||
{/each} |
|||
</Row> |
|||
{/if} |
|||
{#if acquiredItems.length > 0} |
|||
<Row title="Today"> |
|||
{#each acquiredItems as item (item.id)} |
|||
<ItemSubSection item={item} /> |
|||
{/each} |
|||
</Row> |
|||
{/if} |
|||
</Column> |
|||
<Column> |
|||
<Row title="Sprints"> |
|||
<OptionsRow slot="right"> |
|||
<Option open={{name: "sprint.create"}}>Create</Option> |
|||
</OptionsRow> |
|||
<SprintList sub /> |
|||
</Row> |
|||
<Row title="Stats"> |
|||
<OptionsRow slot="right"> |
|||
<Option open={{name: "stat.create"}}>Create</Option> |
|||
</OptionsRow> |
|||
{#each $scope.stats as stat (stat.id)} |
|||
<StatSubSection stat={stat} /> |
|||
{/each} |
|||
</Row> |
|||
</Column> |
|||
</Columns> |
|||
|
|||
<ItemCreateModal /> |
|||
<ItemAcquireModal /> |
|||
<StatCreateEditModal /> |
|||
<DeletionModal /> |
|||
<SprintCreateUpdateModal /> |
|||
<ProjectCreateEditModal /> |
|||
<ScopeCreateUpdateModal /> |
|||
</SprintListContext> |
@ -0,0 +1,62 @@ |
|||
<script lang="ts" context="module"> |
|||
import type { Load } from "@sveltejs/kit/types/internal"; |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
|
|||
export const load: Load = async({ fetch, params }) => { |
|||
const scopeId = parseInt(params.scope.split("-")[0]); |
|||
|
|||
let interval = parseInterval(params.interval, new Date()); |
|||
if (interval == null) { |
|||
// The Reapers are done by then anyway... |
|||
interval = {min: new Date("2000-01-01T00:00:00Z"), max: new Date("2187-01-01T00:00:00Z")} |
|||
} |
|||
|
|||
const sprints = await sl3(fetch).listSprints(scopeId, interval); |
|||
|
|||
return { |
|||
props: { |
|||
sprints, |
|||
intervalString: params.interval, |
|||
}, |
|||
}; |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { goto } from "$app/navigation"; |
|||
import Main from "$lib/components/layout/Main.svelte"; |
|||
import DeletionModal from "$lib/modals/DeletionModal.svelte"; |
|||
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte"; |
|||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte"; |
|||
import HistorySelector from "$lib/components/common/HistorySelector.svelte"; |
|||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; |
|||
import parseInterval from "$lib/utils/timeinterval"; |
|||
import type Sprint from "$lib/models/sprint"; |
|||
import SprintListContext from "$lib/components/contexts/SprintListContext.svelte"; |
|||
import SprintCreateUpdateModal from "$lib/modals/SprintCreateUpdateModal.svelte"; |
|||
import SprintSectionList from "$lib/components/scope/SprintSectionList.svelte"; |
|||
|
|||
export let intervalString: string |
|||
export let sprints: Sprint[] |
|||
|
|||
const {lastHistoryRange} = getScopeContext(); |
|||
|
|||
function changeInterval(e: Event) { |
|||
const target = e.target as HTMLSelectElement; |
|||
lastHistoryRange.set(target.value); |
|||
|
|||
goto(`./${target.value}`); |
|||
} |
|||
</script> |
|||
|
|||
<SprintListContext sprints={sprints} intervalString={intervalString}> |
|||
<Main big> |
|||
<HistorySelector label="Sprints" value={intervalString} on:change={changeInterval} /> |
|||
<SprintSectionList /> |
|||
</Main> |
|||
|
|||
<ItemCreateModal /> |
|||
<ItemAcquireModal /> |
|||
<DeletionModal /> |
|||
<SprintCreateUpdateModal /> |
|||
</SprintListContext> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue