Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6825584b8d | ||
|
|
a530bed697 | ||
|
|
e65349662a | ||
|
|
fd5d732851 | ||
|
|
9259609efe | ||
|
|
a5b9f85eda | ||
|
|
c503b7a30e | ||
|
|
b089933188 | ||
|
|
2bd1e0a9e3 |
@@ -100,6 +100,8 @@ func main() {
|
||||
apiV1AllScopes := apiV1.Group("/all-scopes")
|
||||
apiV1AllScopes.Use(httpapi.AllScopeMiddleware(scopesService, authService))
|
||||
httpapi.ItemsAllScopes(apiV1AllScopes, itemsService)
|
||||
httpapi.SprintsAllScopes(apiV1AllScopes.Group("/sprints"), sprintsService)
|
||||
|
||||
ginLambda := ginadapter.New(server)
|
||||
lambda.Start(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
|
||||
return ginLambda.ProxyWithContext(ctx, request)
|
||||
|
||||
@@ -98,6 +98,7 @@ func main() {
|
||||
apiV1AllScopes := apiV1.Group("/all-scopes")
|
||||
apiV1AllScopes.Use(httpapi.AllScopeMiddleware(scopesService, authService))
|
||||
httpapi.ItemsAllScopes(apiV1AllScopes.Group("/items"), itemsService)
|
||||
httpapi.SprintsAllScopes(apiV1AllScopes.Group("/sprints"), sprintsService)
|
||||
|
||||
exitSignal := make(chan os.Signal)
|
||||
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
@@ -58,6 +58,8 @@ export const handle: Handle = async({ event, resolve }) => {
|
||||
for (const key in newCookies) {
|
||||
response.headers.append("Set-Cookie", cookie.serialize(key, newCookies[key], {
|
||||
path: "/",
|
||||
maxAge: 8640000,
|
||||
expires: new Date(Date.now() + 8640000000),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -99,15 +99,17 @@ export default class SL3APIClient {
|
||||
return this.fetch<{sprint: Sprint}>("GET", `scopes/${scopeId}/sprints/${sprintId}`).then(r => r.sprint);
|
||||
}
|
||||
|
||||
async listSprints(scopeId: number, interval?: TimeInterval<Date | string>): Promise<Sprint[]> {
|
||||
async listSprints(scopeId: number | "ALL", interval?: TimeInterval<Date | string>): Promise<Sprint[]> {
|
||||
let qs = "";
|
||||
if (interval != null) {
|
||||
const minStr = (interval.min instanceof Date) ? interval.min.toISOString() : interval.min;
|
||||
const maxStr = (interval.max instanceof Date) ? interval.max.toISOString() : interval.max;
|
||||
qs = `?from=${encodeURIComponent(minStr)}&to=${encodeURIComponent(maxStr)}`
|
||||
}
|
||||
|
||||
let prefix = (scopeId === "ALL") ? `all-scopes` : `scopes/${scopeId}`
|
||||
|
||||
return this.fetch<{sprints: Sprint[]}>("GET", `scopes/${scopeId}/sprints${qs}`).then(r => r.sprints);
|
||||
return this.fetch<{sprints: Sprint[]}>("GET", `${prefix}/sprints${qs}`).then(r => r.sprints);
|
||||
}
|
||||
|
||||
async createSprint(scopeId: number, input: SprintInput): Promise<Sprint> {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
const fallback: ScopeContextData = {
|
||||
scope: readable({stats: []} as Scope),
|
||||
lastHistoryRange: writable(""),
|
||||
lastHistoryRange: writable("last:14d"),
|
||||
upsertStat: () => {},
|
||||
deleteStat: () => {},
|
||||
updateScope: () => {},
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
export let sprints: Sprint[];
|
||||
export let intervalString: string;
|
||||
export let global: boolean = false;
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {now} = getTimeContext();
|
||||
@@ -46,7 +47,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const newSprints = await sl3(fetch).listSprints($scope.id, parseInterval(intervalString, $now));
|
||||
const newSprints = await sl3(fetch).listSprints(global ? "ALL" : $scope.id, parseInterval(intervalString, $now));
|
||||
sprintsWritable.set(newSprints);
|
||||
} catch(_) {}
|
||||
|
||||
|
||||
@@ -31,15 +31,19 @@
|
||||
import { browser } from "$app/env";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import type Scope from "$lib/models/scope";
|
||||
import { SprintKind, type SprintInputPart } from "$lib/models/sprint";
|
||||
import Status from "$lib/models/status";
|
||||
import shortestDecimal from "$lib/utils/numbers";
|
||||
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
|
||||
import Checkbox from "../layout/Checkbox.svelte";
|
||||
|
||||
export let value: SprintInputPart[] = [];
|
||||
export let kind: SprintKind;
|
||||
export let scopeId: number;
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {scopes} = getScopeListContext();
|
||||
|
||||
let error = "";
|
||||
let groups: PartOptionGroup[] = [];
|
||||
@@ -47,18 +51,20 @@
|
||||
let loading = false;
|
||||
let onlyActive = true;
|
||||
let loaded: SprintKind = SprintKind.Invalid
|
||||
let hiddenSelections: SprintInputPart[] = [];
|
||||
let actualScope: Scope;
|
||||
|
||||
function forceReload() {
|
||||
loaded = SprintKind.Invalid;
|
||||
}
|
||||
|
||||
async function load(kind: SprintKind) {
|
||||
async function load(kind: SprintKind, scope: Scope) {
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
switch (kind) {
|
||||
case SprintKind.Items: {
|
||||
const items = await sl3(fetch).listItems($scope.id, {unAcquired: onlyActive});
|
||||
const items = await sl3(fetch).listItems(scope.id, {unAcquired: onlyActive});
|
||||
const projects = items.filter(i => i.project != null).map(i => i.project).sort(compareStatus).filter((p, i, a) => p.id != a[i-1]?.id);
|
||||
const requirements = items.filter(i => i.requirement != null).map(i => i.requirement).sort(compareStatus).filter((r, i, a) => r.id != a[i-1]?.id);
|
||||
const loose = items.filter(i => i.requirementId == null);
|
||||
@@ -90,6 +96,9 @@
|
||||
required: 0,
|
||||
})).sort((a,b) => a.partId - b.partId)})
|
||||
|
||||
const allParts = groups.flatMap(g => [...g.subs.flatMap(g => g.options), ...g.options]);
|
||||
hiddenSelections = value.filter(v => !allParts.find(p => p.partId === v.partId));
|
||||
|
||||
showRequired = false;
|
||||
break;
|
||||
}
|
||||
@@ -102,7 +111,7 @@
|
||||
id: -1,
|
||||
label: "",
|
||||
subs: [],
|
||||
options: $scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0})),
|
||||
options: scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0})),
|
||||
}];
|
||||
showRequired = true;
|
||||
break;
|
||||
@@ -148,11 +157,16 @@
|
||||
}
|
||||
|
||||
$: if (loaded === kind && error === "") {
|
||||
value = groups.flatMap(g => [...g.subs.flatMap(g => g.options), ...g.options]).filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required}))
|
||||
value = [
|
||||
...hiddenSelections,
|
||||
...groups.flatMap(g => [...g.subs.flatMap(g => g.options), ...g.options]).filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required})),
|
||||
]
|
||||
}
|
||||
|
||||
$: actualScope = ($scope.id === scopeId || scopeId == null) ? $scope : $scopes.find(s => s.id === scopeId);
|
||||
|
||||
$: if (browser && !loading && loaded !== kind) {
|
||||
load(kind);
|
||||
load(kind, actualScope);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
for (const stat of actualScope.stats) {
|
||||
const found = value.find(v => v.statId == stat.id);
|
||||
|
||||
enabledMap[stat.id] = found != null && (!!found?.required || !!found?.acquired);
|
||||
enabledMap[stat.id] = found != null && (found?.required > (zeroDeletes ? 0 : -1));
|
||||
requiredMap[stat.id] = found?.required || 0;
|
||||
acquiredMap[stat.id] = found?.acquired || 0;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||
import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot";
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons/faEye";
|
||||
|
||||
const icons = {
|
||||
"clock": faClock,
|
||||
@@ -66,6 +67,7 @@
|
||||
"chevron_down": faChevronDown,
|
||||
"trash": faTrash,
|
||||
"check_slot": faCheckToSlot,
|
||||
"eye": faEye,
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
import SprintBody from "./SprintBody.svelte";
|
||||
import { projectPrettyId } from "$lib/utils/prettyIds";
|
||||
import Section from "../layout/Section.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
|
||||
export let sprint: Sprint;
|
||||
</script>
|
||||
|
||||
<Section anchor={projectPrettyId(sprint)} noProgress title={sprint.name}>
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "sprint.edit", sprint}}>Edit</Option>
|
||||
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
|
||||
<Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
<SprintBody sprint={sprint} />
|
||||
</Section>
|
||||
@@ -8,20 +8,28 @@
|
||||
import SprintBody from "./SprintBody.svelte";
|
||||
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
|
||||
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
||||
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
|
||||
export let sprint: Sprint;
|
||||
const {scope, lastHistoryRange} = getScopeContext();
|
||||
const {scopes} = getScopeListContext();
|
||||
|
||||
function gotoSprint() {
|
||||
goto(`/${scopePrettyId($scope)}/sprints/${$lastHistoryRange}#${projectPrettyId(sprint)}`)
|
||||
let sc = $scope;
|
||||
if (sc.id !== sprint.id) {
|
||||
sc = $scopes.find(s => s.id === sprint.scopeId);
|
||||
}
|
||||
|
||||
goto(`/${scopePrettyId(sc)}/sprints/${$lastHistoryRange}#${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>
|
||||
<Option on:click={gotoSprint}><Icon name="eye" /></Option>
|
||||
<Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
<SprintBody compact sprint={sprint} />
|
||||
</SubSection>
|
||||
@@ -3,6 +3,7 @@
|
||||
import Amount from "../common/Amount.svelte";
|
||||
import AmountRow from "../common/AmountRow.svelte";
|
||||
import Markdown from "../common/Markdown.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
import Option from "../layout/Option.svelte";
|
||||
import OptionsRow from "../layout/OptionsRow.svelte";
|
||||
import SubSection from "../layout/SubSection.svelte";
|
||||
@@ -12,8 +13,8 @@
|
||||
|
||||
<SubSection noProgress title={stat.name}>
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "stat.edit", stat}}>Edit</Option>
|
||||
<Option open={{name: "stat.delete", stat}} color="red">Delete</Option>
|
||||
<Option open={{name: "stat.edit", stat}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "stat.delete", stat}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
<Markdown source={stat.description} />
|
||||
{#if stat.weight !== 1}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.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";
|
||||
@@ -26,6 +27,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
const {reloadProjectList} = getProjectListContext();
|
||||
const {reloadSprintList} = getSprintListContext();
|
||||
const {reloadItemList} = getItemListContext();
|
||||
const {reloadItemLists} = getItemMultiListContext();
|
||||
|
||||
let endpoint: string;
|
||||
|
||||
@@ -49,7 +51,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
break;
|
||||
|
||||
case "item.delete":
|
||||
init(`items/${$currentModal.item.id}`)
|
||||
init(`items/${$currentModal.item.id}`, [], $currentModal.item.scopeId)
|
||||
noun = "Item";
|
||||
name = $currentModal.item.name;
|
||||
break;
|
||||
@@ -62,7 +64,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
break;
|
||||
|
||||
case "sprint.delete":
|
||||
init(`sprints/${$currentModal.sprint.id}`)
|
||||
init(`sprints/${$currentModal.sprint.id}`, [], $currentModal.sprint.scopeId)
|
||||
noun = "Sprint";
|
||||
name = $currentModal.sprint.name;
|
||||
break;
|
||||
@@ -72,7 +74,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
$currentModal.requirement.items.map(i => ({
|
||||
op: "Detach item",
|
||||
name: i.name,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
noun = "Requirement";
|
||||
name = $currentModal.requirement.name;
|
||||
@@ -100,8 +102,13 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
show = false;
|
||||
}
|
||||
|
||||
function init(newEndpoint: string, newSideEffects: DeletionSideEffect[] = []) {
|
||||
endpoint = `scopes/${$scope.id}/${newEndpoint}`;
|
||||
function init(newEndpoint: string, newSideEffects: DeletionSideEffect[] = [], overrideScopeId?: number) {
|
||||
let scopeId = $scope.id;
|
||||
if (overrideScopeId != null) {
|
||||
scopeId = overrideScopeId;
|
||||
}
|
||||
|
||||
endpoint = `scopes/${scopeId}/${newEndpoint}`;
|
||||
if (endpoint.endsWith("/")) {
|
||||
endpoint = endpoint.slice(0, -1);
|
||||
}
|
||||
@@ -131,6 +138,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
await reloadProject();
|
||||
|
||||
if (noun === "Stat") {
|
||||
reloadItemLists();
|
||||
deleteStat((res as {stat: Stat}).stat);
|
||||
}
|
||||
|
||||
@@ -139,6 +147,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
}
|
||||
|
||||
if (noun === "Item") {
|
||||
reloadItemLists();
|
||||
await reloadItemList();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
@@ -16,6 +17,7 @@
|
||||
const {reloadProject} = getProjectContext();
|
||||
const {reloadItemList} = getItemListContext();
|
||||
const {reloadSprintList} = getSprintListContext();
|
||||
const {reloadItemLists} = getItemMultiListContext();
|
||||
|
||||
let item: Partial<ItemInput>
|
||||
let itemId: number
|
||||
@@ -68,6 +70,7 @@
|
||||
await reloadProject();
|
||||
}
|
||||
|
||||
reloadItemLists();
|
||||
await reloadItemList();
|
||||
await reloadSprintList();
|
||||
|
||||
@@ -93,7 +96,7 @@
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={item.description} />
|
||||
<label for="stats">Stats</label>
|
||||
<StatInput showAcquired hideUnseen noToggle bind:value={item.stats} />
|
||||
<StatInput overrideScopeId={scopeId} showAcquired hideUnseen noToggle bind:value={item.stats} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import { getScopeListContext } from "$lib/components/contexts/ScopeListContext.svelte";
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import { getScopeListContext } from "$lib/components/contexts/ScopeListContext.svelte";
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte";
|
||||
import StatInput from "$lib/components/controls/StatInput.svelte";
|
||||
import type Item from "$lib/models/item";
|
||||
|
||||
@@ -22,6 +22,7 @@ import { partsDiff } from "$lib/utils/sprint";
|
||||
|
||||
let sprint: SprintInput
|
||||
let sprintId: number
|
||||
let scopeId: number
|
||||
let oldParts: SprintInputPart[]
|
||||
let intervalName: string;
|
||||
|
||||
@@ -60,6 +61,7 @@ import { partsDiff } from "$lib/utils/sprint";
|
||||
parts: [],
|
||||
}
|
||||
intervalName = "this_month";
|
||||
scopeId = scope.id;
|
||||
|
||||
op = "Create"
|
||||
openedDate = new Date();
|
||||
@@ -80,6 +82,7 @@ import { partsDiff } from "$lib/utils/sprint";
|
||||
isUnweighted: current.isUnweighted,
|
||||
};
|
||||
sprintId = current.id;
|
||||
scopeId = current.scopeId;
|
||||
intervalName = "specific_dates";
|
||||
|
||||
if (sprint.kind === SprintKind.Stats) {
|
||||
@@ -107,16 +110,16 @@ import { partsDiff } from "$lib/utils/sprint";
|
||||
try {
|
||||
switch (op) {
|
||||
case "Create":
|
||||
await sl3(fetch).createSprint($scope.id, submission);
|
||||
await sl3(fetch).createSprint(scopeId, submission);
|
||||
break;
|
||||
case "Edit":
|
||||
await sl3(fetch).updateSprint($scope.id, sprintId, submission);
|
||||
await sl3(fetch).updateSprint(scopeId, sprintId, submission);
|
||||
const {added, removed} = partsDiff(oldParts, submission.parts)
|
||||
for (const part of added) {
|
||||
await sl3(fetch).upsertSprintPart($scope.id, sprintId, part).catch(() => {});
|
||||
await sl3(fetch).upsertSprintPart(scopeId, sprintId, part).catch(() => {});
|
||||
}
|
||||
for (const part of removed) {
|
||||
await sl3(fetch).deleteSprintPart($scope.id, sprintId, part).catch(() => {});
|
||||
await sl3(fetch).deleteSprintPart(scopeId, sprintId, part).catch(() => {});
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -161,7 +164,7 @@ import { partsDiff } from "$lib/utils/sprint";
|
||||
<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} />
|
||||
<PartInput bind:value={sprint.parts} kind={sprint.kind} scopeId={scopeId} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -62,6 +62,7 @@
|
||||
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
|
||||
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
|
||||
import Icon from "$lib/components/layout/Icon.svelte";
|
||||
|
||||
export let acquiredItems: Item[];
|
||||
export let scheduledItems: Item[];
|
||||
@@ -106,13 +107,13 @@
|
||||
<Column>
|
||||
<Row title="Sprints">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "sprint.create"}}>Create</Option>
|
||||
<Option open={{name: "sprint.create"}}><Icon name="plus" /></Option>
|
||||
</OptionsRow>
|
||||
<SprintList sub />
|
||||
</Row>
|
||||
<Row title="Stats">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "stat.create"}}>Create</Option>
|
||||
<Option open={{name: "stat.create"}}><Icon name="plus" /></Option>
|
||||
</OptionsRow>
|
||||
{#each $scope.stats as stat (stat.id)}
|
||||
<StatSubSection stat={stat} />
|
||||
|
||||
@@ -26,9 +26,10 @@
|
||||
const scheduledItems = (await sl3(fetch).listItems("ALL", scheduledFilter));
|
||||
const acquiredItems = (await sl3(fetch).listItems("ALL", acquiredFilter));
|
||||
const looseItems = (await sl3(fetch).listItems("ALL", looseFilter));
|
||||
const sprints = await sl3(fetch).listSprints("ALL");
|
||||
|
||||
return {
|
||||
props: { scopes, scheduledItems, acquiredItems, looseItems }
|
||||
props: { scopes, scheduledItems, acquiredItems, looseItems, sprints }
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -52,12 +53,17 @@
|
||||
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
|
||||
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
|
||||
import type { ItemFilter } from "$lib/models/item";
|
||||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
|
||||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
|
||||
import SprintList from "$lib/components/scope/SprintList.svelte";
|
||||
import SprintCreateUpdateModal from "$lib/modals/SprintCreateUpdateModal.svelte";
|
||||
import SprintListContext from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import type Sprint from "$lib/models/sprint";
|
||||
|
||||
export let scopes: Scope[] = [];
|
||||
export let acquiredItems: Item[];
|
||||
export let scheduledItems: Item[];
|
||||
export let looseItems: Item[];
|
||||
export let sprints: Sprint[];
|
||||
|
||||
const {now} = getTimeContext();
|
||||
</script>
|
||||
@@ -66,25 +72,31 @@ import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
|
||||
|
||||
<ItemMultiListContext global lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
|
||||
<ScopeListContext scopes={scopes}>
|
||||
<Columns wide>
|
||||
<Column>
|
||||
<Row title="Scopes">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "scope.create"}}>Create</Option>
|
||||
</OptionsRow>
|
||||
<ScopeLinkList />
|
||||
</Row>
|
||||
<ItemListRow title="Scheduled" key="scheduleItems" />
|
||||
<ItemListRow title="Today" key="acquiredItems" showAcquiredTime />
|
||||
<ItemListRow title="Loose" key="looseItems" />
|
||||
</Column>
|
||||
<Column>
|
||||
</Column>
|
||||
</Columns>
|
||||
|
||||
<ItemCreateModal />
|
||||
<ItemAcquireModal />
|
||||
<DeletionModal />
|
||||
<ScopeCreateUpdateModal />
|
||||
<SprintListContext global sprints={sprints} intervalString="">
|
||||
<Columns wide>
|
||||
<Column>
|
||||
<Row title="Scopes">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "scope.create"}}>Create</Option>
|
||||
</OptionsRow>
|
||||
<ScopeLinkList />
|
||||
</Row>
|
||||
<ItemListRow title="Scheduled" key="scheduleItems" />
|
||||
<ItemListRow title="Today" key="acquiredItems" showAcquiredTime />
|
||||
<ItemListRow title="Loose" key="looseItems" />
|
||||
</Column>
|
||||
<Column>
|
||||
<Row title="Sprints">
|
||||
<SprintList sub />
|
||||
</Row>
|
||||
</Column>
|
||||
</Columns>
|
||||
|
||||
<ItemCreateModal />
|
||||
<ItemAcquireModal />
|
||||
<DeletionModal />
|
||||
<ScopeCreateUpdateModal />
|
||||
<SprintCreateUpdateModal />
|
||||
</SprintListContext>
|
||||
</ScopeListContext>
|
||||
</ItemMultiListContext>
|
||||
@@ -0,0 +1,10 @@
|
||||
package genutils
|
||||
|
||||
func Flatten2[T any](arrs [][]T) []T {
|
||||
res := make([]T, 0, 128)
|
||||
for _, arr := range arrs {
|
||||
res = append(res, arr...)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
+44
-24
@@ -9,34 +9,22 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func Sprints(g *gin.RouterGroup, sprintsService *sprints.Service) {
|
||||
func SprintsAllScopes(g *gin.RouterGroup, sprintsService *sprints.Service) {
|
||||
g.GET("", handler("sprints", func(c *gin.Context) (interface{}, error) {
|
||||
from := time.Now().Add(-time.Hour * 24 * 7)
|
||||
if fromQuery := c.Query("from"); fromQuery != "" {
|
||||
parsed, err := time.Parse(time.RFC3339Nano, fromQuery)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "from",
|
||||
Problem: "Incorrect date format: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
from = parsed
|
||||
from, to, err := getFromToFromQuery(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
to := time.Now().Add(time.Hour * 24 * 7)
|
||||
if toQuery := c.Query("to"); toQuery != "" {
|
||||
parsed, err := time.Parse(time.RFC3339Nano, toQuery)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "to",
|
||||
Problem: "Incorrect date format: " + err.Error(),
|
||||
}
|
||||
}
|
||||
return sprintsService.ListBetween(c.Request.Context(), from, to)
|
||||
}))
|
||||
}
|
||||
|
||||
to = parsed
|
||||
func Sprints(g *gin.RouterGroup, sprintsService *sprints.Service) {
|
||||
g.GET("", handler("sprints", func(c *gin.Context) (interface{}, error) {
|
||||
from, to, err := getFromToFromQuery(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sprintsService.ListScopedBetween(c.Request.Context(), from, to)
|
||||
@@ -126,3 +114,35 @@ func Sprints(g *gin.RouterGroup, sprintsService *sprints.Service) {
|
||||
return sprintsService.Delete(c.Request.Context(), sprintID)
|
||||
}))
|
||||
}
|
||||
|
||||
func getFromToFromQuery(c *gin.Context) (time.Time, time.Time, error) {
|
||||
from := time.Now().Add(-time.Hour * 24 * 7)
|
||||
if fromQuery := c.Query("from"); fromQuery != "" {
|
||||
parsed, err := time.Parse(time.RFC3339Nano, fromQuery)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "from",
|
||||
Problem: "Incorrect date format: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
from = parsed
|
||||
}
|
||||
|
||||
to := time.Now().Add(time.Hour * 24 * 7)
|
||||
if toQuery := c.Query("to"); toQuery != "" {
|
||||
parsed, err := time.Parse(time.RFC3339Nano, toQuery)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "to",
|
||||
Problem: "Incorrect date format: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
to = parsed
|
||||
}
|
||||
|
||||
return from, to, nil
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ AND (
|
||||
OR (from_time <= ? AND to_time > ?)
|
||||
-- or from and to time are on each their side of from..to
|
||||
OR (from_time >= ? AND to_time < ?)
|
||||
);
|
||||
|
||||
)
|
||||
ORDER BY from_time, name;
|
||||
|
||||
-- name: GetSprint :one
|
||||
SELECT * FROM sprint WHERE id = ? AND scope_id = ?;
|
||||
|
||||
@@ -88,6 +88,7 @@ func (r *statsRepository) List(ctx context.Context, scopeIDs ...int) ([]entities
|
||||
Select("id, scope_id, name, description, weight, allowed_amounts, is_primary").
|
||||
From("stat").
|
||||
Where(squirrel.Eq{"scope_id": scopeIDs}).
|
||||
OrderBy("is_primary DESC", "name").
|
||||
RunWith(r.db).
|
||||
Query()
|
||||
if err == sql.ErrNoRows {
|
||||
|
||||
@@ -125,7 +125,7 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
|
||||
return nil, models.PermissionDeniedError{}
|
||||
}
|
||||
|
||||
if filter.ScopeIDs != nil {
|
||||
if len(filter.ScopeIDs) > 0 {
|
||||
userScopes, err := sc.Scopes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -185,7 +185,26 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
|
||||
|
||||
res := make([]Result, 0, len(items))
|
||||
for _, item := range items {
|
||||
res = append(res, generateResult(item, sc.Scope, progresses, requirements, projects))
|
||||
scope := &sc.Scope
|
||||
if scope.ID != item.ScopeID {
|
||||
userScopes, err := sc.Scopes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, userScope := range userScopes {
|
||||
if userScope.ID == item.ScopeID {
|
||||
scope = &userScope
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if scope.ID != item.ScopeID {
|
||||
return nil, models.NotFoundError("Scope")
|
||||
}
|
||||
}
|
||||
|
||||
res = append(res, generateResult(item, *scope, progresses, requirements, projects))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
|
||||
@@ -195,12 +195,12 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
|
||||
continue
|
||||
}
|
||||
|
||||
if item.AcquiredTime != nil {
|
||||
for _, stat := range item.Stats {
|
||||
if resStats[stat.ID] != nil {
|
||||
for _, stat := range item.Stats {
|
||||
if resStats[stat.ID] != nil {
|
||||
if item.AcquiredTime != nil {
|
||||
resStats[stat.ID].Acquired += stat.Acquired
|
||||
resStats[stat.ID].Planned += stat.Required
|
||||
}
|
||||
resStats[stat.ID].Planned += stat.Required
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ func generateResult(scope entities.Scope, members []entities.ScopeMember, stats
|
||||
Owner: member.Owner,
|
||||
}
|
||||
}
|
||||
|
||||
if stats != nil {
|
||||
res.Stats = make([]ResultStat, 0, len(stats))
|
||||
for _, stat := range stats {
|
||||
|
||||
@@ -37,25 +37,67 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
|
||||
return s.fill(ctx, *sprint, parts)
|
||||
}
|
||||
|
||||
func (s *Service) ListBetween(ctx context.Context, from, to time.Time) ([]Result, error) {
|
||||
allScopes, err := s.Scopes.Context(ctx).Scopes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allSprints := make([][]entities.Sprint, len(allScopes))
|
||||
|
||||
eg, ctx2 := errgroup.WithContext(ctx)
|
||||
for i, scope := range allScopes {
|
||||
list := &allSprints[i]
|
||||
scopeID := scope.ID
|
||||
|
||||
eg.Go(func() (err error) {
|
||||
*list, err = s.Repository.ListBetween(ctx2, scopeID, from, to)
|
||||
return
|
||||
})
|
||||
}
|
||||
err = eg.Wait()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sprints := genutils.Flatten2(allSprints)
|
||||
sort.Slice(sprints, func(i, j int) bool {
|
||||
si := sprints[i]
|
||||
sj := sprints[j]
|
||||
|
||||
if si.FromTime.Equal(sj.FromTime) {
|
||||
return si.Name < sj.Name
|
||||
}
|
||||
|
||||
return si.FromTime.Before(sj.FromTime)
|
||||
})
|
||||
|
||||
return s.fillMany(ctx, sprints)
|
||||
}
|
||||
|
||||
func (s *Service) ListScopedBetween(ctx context.Context, from, to time.Time) ([]Result, error) {
|
||||
sprints, err := s.Repository.ListBetween(ctx, s.Scopes.Context(ctx).ID, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.fillMany(ctx, sprints)
|
||||
}
|
||||
|
||||
func (s *Service) fillMany(ctx context.Context, sprints []entities.Sprint) ([]Result, error) {
|
||||
parts, err := s.Repository.ListParts(ctx, sprints...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
eg, ctx2 := errgroup.WithContext(ctx)
|
||||
|
||||
results := make([]Result, len(sprints))
|
||||
for i := range sprints {
|
||||
iCopy := i
|
||||
|
||||
eg.Go(func() error {
|
||||
res, err := s.fill(ctx, sprints[iCopy], parts)
|
||||
res, err := s.fill(ctx2, sprints[iCopy], parts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -309,7 +351,28 @@ func (s *Service) Delete(ctx context.Context, id int) (*Result, error) {
|
||||
|
||||
func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []entities.SprintPart) (*Result, error) {
|
||||
res := Result{Sprint: sprint}
|
||||
|
||||
var allStats []scopes.ResultStat
|
||||
sc := s.Scopes.Context(ctx)
|
||||
if sc.ID != sprint.ScopeID {
|
||||
allScopes, err := sc.Scopes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, scope := range allScopes {
|
||||
if scope.ID == sprint.ScopeID {
|
||||
allStats = scope.Stats
|
||||
break
|
||||
}
|
||||
}
|
||||
if allStats == nil {
|
||||
return nil, models.NotFoundError("Scope")
|
||||
}
|
||||
} else {
|
||||
allStats = sc.Scope.Stats
|
||||
}
|
||||
|
||||
partIDs := make([]int, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if part.SprintID == sprint.ID {
|
||||
@@ -317,8 +380,6 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
||||
}
|
||||
}
|
||||
|
||||
allStats := sc.Scope.Stats
|
||||
|
||||
progressMap := make(map[int]int, len(allStats))
|
||||
res.Progress = make([]ResultProgress, 0, len(allStats))
|
||||
res.PartIDs = partIDs
|
||||
@@ -326,7 +387,8 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
||||
switch res.Kind {
|
||||
case models.SprintKindItems:
|
||||
pickedItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
|
||||
IDs: partIDs,
|
||||
IDs: partIDs,
|
||||
ScopeIDs: []int{sprint.ScopeID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -425,6 +487,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
||||
acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
|
||||
AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime},
|
||||
StatIDs: partIDs,
|
||||
ScopeIDs: []int{sprint.ScopeID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -434,6 +497,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
||||
case models.SprintKindScope:
|
||||
acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
|
||||
AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime},
|
||||
ScopeIDs: []int{sprint.ScopeID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user