Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eff677cc69 | ||
|
|
702b2254c8 | ||
|
|
972ad748f2 | ||
|
|
2f4c03b9af | ||
|
|
0bbfe3689e | ||
|
|
a93ff5b292 | ||
|
|
ae408390a5 | ||
|
|
5987b7c8e1 | ||
|
|
e268c1fd97 | ||
|
|
7b7a46eae9 | ||
|
|
6825584b8d | ||
|
|
a530bed697 | ||
|
|
e65349662a | ||
|
|
fd5d732851 | ||
|
|
9259609efe | ||
|
|
a5b9f85eda |
@@ -25,6 +25,14 @@ type ItemStat struct {
|
|||||||
Required int `json:"required"`
|
Required int `json:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (item *Item) AcquiredOutside(fromTime, toTime time.Time) bool {
|
||||||
|
return item.AcquiredTime != nil && (item.AcquiredTime.Before(fromTime) || !item.AcquiredTime.Before(toTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item *Item) AcquiredBetween(fromTime, toTime time.Time) bool {
|
||||||
|
return item.AcquiredTime != nil && !item.AcquiredTime.Before(fromTime) && item.AcquiredTime.Before(toTime)
|
||||||
|
}
|
||||||
|
|
||||||
func (item *Item) ApplyUpdate(update models.ItemUpdate) {
|
func (item *Item) ApplyUpdate(update models.ItemUpdate) {
|
||||||
if update.RequirementID != nil {
|
if update.RequirementID != nil {
|
||||||
if *update.RequirementID <= 0 {
|
if *update.RequirementID <= 0 {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -13,7 +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
|
groupOptions?: import("$lib/models/items").GroupItemsOptions
|
||||||
amplifyConfig: any
|
amplifyConfig: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export const handle: Handle = async({ event, resolve }) => {
|
|||||||
for (const key in newCookies) {
|
for (const key in newCookies) {
|
||||||
response.headers.append("Set-Cookie", cookie.serialize(key, newCookies[key], {
|
response.headers.append("Set-Cookie", cookie.serialize(key, newCookies[key], {
|
||||||
path: "/",
|
path: "/",
|
||||||
|
maxAge: 8640000,
|
||||||
|
expires: new Date(Date.now() + 8640000000),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { TimeInterval } from "$lib/models/common";
|
import type { TimeInterval } from "$lib/models/common";
|
||||||
import type Item from "$lib/models/item";
|
import type Item from "$lib/models/item";
|
||||||
import type { ItemFilter, ItemInput } from "$lib/models/item";
|
import type { GroupItemsOptions, ItemFilter, ItemGroup, ItemInput } from "$lib/models/item";
|
||||||
import type Project from "$lib/models/project";
|
import type Project from "$lib/models/project";
|
||||||
import type { ProjectEntry, ProjectInput, Requirement, RequirementInput } from "$lib/models/project";
|
import type { ProjectEntry, ProjectInput, Requirement, RequirementInput } from "$lib/models/project";
|
||||||
import type { ScopeInput } from "$lib/models/scope";
|
import type { ScopeInput } from "$lib/models/scope";
|
||||||
@@ -73,10 +73,19 @@ export default class SL3APIClient {
|
|||||||
return this.fetch<{project: Project}>("PUT", `scopes/${scopeId}/projects/${projectId}`, input).then(r => r.project);
|
return this.fetch<{project: Project}>("PUT", `scopes/${scopeId}/projects/${projectId}`, input).then(r => r.project);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listItems(scopeId: number | "ALL", filter: ItemFilter): Promise<Item[]> {
|
async listItems(scopeId: number | "ALL", filter: ItemFilter, grouped: true, groupOptions: GroupItemsOptions): Promise<{items: Item[], groups: ItemGroup[]}>
|
||||||
|
async listItems(scopeId: number | "ALL", filter: ItemFilter, grouped?: false): Promise<Item[]>
|
||||||
|
async listItems(scopeId: number | "ALL", filter: ItemFilter, grouped?: boolean, groupOptions?: GroupItemsOptions): Promise<Item[] | {items: Item[], groups: ItemGroup[]}> {
|
||||||
const scopePrefix = (scopeId !== "ALL") ? `scopes/${scopeId}` : "all-scopes";
|
const scopePrefix = (scopeId !== "ALL") ? `scopes/${scopeId}` : "all-scopes";
|
||||||
|
const groupedSuffix = !!grouped ? `&grouped=true&hideCreated=${groupOptions?.hideCreated}&hideAcquired=${groupOptions?.hideAcquired}&hideScheduled=${groupOptions?.hideScheduled}` : "";
|
||||||
|
|
||||||
return this.fetch<{items: Item[]}>("GET", `${scopePrefix}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items);
|
return this.fetch<{items: Item[], groups: ItemGroup[]}>("GET", `${scopePrefix}/items?filter=${encodeURIComponent(JSON.stringify(filter))}${groupedSuffix}`).then(r => {
|
||||||
|
if (!grouped) {
|
||||||
|
return r.items
|
||||||
|
}
|
||||||
|
|
||||||
|
return {groups: r.groups, items: r.items}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createItem(scopeId: number, input: ItemInput): Promise<Item> {
|
async createItem(scopeId: number, input: ItemInput): Promise<Item> {
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
|
|
||||||
<div class="markdown">{@html outputHtml}</div>
|
<div class="markdown">{@html outputHtml}</div>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
|
@import "../../css/colors.sass";
|
||||||
|
|
||||||
div.markdown :global(p) {
|
div.markdown :global(p) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
@@ -58,6 +60,17 @@
|
|||||||
counter-reset: li;
|
counter-reset: li;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.markdown :global(a) {
|
||||||
|
color: #d1a31b;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: 250ms;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #FFCC11;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div.markdown :global(ul) :global(li:before) {
|
div.markdown :global(ul) :global(li:before) {
|
||||||
content: "\2022"; /* Add content: \2022 is the CSS Code/unicode for a bullet */
|
content: "\2022"; /* Add content: \2022 is the CSS Code/unicode for a bullet */
|
||||||
color: #333; /* Change the color */
|
color: #333; /* Change the color */
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
import { getScopeContext } from "./ScopeContext.svelte";
|
import { getScopeContext } from "./ScopeContext.svelte";
|
||||||
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";
|
||||||
import { groupItems } from "$lib/utils/items";
|
|
||||||
|
|
||||||
export let items: Item[];
|
export let items: Item[];
|
||||||
export let groups: ItemGroup[];
|
export let groups: ItemGroup[];
|
||||||
@@ -50,9 +49,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newItems = await sl3(fetch).listItems($scope.id, filter)
|
const res = await sl3(fetch).listItems($scope.id, filter, true)
|
||||||
itemsWritable.set(newItems);
|
itemsWritable.set(res.items);
|
||||||
groupsWritable.set(groupItems(newItems, filter.scheduledDate.min))
|
groupsWritable.set(res.groups);
|
||||||
} catch(_) {}
|
} catch(_) {}
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
for (const stat of actualScope.stats) {
|
for (const stat of actualScope.stats) {
|
||||||
const found = value.find(v => v.statId == stat.id);
|
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;
|
requiredMap[stat.id] = found?.required || 0;
|
||||||
acquiredMap[stat.id] = found?.acquired || 0;
|
acquiredMap[stat.id] = found?.acquired || 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { formatFormTime } from "$lib/utils/date";
|
import { formatFormTime } from "$lib/utils/date";
|
||||||
import parseInterval from "$lib/utils/timeinterval";
|
import parseInterval, { morningInterval } from "$lib/utils/timeinterval";
|
||||||
|
|
||||||
export let from: string;
|
export let from: string;
|
||||||
export let to: string;
|
export let to: string;
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (intervalName !== "specific_dates") {
|
if (intervalName !== "specific_dates") {
|
||||||
const int = parseInterval(intervalName, openDate);
|
const int = morningInterval(parseInterval(intervalName, openDate));
|
||||||
from = formatFormTime(int.min);
|
from = formatFormTime(int.min);
|
||||||
to = formatFormTime(int.max);
|
to = formatFormTime(int.max);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
|
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
|
||||||
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||||
import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot";
|
import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot";
|
||||||
|
import { faEye } from "@fortawesome/free-solid-svg-icons/faEye";
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
"clock": faClock,
|
"clock": faClock,
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
"chevron_down": faChevronDown,
|
"chevron_down": faChevronDown,
|
||||||
"trash": faTrash,
|
"trash": faTrash,
|
||||||
"check_slot": faCheckToSlot,
|
"check_slot": faCheckToSlot,
|
||||||
|
"eye": faEye,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconName = keyof typeof icons;
|
export type IconName = keyof typeof icons;
|
||||||
|
|||||||
@@ -6,14 +6,15 @@
|
|||||||
import SprintBody from "./SprintBody.svelte";
|
import SprintBody from "./SprintBody.svelte";
|
||||||
import { projectPrettyId } from "$lib/utils/prettyIds";
|
import { projectPrettyId } from "$lib/utils/prettyIds";
|
||||||
import Section from "../layout/Section.svelte";
|
import Section from "../layout/Section.svelte";
|
||||||
|
import Icon from "../layout/Icon.svelte";
|
||||||
|
|
||||||
export let sprint: Sprint;
|
export let sprint: Sprint;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Section anchor={projectPrettyId(sprint)} noProgress title={sprint.name}>
|
<Section anchor={projectPrettyId(sprint)} noProgress title={sprint.name}>
|
||||||
<OptionsRow slot="right">
|
<OptionsRow slot="right">
|
||||||
<Option open={{name: "sprint.edit", sprint}}>Edit</Option>
|
<Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
|
||||||
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
|
<Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
|
||||||
</OptionsRow>
|
</OptionsRow>
|
||||||
<SprintBody sprint={sprint} />
|
<SprintBody sprint={sprint} />
|
||||||
</Section>
|
</Section>
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
|
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
|
||||||
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
||||||
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
|
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
|
||||||
|
import Icon from "../layout/Icon.svelte";
|
||||||
|
|
||||||
export let sprint: Sprint;
|
export let sprint: Sprint;
|
||||||
const {scope, lastHistoryRange} = getScopeContext();
|
const {scope, lastHistoryRange} = getScopeContext();
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
|
|
||||||
function gotoSprint() {
|
function gotoSprint() {
|
||||||
let sc = $scope;
|
let sc = $scope;
|
||||||
if (sc.id !== sprint.id) {
|
if (sc.id !== sprint.scopeId) {
|
||||||
sc = $scopes.find(s => s.id === sprint.scopeId);
|
sc = $scopes.find(s => s.id === sprint.scopeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +27,9 @@
|
|||||||
|
|
||||||
<SubSection noProgress title={sprint.name}>
|
<SubSection noProgress title={sprint.name}>
|
||||||
<OptionsRow slot="right">
|
<OptionsRow slot="right">
|
||||||
<Option on:click={gotoSprint}>View</Option>
|
<Option on:click={gotoSprint}><Icon name="eye" /></Option>
|
||||||
<Option open={{name: "sprint.edit", sprint}}>Edit</Option>
|
<Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
|
||||||
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
|
<Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
|
||||||
</OptionsRow>
|
</OptionsRow>
|
||||||
<SprintBody compact sprint={sprint} />
|
<SprintBody compact sprint={sprint} />
|
||||||
</SubSection>
|
</SubSection>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import Amount from "../common/Amount.svelte";
|
import Amount from "../common/Amount.svelte";
|
||||||
import AmountRow from "../common/AmountRow.svelte";
|
import AmountRow from "../common/AmountRow.svelte";
|
||||||
import Markdown from "../common/Markdown.svelte";
|
import Markdown from "../common/Markdown.svelte";
|
||||||
|
import Icon from "../layout/Icon.svelte";
|
||||||
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 SubSection from "../layout/SubSection.svelte";
|
import SubSection from "../layout/SubSection.svelte";
|
||||||
@@ -12,8 +13,8 @@
|
|||||||
|
|
||||||
<SubSection noProgress title={stat.name}>
|
<SubSection noProgress title={stat.name}>
|
||||||
<OptionsRow slot="right">
|
<OptionsRow slot="right">
|
||||||
<Option open={{name: "stat.edit", stat}}>Edit</Option>
|
<Option open={{name: "stat.edit", stat}}><Icon name="pen" /></Option>
|
||||||
<Option open={{name: "stat.delete", stat}} color="red">Delete</Option>
|
<Option open={{name: "stat.delete", stat}} color="red"><Icon name="trash" /></Option>
|
||||||
</OptionsRow>
|
</OptionsRow>
|
||||||
<Markdown source={stat.description} />
|
<Markdown source={stat.description} />
|
||||||
{#if stat.weight !== 1}
|
{#if stat.weight !== 1}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
<label for="description">Description</label>
|
<label for="description">Description</label>
|
||||||
<textarea name="description" bind:value={item.description} />
|
<textarea name="description" bind:value={item.description} />
|
||||||
<label for="stats">Stats</label>
|
<label for="stats">Stats</label>
|
||||||
<StatInput showAcquired hideUnseen noToggle bind:value={item.stats} />
|
<StatInput overrideScopeId={scopeId} showAcquired hideUnseen noToggle bind:value={item.stats} />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
</form>
|
</form>
|
||||||
@@ -103,7 +103,7 @@ import Checkbox from "$lib/components/layout/Checkbox.svelte";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form on:submit|preventDefault={submit}>
|
<form on:submit|preventDefault={submit}>
|
||||||
<Modal closable show={show} verb={op} noun="Item" disabled={loading} error={error}>
|
<Modal closable show={show} verb={op} noun="Stat" disabled={loading} error={error}>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input name="name" type="text" bind:value={stat.name} />
|
<input name="name" type="text" bind:value={stat.name} />
|
||||||
|
|||||||
@@ -68,9 +68,17 @@ export interface ItemGroup {
|
|||||||
export interface ItemGroupReference {
|
export interface ItemGroupReference {
|
||||||
idx: number
|
idx: number
|
||||||
event: "created" | "acquired" | "scheduled"
|
event: "created" | "acquired" | "scheduled"
|
||||||
|
time: string
|
||||||
|
|
||||||
sortKey?: string
|
sortKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GroupItemsOptions {
|
||||||
|
hideCreated?: boolean
|
||||||
|
hideAcquired?: boolean
|
||||||
|
hideScheduled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// this_week
|
// this_week
|
||||||
// this_month
|
// this_month
|
||||||
// all_time
|
// all_time
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import type { ItemGroup, ItemGroupReference } from "$lib/models/item";
|
|
||||||
import type Item from "$lib/models/item";
|
|
||||||
import { formatDate, formatDateTime, formatTime, formatWeekdayDate } from "./date";
|
|
||||||
|
|
||||||
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> = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < items.length; ++i) {
|
|
||||||
const item = items[i];
|
|
||||||
const createKey = formatDate(item.createdTime)
|
|
||||||
const createSortKey = `${createKey} ${formatTime(item.createdTime)}`
|
|
||||||
|
|
||||||
if (!opts.hideCreated) {
|
|
||||||
if (!item.acquiredTime || item.acquiredTime >= item.createdTime) {
|
|
||||||
addItem(groups, createKey, i, createSortKey, item, "created");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.hideScheduled) {
|
|
||||||
if (item.scheduledDate) {
|
|
||||||
addItem(groups, item.scheduledDate, i, `${item.scheduledDate} 23:59:59`, item, "scheduled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.hideAcquired) {
|
|
||||||
if (item.acquiredTime) {
|
|
||||||
addItem(groups, formatDate(item.acquiredTime), i, formatDateTime(item.acquiredTime), item, "acquired");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(groups).sort((a,b) => (
|
|
||||||
b.localeCompare(a)
|
|
||||||
)).map(k => ({
|
|
||||||
...groups[k],
|
|
||||||
list: groups[k].list.sort((a, b) => {
|
|
||||||
return b.sortKey.localeCompare(a.sortKey);
|
|
||||||
}).map(r => ({...r, sortKey: void(0)})),
|
|
||||||
})).filter(g => minDate == null || g.label > minDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addItem(groups: Record<string, ItemGroup>, key: string, index: number, sortKey: string, item: Item, event: ItemGroupReference["event"]) {
|
|
||||||
if (groups[key] == null) {
|
|
||||||
groups[key] = {
|
|
||||||
label: formatWeekdayDate(sortKey),
|
|
||||||
list: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const i = groups[key].list.findIndex(r => r.idx === index)
|
|
||||||
if (i == -1) {
|
|
||||||
groups[key].list.push({
|
|
||||||
idx: index,
|
|
||||||
event: event,
|
|
||||||
sortKey: sortKey,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
groups[key].list[i].event = event;
|
|
||||||
groups[key].list[i].sortKey = sortKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,9 +25,9 @@ export default function parseInterval(s: string, date: Date): TimeInterval<Date>
|
|||||||
const [amount, unit] = unitNumber(args);
|
const [amount, unit] = unitNumber(args);
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case "d": case "days":
|
case "d": case "days":
|
||||||
return {display: "days", min: startOfDay(addDays(date, -amount)), max: startOfDay(addDays(date, 1))}
|
return {display: "days", min: startOfDay(addDays(date, -(amount - 1))), max: startOfDay(addDays(date, 1))}
|
||||||
case "w": case "weeks":
|
case "w": case "weeks":
|
||||||
return {display: "weeks", min: startOfDay(addDays(date, -amount * 7)), max: startOfDay(addDays(date, 1))}
|
return {display: "weeks", min: startOfDay(addDays(date, -((amount * 7) - 1))), max: startOfDay(addDays(date, 1))}
|
||||||
case "m": case "months":
|
case "m": case "months":
|
||||||
return {display: "days", min: startOfDay(addMonths(date, -amount)), max: startOfDay(addDays(date, 1))}
|
return {display: "days", min: startOfDay(addMonths(date, -amount)), max: startOfDay(addDays(date, 1))}
|
||||||
case "y": case "years":
|
case "y": case "years":
|
||||||
@@ -55,6 +55,19 @@ export default function parseInterval(s: string, date: Date): TimeInterval<Date>
|
|||||||
case "last_year":
|
case "last_year":
|
||||||
return {display: "year", min: startOfYear(addYears(date, -1)), max: startOfYear(date)}
|
return {display: "year", min: startOfYear(addYears(date, -1)), max: startOfYear(date)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function morningInterval(interval: TimeInterval<Date>) {
|
||||||
|
if (!interval) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
min: new Date(interval.min.getTime() + 3600000 * 5),
|
||||||
|
max: new Date(interval.max.getTime() + 3600000 * 5),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function intervalLabel(s: string): string {
|
export function intervalLabel(s: string): string {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
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 { 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";
|
||||||
|
|
||||||
@@ -16,13 +15,12 @@
|
|||||||
acquiredTime: interval,
|
acquiredTime: interval,
|
||||||
scheduledDate: datesOf(interval),
|
scheduledDate: datesOf(interval),
|
||||||
};
|
};
|
||||||
const items = await sl3(fetch).listItems(scopeId, filter);
|
const {items, groups} = await sl3(fetch).listItems(scopeId, filter, true, session.groupOptions);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
items, filter,
|
items, groups, filter,
|
||||||
intervalString: params.interval,
|
intervalString: params.interval,
|
||||||
groups: groupItems(items, datesOf(interval)?.min, session.groupOptions || {}),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -41,7 +39,6 @@
|
|||||||
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 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[]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
scheduledDate: datesOf(parseInterval("next:7d", now)),
|
scheduledDate: datesOf(parseInterval("next:7d", now)),
|
||||||
},
|
},
|
||||||
acquiredFilter: {
|
acquiredFilter: {
|
||||||
acquiredTime: parseInterval("today", now),
|
acquiredTime: morningInterval(parseInterval("today", now)),
|
||||||
},
|
},
|
||||||
looseFilter: {
|
looseFilter: {
|
||||||
loose: true,
|
loose: true,
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { sl3 } from "$lib/clients/sl3";
|
||||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||||
import Column from "$lib/components/layout/Column.svelte";
|
import Column from "$lib/components/layout/Column.svelte";
|
||||||
import Columns from "$lib/components/layout/Columns.svelte";
|
import Columns from "$lib/components/layout/Columns.svelte";
|
||||||
@@ -44,8 +45,7 @@
|
|||||||
import StatSubSection from "$lib/components/scope/StatSubSection.svelte";
|
import StatSubSection from "$lib/components/scope/StatSubSection.svelte";
|
||||||
import DeletionModal from "$lib/modals/DeletionModal.svelte";
|
import DeletionModal from "$lib/modals/DeletionModal.svelte";
|
||||||
import StatCreateEditModal from "$lib/modals/StatCreateEditModal.svelte";
|
import StatCreateEditModal from "$lib/modals/StatCreateEditModal.svelte";
|
||||||
import { sl3 } from "$lib/clients/sl3";
|
import parseInterval, { datesOf, morningInterval } from "$lib/utils/timeinterval";
|
||||||
import parseInterval, { datesOf } from "$lib/utils/timeinterval";
|
|
||||||
import type Item from "$lib/models/item";
|
import type Item from "$lib/models/item";
|
||||||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
|
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
|
||||||
import Card from "$lib/components/common/Card.svelte";
|
import Card from "$lib/components/common/Card.svelte";
|
||||||
@@ -62,6 +62,7 @@
|
|||||||
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
|
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||||
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
|
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
|
||||||
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
|
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
|
||||||
|
import Icon from "$lib/components/layout/Icon.svelte";
|
||||||
|
|
||||||
export let acquiredItems: Item[];
|
export let acquiredItems: Item[];
|
||||||
export let scheduledItems: Item[];
|
export let scheduledItems: Item[];
|
||||||
@@ -106,13 +107,13 @@
|
|||||||
<Column>
|
<Column>
|
||||||
<Row title="Sprints">
|
<Row title="Sprints">
|
||||||
<OptionsRow slot="right">
|
<OptionsRow slot="right">
|
||||||
<Option open={{name: "sprint.create"}}>Create</Option>
|
<Option open={{name: "sprint.create"}}><Icon name="plus" /></Option>
|
||||||
</OptionsRow>
|
</OptionsRow>
|
||||||
<SprintList sub />
|
<SprintList sub />
|
||||||
</Row>
|
</Row>
|
||||||
<Row title="Stats">
|
<Row title="Stats">
|
||||||
<OptionsRow slot="right">
|
<OptionsRow slot="right">
|
||||||
<Option open={{name: "stat.create"}}>Create</Option>
|
<Option open={{name: "stat.create"}}><Icon name="plus" /></Option>
|
||||||
</OptionsRow>
|
</OptionsRow>
|
||||||
{#each $scope.stats as stat (stat.id)}
|
{#each $scope.stats as stat (stat.id)}
|
||||||
<StatSubSection stat={stat} />
|
<StatSubSection stat={stat} />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
scheduledDate: datesOf(parseInterval("next:7d", now)),
|
scheduledDate: datesOf(parseInterval("next:7d", now)),
|
||||||
},
|
},
|
||||||
acquiredFilter: {
|
acquiredFilter: {
|
||||||
acquiredTime: parseInterval("today", now),
|
acquiredTime: morningInterval(parseInterval("today", now)),
|
||||||
},
|
},
|
||||||
looseFilter: {
|
looseFilter: {
|
||||||
loose: true,
|
loose: true,
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const load: Load = async({ fetch, session }) => {
|
export const load: Load = async({ fetch }) => {
|
||||||
const scopes = await sl3(fetch).listScopes();
|
const scopes = await sl3(fetch).listScopes();
|
||||||
|
|
||||||
const {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(new Date());
|
const {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(new Date());
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
|
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
|
||||||
import Option from "$lib/components/layout/Option.svelte";
|
import Option from "$lib/components/layout/Option.svelte";
|
||||||
import ScopeListContext from "$lib/components/contexts/ScopeListContext.svelte";
|
import ScopeListContext from "$lib/components/contexts/ScopeListContext.svelte";
|
||||||
import parseInterval, { datesOf } from "$lib/utils/timeinterval";
|
import parseInterval, { datesOf, morningInterval } from "$lib/utils/timeinterval";
|
||||||
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
|
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||||
import type Item from "$lib/models/item";
|
import type Item from "$lib/models/item";
|
||||||
import DeletionModal from "$lib/modals/DeletionModal.svelte";
|
import DeletionModal from "$lib/modals/DeletionModal.svelte";
|
||||||
@@ -58,6 +58,7 @@
|
|||||||
import SprintCreateUpdateModal from "$lib/modals/SprintCreateUpdateModal.svelte";
|
import SprintCreateUpdateModal from "$lib/modals/SprintCreateUpdateModal.svelte";
|
||||||
import SprintListContext from "$lib/components/contexts/SprintListContext.svelte";
|
import SprintListContext from "$lib/components/contexts/SprintListContext.svelte";
|
||||||
import type Sprint from "$lib/models/sprint";
|
import type Sprint from "$lib/models/sprint";
|
||||||
|
import Icon from "$lib/components/layout/Icon.svelte";
|
||||||
|
|
||||||
export let scopes: Scope[] = [];
|
export let scopes: Scope[] = [];
|
||||||
export let acquiredItems: Item[];
|
export let acquiredItems: Item[];
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
<Column>
|
<Column>
|
||||||
<Row title="Scopes">
|
<Row title="Scopes">
|
||||||
<OptionsRow slot="right">
|
<OptionsRow slot="right">
|
||||||
<Option open={{name: "scope.create"}}>Create</Option>
|
<Option open={{name: "scope.create"}}><Icon name="plus" /></Option>
|
||||||
</OptionsRow>
|
</OptionsRow>
|
||||||
<ScopeLinkList />
|
<ScopeLinkList />
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 112 KiB |
@@ -8,3 +8,12 @@ func Map[I any, O any](arr []I, cb func(a I) O) []O {
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MapValues[K comparable, V any](m map[K]V) []V {
|
||||||
|
res := make([]V, 0, len(m))
|
||||||
|
for _, v := range m {
|
||||||
|
res = append(res, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ func (d Date) ToTime() time.Time {
|
|||||||
return time.Date(d[0], time.Month(d[1]), d[2], 0, 0, 0, 0, time.UTC)
|
return time.Date(d[0], time.Month(d[1]), d[2], 0, 0, 0, 0, time.UTC)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d Date) WithTime(hh, mm, ss int) time.Time {
|
||||||
|
return d.WithTimeAndLocation(hh, mm, ss, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Date) WithTimeAndLocation(hh, mm, ss int, loc *time.Location) time.Time {
|
||||||
|
return time.Date(d[0], time.Month(d[1]), d[2], hh, mm, ss, 0, loc)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Date) UnmarshalJSON(b []byte) error {
|
func (d *Date) UnmarshalJSON(b []byte) error {
|
||||||
var str string
|
var str string
|
||||||
err := json.Unmarshal(b, &str)
|
err := json.Unmarshal(b, &str)
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ func (s Status) Less(s2 Status) bool {
|
|||||||
return s < s2
|
return s < s2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Status) Ended() bool {
|
||||||
|
switch s {
|
||||||
|
case Completed, Failed, Dropped:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Blocked Status = iota
|
Blocked Status = iota
|
||||||
Available
|
Available
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ func getterHandler[O any](key, idKey string, callback func(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin.HandlerFunc {
|
func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin.HandlerFunc {
|
||||||
|
type overrider interface {
|
||||||
|
OverrideJSONResponse() bool
|
||||||
|
}
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
res, err := callback(c)
|
res, err := callback(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -82,6 +86,11 @@ func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o, ok := res.(overrider); ok && o.OverrideJSONResponse() {
|
||||||
|
c.JSON(200, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
resJson := make(map[string]interface{}, 1)
|
resJson := make(map[string]interface{}, 1)
|
||||||
resJson[key] = res
|
resJson[key] = res
|
||||||
|
|
||||||
|
|||||||
+51
-10
@@ -12,25 +12,50 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ItemsAllScopes(g *gin.RouterGroup, items *items.Service) {
|
type groupedRes struct {
|
||||||
|
Items []items.Result `json:"items"`
|
||||||
|
Groups []items.Group `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g groupedRes) OverrideJSONResponse() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ItemsAllScopes(g *gin.RouterGroup, itemService *items.Service) {
|
||||||
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
|
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
|
||||||
filter, err := getItemFilterFromQuery(c)
|
filter, err := getItemFilterFromQuery(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.ListAllScopes(c.Request.Context(), filter)
|
res, err := itemService.ListAllScopes(c.Request.Context(), filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Query("grouped") == "true" {
|
||||||
|
return groupedRes{
|
||||||
|
Items: res,
|
||||||
|
Groups: items.GenerateGroups(res, items.GroupOptions{
|
||||||
|
HideCreated: c.Query("hideCreated") == "true",
|
||||||
|
HideAcquired: c.Query("hideAcquired") == "true",
|
||||||
|
HideScheduled: c.Query("hideScheduled") == "true",
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Items(g *gin.RouterGroup, items *items.Service) {
|
func Items(g *gin.RouterGroup, itemService *items.Service) {
|
||||||
g.GET("/:item_id", handler("project", func(c *gin.Context) (interface{}, error) {
|
g.GET("/:item_id", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||||
id, err := reqInt(c, "item_id")
|
id, err := reqInt(c, "item_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.Find(c.Request.Context(), id)
|
return itemService.Find(c.Request.Context(), id)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
|
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -39,7 +64,23 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.ListScoped(c.Request.Context(), filter)
|
res, err := itemService.ListScoped(c.Request.Context(), filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Query("grouped") == "true" {
|
||||||
|
return groupedRes{
|
||||||
|
Items: res,
|
||||||
|
Groups: items.GenerateGroups(res, items.GroupOptions{
|
||||||
|
HideCreated: c.Query("hideCreated") == "true",
|
||||||
|
HideAcquired: c.Query("hideAcquired") == "true",
|
||||||
|
HideScheduled: c.Query("hideScheduled") == "true",
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
g.POST("", handler("item", func(c *gin.Context) (interface{}, error) {
|
g.POST("", handler("item", func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -55,7 +96,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.Create(c.Request.Context(), input.Item, input.Stats)
|
return itemService.Create(c.Request.Context(), input.Item, input.Stats)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
g.PUT("/:item_id", handler("item", func(c *gin.Context) (interface{}, error) {
|
g.PUT("/:item_id", handler("item", func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -76,7 +117,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.Update(c.Request.Context(), id, input.ItemUpdate, input.Stats)
|
return itemService.Update(c.Request.Context(), id, input.ItemUpdate, input.Stats)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
g.PUT("/:item_id/stats/:stat_id", handler("item", func(c *gin.Context) (interface{}, error) {
|
g.PUT("/:item_id/stats/:stat_id", handler("item", func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -100,7 +141,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
|||||||
|
|
||||||
input.StatID = statID
|
input.StatID = statID
|
||||||
|
|
||||||
return items.UpdateStat(c.Request.Context(), itemID, input)
|
return itemService.UpdateStat(c.Request.Context(), itemID, input)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
g.DELETE("/:item_id/stats/:stat_id", handler("item", func(c *gin.Context) (interface{}, error) {
|
g.DELETE("/:item_id/stats/:stat_id", handler("item", func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -113,7 +154,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.DeleteStat(c.Request.Context(), itemID, statID)
|
return itemService.DeleteStat(c.Request.Context(), itemID, statID)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
g.DELETE("/:item_id", handler("item", func(c *gin.Context) (interface{}, error) {
|
g.DELETE("/:item_id", handler("item", func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -122,7 +163,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.Delete(c.Request.Context(), id)
|
return itemService.Delete(c.Request.Context(), id)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,37 +191,21 @@ func (q *Queries) ListSprintsAt(ctx context.Context, arg ListSprintsAtParams) ([
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listSprintsBetween = `-- name: ListSprintsBetween :many
|
const listSprintsBetween = `-- name: ListSprintsBetween :many
|
||||||
SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required FROM sprint WHERE scope_id = ?
|
SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required FROM sprint
|
||||||
AND (
|
WHERE scope_id = ?
|
||||||
-- from time is within from..to
|
AND from_time < ?
|
||||||
(from_time <= ? AND to_time > ?)
|
AND to_time >= ?
|
||||||
-- or to time is within from..to
|
ORDER BY from_time, name
|
||||||
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 < ?)
|
|
||||||
)
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListSprintsBetweenParams struct {
|
type ListSprintsBetweenParams struct {
|
||||||
ScopeID int
|
ScopeID int
|
||||||
FromTime time.Time
|
ToTime time.Time
|
||||||
ToTime time.Time
|
FromTime time.Time
|
||||||
FromTime_2 time.Time
|
|
||||||
ToTime_2 time.Time
|
|
||||||
FromTime_3 time.Time
|
|
||||||
ToTime_3 time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListSprintsBetween(ctx context.Context, arg ListSprintsBetweenParams) ([]Sprint, error) {
|
func (q *Queries) ListSprintsBetween(ctx context.Context, arg ListSprintsBetweenParams) ([]Sprint, error) {
|
||||||
rows, err := q.query(ctx, q.listSprintsBetweenStmt, listSprintsBetween,
|
rows, err := q.query(ctx, q.listSprintsBetweenStmt, listSprintsBetween, arg.ScopeID, arg.ToTime, arg.FromTime)
|
||||||
arg.ScopeID,
|
|
||||||
arg.FromTime,
|
|
||||||
arg.ToTime,
|
|
||||||
arg.FromTime_2,
|
|
||||||
arg.ToTime_2,
|
|
||||||
arg.FromTime_3,
|
|
||||||
arg.ToTime_3,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,10 @@
|
|||||||
SELECT * FROM sprint WHERE scope_id = ? AND from_time <= sqlc.arg(time) AND to_time > sqlc.arg(time);
|
SELECT * FROM sprint WHERE scope_id = ? AND from_time <= sqlc.arg(time) AND to_time > sqlc.arg(time);
|
||||||
|
|
||||||
-- name: ListSprintsBetween :many
|
-- name: ListSprintsBetween :many
|
||||||
SELECT * FROM sprint WHERE scope_id = ?
|
SELECT * FROM sprint
|
||||||
AND (
|
WHERE scope_id = sqlc.arg(scope_id)
|
||||||
-- from time is within from..to
|
AND from_time < sqlc.arg(to_time)
|
||||||
(from_time <= ? AND to_time > ?)
|
AND to_time >= sqlc.arg(from_time)
|
||||||
-- or to time is within from..to
|
|
||||||
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;
|
ORDER BY from_time, name;
|
||||||
|
|
||||||
-- name: GetSprint :one
|
-- name: GetSprint :one
|
||||||
|
|||||||
@@ -75,13 +75,9 @@ func (r *sprintRepository) ListAt(ctx context.Context, scopeID int, at time.Time
|
|||||||
|
|
||||||
func (r *sprintRepository) ListBetween(ctx context.Context, scopeID int, from, to time.Time) ([]entities.Sprint, error) {
|
func (r *sprintRepository) ListBetween(ctx context.Context, scopeID int, from, to time.Time) ([]entities.Sprint, error) {
|
||||||
rows, err := r.q.ListSprintsBetween(ctx, mysqlcore.ListSprintsBetweenParams{
|
rows, err := r.q.ListSprintsBetween(ctx, mysqlcore.ListSprintsBetweenParams{
|
||||||
ScopeID: scopeID,
|
ScopeID: scopeID,
|
||||||
FromTime: from,
|
FromTime: from,
|
||||||
ToTime: from,
|
ToTime: to,
|
||||||
FromTime_2: to,
|
|
||||||
ToTime_2: to,
|
|
||||||
FromTime_3: from,
|
|
||||||
ToTime_3: to,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
|
|||||||
@@ -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").
|
Select("id, scope_id, name, description, weight, allowed_amounts, is_primary").
|
||||||
From("stat").
|
From("stat").
|
||||||
Where(squirrel.Eq{"scope_id": scopeIDs}).
|
Where(squirrel.Eq{"scope_id": scopeIDs}).
|
||||||
|
OrderBy("is_primary DESC", "name").
|
||||||
RunWith(r.db).
|
RunWith(r.db).
|
||||||
Query()
|
Query()
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package items
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
Label string `json:"label,omitempty"`
|
||||||
|
List []GroupReference `json:"list,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupOptions struct {
|
||||||
|
HideCreated bool `json:"hideCreated,omitempty"`
|
||||||
|
HideScheduled bool `json:"hideScheduled,omitempty"`
|
||||||
|
HideAcquired bool `json:"hideAcquired,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupReference struct {
|
||||||
|
Index int `json:"idx"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
|
||||||
|
sortKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateGroups(items []Result, opts GroupOptions) []Group {
|
||||||
|
groups := make(map[string]Group, 16)
|
||||||
|
|
||||||
|
// TODO: Get the scope from somewhere else, if necessary.
|
||||||
|
|
||||||
|
for i, item := range items {
|
||||||
|
if !opts.HideCreated && (item.AcquiredTime == nil || item.AcquiredTime.After(item.CreatedTime)) {
|
||||||
|
addItem(groups, i, "created", item.CreatedTime, tz)
|
||||||
|
}
|
||||||
|
if !opts.HideScheduled && item.ScheduledDate != nil {
|
||||||
|
addItem(groups, i, "scheduled", item.ScheduledDate.WithTimeAndLocation(23, 59, 59, tz), tz)
|
||||||
|
}
|
||||||
|
if !opts.HideAcquired && item.AcquiredTime != nil {
|
||||||
|
addItem(groups, i, "acquired", *item.AcquiredTime, tz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
sort.Slice(group.List, func(i, j int) bool {
|
||||||
|
return group.List[j].sortKey < group.List[i].sortKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
groupSlice := genutils.MapValues(groups)
|
||||||
|
sort.Slice(groupSlice, func(i, j int) bool {
|
||||||
|
return groupSlice[j].Label < groupSlice[i].Label
|
||||||
|
})
|
||||||
|
|
||||||
|
return groupSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
func addItem(groups map[string]Group, index int, event string, eventDate time.Time, tz *time.Location) {
|
||||||
|
groupDate := eventDate.In(tz).Add(-time.Minute * 300)
|
||||||
|
eventDate = eventDate.In(tz)
|
||||||
|
|
||||||
|
groupKey := groupDate.Format("2006-01-02")
|
||||||
|
var sortKey string
|
||||||
|
if event == "scheduled" {
|
||||||
|
sortKey = eventDate.Format("2006-01-02") + " 23:59:59.999"
|
||||||
|
} else {
|
||||||
|
sortKey = eventDate.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Why, Go, must you be so.
|
||||||
|
group, ok := groups[groupKey]
|
||||||
|
if !ok {
|
||||||
|
group = Group{
|
||||||
|
Label: groupDate.Format("2006-01-02 (Monday)"),
|
||||||
|
List: make([]GroupReference, 0, 16),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, ref := range group.List {
|
||||||
|
if ref.Index == index {
|
||||||
|
group.List = append(group.List[:i], group.List[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.List = append(group.List, GroupReference{
|
||||||
|
Index: index,
|
||||||
|
Event: event,
|
||||||
|
Time: eventDate.Format("15:04"),
|
||||||
|
sortKey: sortKey,
|
||||||
|
})
|
||||||
|
groups[groupKey] = group
|
||||||
|
}
|
||||||
|
|
||||||
|
var tz *time.Location
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
tz, err = time.LoadLocation("Europe/Oslo")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,8 +136,12 @@ func generateResult(
|
|||||||
resReq := generateRequirementResult(req, scope, requirementStats, projectItems)
|
resReq := generateRequirementResult(req, scope, requirementStats, projectItems)
|
||||||
|
|
||||||
res.TotalRequired += resReq.TotalRequired
|
res.TotalRequired += resReq.TotalRequired
|
||||||
res.TotalAcquired += resReq.TotalAcquired
|
|
||||||
res.TotalPlanned += resReq.TotalPlanned
|
res.TotalPlanned += resReq.TotalPlanned
|
||||||
|
if req.Status.Ended() {
|
||||||
|
res.TotalAcquired += resReq.TotalRequired
|
||||||
|
} else {
|
||||||
|
res.TotalAcquired += resReq.TotalAcquired
|
||||||
|
}
|
||||||
|
|
||||||
res.Requirements = append(res.Requirements, resReq)
|
res.Requirements = append(res.Requirements, resReq)
|
||||||
}
|
}
|
||||||
@@ -195,12 +199,12 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.AcquiredTime != nil {
|
for _, stat := range item.Stats {
|
||||||
for _, stat := range item.Stats {
|
if resStats[stat.ID] != nil {
|
||||||
if resStats[stat.ID] != nil {
|
if item.AcquiredTime != nil {
|
||||||
resStats[stat.ID].Acquired += stat.Acquired
|
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,
|
Owner: member.Owner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stats != nil {
|
if stats != nil {
|
||||||
res.Stats = make([]ResultStat, 0, len(stats))
|
res.Stats = make([]ResultStat, 0, len(stats))
|
||||||
for _, stat := range stats {
|
for _, stat := range stats {
|
||||||
|
|||||||
@@ -85,6 +85,21 @@ func (s *Service) ListScopedBetween(ctx context.Context, from, to time.Time) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) fillMany(ctx context.Context, sprints []entities.Sprint) ([]Result, error) {
|
func (s *Service) fillMany(ctx context.Context, sprints []entities.Sprint) ([]Result, error) {
|
||||||
|
sort.Slice(sprints, func(i, j int) bool {
|
||||||
|
si := sprints[i]
|
||||||
|
sj := sprints[j]
|
||||||
|
|
||||||
|
if si.IsTimed != sj.IsTimed {
|
||||||
|
return si.IsTimed
|
||||||
|
}
|
||||||
|
|
||||||
|
if !si.FromTime.Equal(sj.FromTime) {
|
||||||
|
return si.FromTime.Before(sj.FromTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return si.ID < sj.ID
|
||||||
|
})
|
||||||
|
|
||||||
parts, err := s.Repository.ListParts(ctx, sprints...)
|
parts, err := s.Repository.ListParts(ctx, sprints...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -406,6 +421,10 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
|||||||
for _, stat := range allStats {
|
for _, stat := range allStats {
|
||||||
totalRequired := 0
|
totalRequired := 0
|
||||||
for _, item := range res.Items {
|
for _, item := range res.Items {
|
||||||
|
if item.AcquiredOutside(sprint.FromTime, sprint.ToTime) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if itemStat := item.Stat(stat.ID); itemStat != nil {
|
if itemStat := item.Stat(stat.ID); itemStat != nil {
|
||||||
totalRequired += itemStat.Required
|
totalRequired += itemStat.Required
|
||||||
}
|
}
|
||||||
@@ -507,9 +526,10 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
|||||||
for _, stat := range allStats {
|
for _, stat := range allStats {
|
||||||
progressMap[stat.ID] = len(res.Progress)
|
progressMap[stat.ID] = len(res.Progress)
|
||||||
res.Progress = append(res.Progress, ResultProgress{
|
res.Progress = append(res.Progress, ResultProgress{
|
||||||
ID: stat.ID,
|
ID: stat.ID,
|
||||||
Name: stat.Name,
|
Name: stat.Name,
|
||||||
Weight: stat.Weight,
|
Weight: stat.Weight,
|
||||||
|
Required: genutils.Ptr(0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,7 +551,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
|||||||
|
|
||||||
// Measure progress
|
// Measure progress
|
||||||
for _, item := range res.Items {
|
for _, item := range res.Items {
|
||||||
if item.AcquiredTime == nil || item.AcquiredTime.Before(sprint.FromTime) || !item.AcquiredTime.Before(sprint.ToTime) {
|
if !item.AcquiredBetween(sprint.FromTime, sprint.ToTime) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user