Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ab0a8555 | ||
|
|
22885734c7 | ||
|
|
795d49a942 | ||
|
|
d083c9b1e8 | ||
|
|
0e9b1a4930 | ||
|
|
6bbbf33c83 | ||
|
|
ad53ec5c7e | ||
|
|
1195f6ab03 | ||
|
|
2bd68930fe | ||
|
|
0ef4e38b8a | ||
|
|
48b5c17166 | ||
|
|
2fcb6a5d1a | ||
|
|
0dfda26c09 | ||
|
|
701932546d | ||
|
|
287b18e2e1 | ||
|
|
75dd79127e | ||
|
|
da6cdfdb0d | ||
|
|
7eb5f07299 | ||
|
|
d114f0d38c | ||
|
|
a3336b183c | ||
|
|
d8a269328f | ||
|
|
297e7895fb | ||
|
|
0cba681ddc | ||
|
|
2777617bfc |
+1
-31
@@ -10,7 +10,7 @@ steps:
|
||||
- go generate ./...
|
||||
- go test -v ./...
|
||||
|
||||
- name: docker-backend-release-tag
|
||||
- name: docker-backend-tag
|
||||
image: plugins/docker
|
||||
settings:
|
||||
auto_tag: true
|
||||
@@ -24,20 +24,6 @@ steps:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: docker-backend-release-latest
|
||||
image: plugins/docker
|
||||
settings:
|
||||
auto_tag: false
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: r.vmaple.dev/stufflog3/backend
|
||||
registry: r.vmaple.dev
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: docker-frontend-tag
|
||||
image: plugins/docker
|
||||
settings:
|
||||
@@ -53,19 +39,3 @@ steps:
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: docker-frontend-latest
|
||||
image: plugins/docker
|
||||
settings:
|
||||
dockerfile: ./frontend/Dockerfile
|
||||
context: ./frontend
|
||||
auto_tag: false
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: r.vmaple.dev/stufflog3/frontend
|
||||
registry: r.vmaple.dev
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
@@ -36,6 +36,7 @@ type Requirement struct {
|
||||
ProjectID int `json:"projectId"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsCoarse bool `json:"isCoarse"`
|
||||
Status models.Status `json:"status"`
|
||||
}
|
||||
|
||||
@@ -49,6 +50,9 @@ func (requirement *Requirement) Update(update models.RequirementUpdate) {
|
||||
if update.Status != nil && update.Status.Valid() {
|
||||
requirement.Status = *update.Status
|
||||
}
|
||||
if update.IsCoarse != nil {
|
||||
requirement.IsCoarse = *update.IsCoarse
|
||||
}
|
||||
}
|
||||
|
||||
type RequirementStat struct {
|
||||
|
||||
@@ -17,6 +17,7 @@ RUN npm ci
|
||||
# Build templates
|
||||
RUN npm run build
|
||||
RUN cp package.json ./build/
|
||||
RUN cp -r node_modules ./build/
|
||||
RUN ls -lh ./build
|
||||
|
||||
## 2. Regroup
|
||||
|
||||
Vendored
+2
@@ -13,6 +13,8 @@ declare namespace App {
|
||||
user?: import("$lib/models/user").default
|
||||
idToken?: string
|
||||
req?: any
|
||||
groupOptions?: import("$lib/utils/items").GroupItemsOptions
|
||||
amplifyConfig: any
|
||||
}
|
||||
|
||||
interface Stuff {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
var global = global || window
|
||||
var Buffer = Buffer || []
|
||||
var process = process || { env: { DEBUG: undefined }, version: [] }
|
||||
|
||||
var fetch = window.fetch.bind(window)
|
||||
</script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { GetSession, Handle } from '@sveltejs/kit';
|
||||
|
||||
Amplify.configure({
|
||||
Auth: {
|
||||
region: import.meta.env.VITE_STUFFLOG3_AWS_POOL_REGION,
|
||||
userPoolId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_ID,
|
||||
userPoolWebClientId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_CLIENT_ID,
|
||||
region: process.env.STUFFLOG3_AWS_POOL_REGION,
|
||||
userPoolId: process.env.STUFFLOG3_AWS_POOL_ID,
|
||||
userPoolWebClientId: process.env.STUFFLOG3_AWS_POOL_PUBLIC_CLIENT_ID,
|
||||
},
|
||||
ssr: true,
|
||||
});
|
||||
@@ -16,7 +16,12 @@ export const getSession: GetSession = ({ locals, request }) => {
|
||||
return {
|
||||
idToken: locals.idToken,
|
||||
user: locals.user,
|
||||
req: { headers: { cookie: request.headers.get("Cookie") } }
|
||||
req: { headers: { cookie: request.headers.get("Cookie") } },
|
||||
amplifyConfig: {
|
||||
region: process.env.STUFFLOG3_AWS_POOL_REGION,
|
||||
userPoolId: process.env.STUFFLOG3_AWS_POOL_ID,
|
||||
userPoolWebClientId: process.env.STUFFLOG3_AWS_POOL_PUBLIC_CLIENT_ID,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -73,8 +73,10 @@ export default class SL3APIClient {
|
||||
return this.fetch<{project: Project}>("PUT", `scopes/${scopeId}/projects/${projectId}`, input).then(r => r.project);
|
||||
}
|
||||
|
||||
async listItems(scopeId: number, filter: ItemFilter): Promise<Item[]> {
|
||||
return this.fetch<{items: Item[]}>("GET", `scopes/${scopeId}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items);
|
||||
async listItems(scopeId: number | "ALL", filter: ItemFilter): Promise<Item[]> {
|
||||
const scopePrefix = (scopeId !== "ALL") ? `scopes/${scopeId}` : "all-scopes";
|
||||
|
||||
return this.fetch<{items: Item[]}>("GET", `${scopePrefix}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items);
|
||||
}
|
||||
|
||||
async createItem(scopeId: number, input: ItemInput): Promise<Item> {
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
<script lang="ts">
|
||||
import PlusMinus from "../layout/PlusMinus.svelte"
|
||||
import shortestDecimal from "$lib/utils/numbers";
|
||||
|
||||
import PlusMinus from "../layout/PlusMinus.svelte"
|
||||
|
||||
export let label: string
|
||||
export let value: number
|
||||
export let target: number = null
|
||||
export let right: boolean = false;
|
||||
export let dark: boolean = false;
|
||||
|
||||
let valueRounded: number;
|
||||
let targetRounded: number;
|
||||
let valueString: string;
|
||||
let targetString: string;
|
||||
$: {
|
||||
valueRounded = Math.round(value * 1000) / 1000;
|
||||
|
||||
if (target != null) {
|
||||
targetRounded = Math.round(target * 1000) / 1000;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="amount">
|
||||
<span class="value">{value}×</span>
|
||||
<div class="amount" class:right>
|
||||
<span class:dark class="value">{shortestDecimal(valueRounded)}×</span>
|
||||
<span class="label">{label}</span>
|
||||
{ #if target != null && target != value }
|
||||
<span class="target">(<PlusMinus offset={value - target} />)</span>
|
||||
{ #if target != null && targetRounded != valueRounded }
|
||||
<span class="target">(<PlusMinus offset={valueRounded - targetRounded} />)</span>
|
||||
{ /if }
|
||||
</div>
|
||||
|
||||
@@ -19,9 +35,14 @@ import PlusMinus from "../layout/PlusMinus.svelte"
|
||||
|
||||
div.amount {
|
||||
padding-right: 1ch;
|
||||
|
||||
&.right {
|
||||
padding-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
span.label {
|
||||
span.label, span.dark {
|
||||
opacity: $opacity-entry4;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,24 +8,26 @@
|
||||
<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>
|
||||
<option selected={value === "last:7d" } value="last:7d">Last 7 days</option>
|
||||
<option selected={value === "last:14d" } value="last:14d">Last 14 days</option>
|
||||
<option selected={value === "last:30d" } value="last:30d">Last 30 days</option>
|
||||
<option selected={value === "last:90d" } value="last:90d">Last 90 days</option>
|
||||
<option selected={value === "last:1m" } value="last:1m">Last 1 month</option>
|
||||
<option selected={value === "last:3m" } value="last:3m">Last 3 months</option>
|
||||
<option selected={value === "last:6m" } value="last:6m">Last 6 months</option>
|
||||
<option selected={value === "last:1y" } value="last:1y">Last 1 year</option>
|
||||
<option selected={value === "this_week" } value="this_week">This week</option>
|
||||
<option selected={value === "next_week" } value="next_week">Next week</option>
|
||||
<option selected={value === "last_week" } value="last_week">Last week</option>
|
||||
<option selected={value === "this_month" } value="this_month">This month</option>
|
||||
<option selected={value === "next_month" } value="next_month">Next month</option>
|
||||
<option selected={value === "last_month" } value="last_month">Last month</option>
|
||||
<option selected={value === "all_time" } value="all_time">All time</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="par">)</div>
|
||||
<div class="par">)
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
@media screen and (max-width: 749px) {
|
||||
> div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts" context="module">
|
||||
const contextKey = {ctx: "itemMultiListContext"};
|
||||
|
||||
interface ItemMultiListContextData {
|
||||
lists: Readable<Record<string, Item[]>>,
|
||||
reloadItemLists(): void,
|
||||
};
|
||||
|
||||
const fallback: ItemMultiListContextData = {
|
||||
lists: readable({}),
|
||||
reloadItemLists() {},
|
||||
}
|
||||
|
||||
export function getItemMultiListContext() {
|
||||
return getContext(contextKey) as ItemMultiListContextData || fallback
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { readable, writable, type Readable } from "svelte/store";
|
||||
import { getContext, setContext } from "svelte";
|
||||
import type Item from "$lib/models/item";
|
||||
import type { ItemFilter } from "$lib/models/item";
|
||||
import { getScopeContext } from "./ScopeContext.svelte";
|
||||
import deepEqual from "$lib/utils/object";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
|
||||
export let lists: Record<string, Item[]>
|
||||
export let filters: Record<string, ItemFilter>
|
||||
|
||||
let loaded: Record<string, ItemFilter> = {...filters};
|
||||
let loading: Record<string, boolean> = {};
|
||||
const listsWritable = writable<Record<string, Item[]>>({...lists});
|
||||
|
||||
function reloadItemLists() {
|
||||
loaded = {};
|
||||
}
|
||||
|
||||
async function runLoad(key: string, filter: ItemFilter) {
|
||||
loading = {...loading, [key]: true};
|
||||
|
||||
try {
|
||||
const items = await sl3(fetch).listItems($scope.id, filter)
|
||||
listsWritable.update(l => ({...l, [key.replace("Filter", "Items")]: items}));
|
||||
} catch(_err) {
|
||||
// TODO: Use err
|
||||
}
|
||||
|
||||
loading = {...loading, [key]: false};
|
||||
loaded = {...loaded, [key]: filter};
|
||||
}
|
||||
|
||||
setContext<ItemMultiListContextData>(contextKey, {
|
||||
lists: {subscribe: listsWritable.subscribe},
|
||||
reloadItemLists,
|
||||
});
|
||||
|
||||
$: for (const key in filters) {
|
||||
if (loading[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (loaded[key] == null || !deepEqual(filters[key], loaded[key])) {
|
||||
runLoad(key, {...filters[key]})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot></slot>
|
||||
@@ -7,7 +7,7 @@
|
||||
};
|
||||
|
||||
const fallback: ProjectContextData = {
|
||||
project: writable({} as Project),
|
||||
project: readable({requirements: []} as Project),
|
||||
reloadProject: () => Promise.resolve()
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import type Project from "$lib/models/project";
|
||||
import { writable, type Readable } from "svelte/store";
|
||||
import { readable, writable, type Readable } from "svelte/store";
|
||||
import { getContext, setContext } from "svelte";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getScopeContext } from "./ScopeContext.svelte";
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
export let hideUnseen = false;
|
||||
export let zeroDeletes = false;
|
||||
export let noToggle = false;
|
||||
export let initOnList: number[] = [];
|
||||
|
||||
let acquiredMap: Record<number, number>;
|
||||
let requiredMap: Record<number, number>;
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
if (hideUnseen) {
|
||||
for (const v of value) {
|
||||
enabledMap[v.statId] = zeroDeletes ? v.required > 0 : v.required >= 0;
|
||||
enabledMap[v.statId] = initOnList.includes(v.statId) || (zeroDeletes ? v.required > 0 : v.required >= 0);
|
||||
requiredMap[v.statId] = v.required;
|
||||
acquiredMap[v.statId] = v.acquired;
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
|
||||
<style lang="sass">
|
||||
div.desktop
|
||||
@media screen and (max-width: 800px)
|
||||
@media screen and (max-width: 749px)
|
||||
display: none
|
||||
|
||||
div.mobile
|
||||
@media screen and (min-width: 800px)
|
||||
@media screen and (min-width: 750px)
|
||||
display: none
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Icon, { type IconName } from "../layout/Icon.svelte";
|
||||
|
||||
export let name: IconName;
|
||||
export let value: boolean;
|
||||
export let inverse: boolean = false;
|
||||
|
||||
const dispatcher = createEventDispatcher();
|
||||
|
||||
function onClick() {
|
||||
value = !value;
|
||||
dispatcher("click");
|
||||
}
|
||||
</script>
|
||||
|
||||
<span on:click={onClick} class="toggle-icon" class:checked={value !== inverse}>
|
||||
<Icon name={name} />
|
||||
</span>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../css/colors"
|
||||
|
||||
.toggle-icon
|
||||
font-size: 0.666em
|
||||
color: $color-entry1
|
||||
user-select: none
|
||||
-moz-user-select: none
|
||||
-webkit-user-select: none
|
||||
|
||||
&.checked
|
||||
color: $color-entry7
|
||||
</style>
|
||||
@@ -14,6 +14,6 @@
|
||||
flex-basis: 50
|
||||
|
||||
&.hideMobile
|
||||
@media screen and (max-width: 800px)
|
||||
@media screen and (max-width: 749px)
|
||||
display: none
|
||||
</style>
|
||||
@@ -25,7 +25,7 @@
|
||||
&.fullwidth
|
||||
width: 100%
|
||||
|
||||
@media screen and (max-width: 800px)
|
||||
@media screen and (max-width: 749px)
|
||||
width: 100%
|
||||
flex-direction: column
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb";
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
|
||||
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";
|
||||
|
||||
const icons = {
|
||||
"clock": faClock,
|
||||
@@ -62,6 +64,8 @@
|
||||
"search": faSearch,
|
||||
"chevron_right": faChevronRight,
|
||||
"chevron_down": faChevronDown,
|
||||
"trash": faTrash,
|
||||
"check_slot": faCheckToSlot,
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
@@ -51,21 +51,6 @@
|
||||
div.header
|
||||
display: flex
|
||||
|
||||
h2
|
||||
font-size: 1.25em
|
||||
margin: 0
|
||||
color: $color-entry10
|
||||
|
||||
span.sub
|
||||
color: $color-entry5
|
||||
|
||||
&.big
|
||||
margin-top: 1em
|
||||
font-size: 2em
|
||||
|
||||
&.small
|
||||
font-size: 1em
|
||||
|
||||
div.right
|
||||
color: $color-entry2-transparent
|
||||
margin-left: auto
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let centered = false;
|
||||
export let vertical = false;
|
||||
</script>
|
||||
|
||||
<div class="option-row" class:centered>
|
||||
<div class="option-row" class:vertical class:centered>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
@@ -10,10 +11,18 @@
|
||||
div.option-row {
|
||||
margin-left: -0.4ch;
|
||||
margin-right: -0.4ch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
div.option-row.centered {
|
||||
text-align: center;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
div.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import shortestDecimal from "$lib/utils/numbers";
|
||||
|
||||
|
||||
export let offset: number;
|
||||
|
||||
let value: string;
|
||||
let negative: boolean
|
||||
$: value = (offset > 0) ? `+${offset.toFixed()}` : offset.toFixed();
|
||||
$: value = (offset > 0) ? `+${shortestDecimal(offset)}` : shortestDecimal(offset);
|
||||
$: negative = offset < 0
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
export let noProgress = false;
|
||||
export let icon: IconName = null;
|
||||
export let status: Status = null;
|
||||
export let anchor: string = "";
|
||||
</script>
|
||||
|
||||
<div class="sl3-entry">
|
||||
<div id={anchor} class="sl3-entry">
|
||||
<div class="header">
|
||||
<div class="entry-icon">
|
||||
{#if icon != null}
|
||||
@@ -44,33 +45,20 @@
|
||||
opacity: 0.6
|
||||
margin-right: 0.5ch
|
||||
font-size: 1em
|
||||
margin-top: 0.225em
|
||||
margin-top: 0.15em
|
||||
|
||||
&:empty
|
||||
display: none
|
||||
|
||||
div.header
|
||||
display: flex
|
||||
|
||||
h2
|
||||
font-size: 1.25em
|
||||
margin: 0
|
||||
color: $color-entry10
|
||||
|
||||
span.sub
|
||||
color: $color-entry5
|
||||
|
||||
&.big
|
||||
margin-top: 1em
|
||||
font-size: 2em
|
||||
|
||||
&.small
|
||||
font-size: 1em
|
||||
line-height: 1.25em
|
||||
|
||||
div.right
|
||||
color: $color-entry2-transparent
|
||||
margin-left: auto
|
||||
margin-top: auto
|
||||
margin-bottom: 0
|
||||
|
||||
&.noProgress
|
||||
margin-top: -0.25em
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
div.sub-section
|
||||
margin: 0.5em 0.4ch
|
||||
padding: 0.33em 0.75ch
|
||||
padding-bottom: 0.25em
|
||||
background: $color-entry1-transparent
|
||||
border-radius: 0.125em
|
||||
border-bottom-right-radius: 0.5em
|
||||
@@ -54,7 +55,6 @@
|
||||
opacity: 0.6
|
||||
margin-right: 1.5ch
|
||||
font-size: 0.75em
|
||||
margin-top: auto
|
||||
|
||||
span.event
|
||||
font-size: 1.33em
|
||||
@@ -64,7 +64,8 @@
|
||||
display: none
|
||||
|
||||
&.blankEvent
|
||||
margin-right: 0.5ch
|
||||
margin-top: 0.35em
|
||||
margin-right: 0.75ch
|
||||
|
||||
span.event
|
||||
display: none
|
||||
@@ -78,6 +79,6 @@
|
||||
margin-top: auto
|
||||
|
||||
&.noProgress
|
||||
margin-top: -0.25em
|
||||
margin-top: 0em
|
||||
margin-bottom: auto
|
||||
</style>
|
||||
@@ -8,40 +8,49 @@
|
||||
import Amount from "../common/Amount.svelte";
|
||||
import AmountRow from "../common/AmountRow.svelte";
|
||||
import type { IconName } from "../layout/Icon.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
import { formatTime } from "$lib/utils/date";
|
||||
|
||||
export let item: Item;
|
||||
export let event: "created" | "scheduled" | "acquired" | "none" = "none";
|
||||
export let compact = false;
|
||||
export let showAcquiredTime = false;
|
||||
|
||||
let icon: IconName
|
||||
let status: Status
|
||||
let eventText: string
|
||||
|
||||
$: {
|
||||
icon = void(0);
|
||||
|
||||
if (event === "none" || event === "acquired") {
|
||||
if (item.acquiredTime) {
|
||||
icon = "check";
|
||||
status = Status.Completed;
|
||||
eventText = (showAcquiredTime || event === "acquired") ? formatTime(item.acquiredTime) : void(0)
|
||||
}
|
||||
} else if (event === "scheduled") {
|
||||
if (item.acquiredTime) {
|
||||
icon = "hourglass";
|
||||
status = Status.Blocked;
|
||||
eventText = "scheduled"
|
||||
}
|
||||
} else if (event === "created") {
|
||||
icon = "lightbulb";
|
||||
status = Status.Available;
|
||||
icon = "lightbulb";
|
||||
status = Status.Available;
|
||||
eventText = formatTime(item.createdTime)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubSection small noProgress title={item.name} icon={icon} status={status} event={event}>
|
||||
<SubSection small noProgress title={item.name} icon={icon} status={status} event={eventText}>
|
||||
<OptionsRow slot="right">
|
||||
{#if item.acquiredTime == null}
|
||||
<Option open={{name: "item.acquire", item}} color="green">Acquire</Option>
|
||||
<Option open={{name: "item.acquire", item}} color="green"><Icon name="check_slot" /></Option>
|
||||
{/if}
|
||||
{#if !compact}
|
||||
<Option open={{name: "item.edit", item}}>Edit</Option>
|
||||
<Option open={{name: "item.delete", item}} color="red">Delete</Option>
|
||||
<Option open={{name: "item.edit", item}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "item.delete", item}} color="red"><Icon name="trash" /></Option>
|
||||
{/if}
|
||||
</OptionsRow>
|
||||
{#if !compact}
|
||||
@@ -54,12 +63,18 @@
|
||||
<Amount label={stat.name} value={stat.acquired} target={stat.required} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if item.weightedRequired}
|
||||
<Amount dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
|
||||
{/if}
|
||||
{:else}
|
||||
{#each item.stats as stat (stat.id)}
|
||||
{#if stat.required > 0}
|
||||
<Amount label={stat.name} value={stat.required} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if item.weightedRequired}
|
||||
<Amount dark right label="" value={item.weightedRequired} />
|
||||
{/if}
|
||||
{/if}
|
||||
</AmountRow>
|
||||
</SubSection>
|
||||
@@ -5,6 +5,7 @@
|
||||
import Option from "$lib/components/layout/Option.svelte";
|
||||
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
|
||||
import { getProjectContext } from "../contexts/ProjectContext.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
import RequirementEntry from "./RequirementSection.svelte";
|
||||
|
||||
const {project} = getProjectContext();
|
||||
@@ -15,9 +16,9 @@
|
||||
<Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} />
|
||||
<Markdown source={$project.description} />
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "requirement.create", project: $project}}>Add Requirement</Option>
|
||||
<Option open={{name: "project.edit", project: $project}}>Edit</Option>
|
||||
<Option open={{name: "project.delete", project: $project}} color="red">Delete</Option>
|
||||
<Option open={{name: "requirement.create", project: $project}}><Icon name="plus" /></Option>
|
||||
<Option open={{name: "project.edit", project: $project}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "project.delete", project: $project}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
{#each $project.requirements as requirement (requirement.id)}
|
||||
<RequirementEntry requirement={requirement} />
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import LabeledProgressRow from "../common/LabeledProgressRow.svelte";
|
||||
import { STATUS_ICONS } from "../common/StatusIcon.svelte";
|
||||
import ItemEntry from "./ItemSubSection.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
|
||||
export let requirement: Requirement;
|
||||
</script>
|
||||
@@ -18,13 +19,15 @@
|
||||
<Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} />
|
||||
<Markdown source={requirement.description} />
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "item.create", requirement}}>Add Item</Option>
|
||||
<Option open={{name: "requirement.edit", requirement}}>Edit</Option>
|
||||
<Option open={{name: "requirement.delete", requirement}} color="red">Delete</Option>
|
||||
<Option open={{name: "item.create", requirement}}><Icon name="plus" /></Option>
|
||||
<Option open={{name: "requirement.edit", requirement}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "requirement.delete", requirement}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
<LabeledProgressRow>
|
||||
{#each requirement.stats as stat (stat.id)}
|
||||
<LabeledProgress count={stat.acquired} target={stat.required} name={stat.name} />
|
||||
{#if !requirement.isCoarse || stat.required > 0}
|
||||
<LabeledProgress count={stat.acquired} target={stat.required} name={stat.name} />
|
||||
{/if}
|
||||
{/each}
|
||||
</LabeledProgressRow>
|
||||
{#each requirement.items as item (item.id)}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { getItemMultiListContext } from "../contexts/ItemMultiListContext.svelte";
|
||||
import Row from "../layout/Row.svelte";
|
||||
import ItemSubSection from "../project/ItemSubSection.svelte";
|
||||
|
||||
export let key: string;
|
||||
export let title: string;
|
||||
export let showAcquiredTime: boolean = false;
|
||||
|
||||
const {lists} = getItemMultiListContext();
|
||||
</script>
|
||||
|
||||
|
||||
{#if ($lists[key]||[]).length > 0}
|
||||
<Row title={title}>
|
||||
{#each $lists[key] as item (item.id)}
|
||||
<ItemSubSection showAcquiredTime={showAcquiredTime} item={item} />
|
||||
{/each}
|
||||
</Row>
|
||||
{/if}
|
||||
@@ -9,6 +9,7 @@
|
||||
import ItemSubSection from "../project/ItemSubSection.svelte";
|
||||
|
||||
export let sprint: Sprint
|
||||
export let compact: boolean = false;
|
||||
|
||||
let timeRange: [Date, Date];
|
||||
|
||||
@@ -33,9 +34,9 @@
|
||||
</LabeledProgressRow>
|
||||
{/if}
|
||||
{#if sprint.kind === SprintKind.Items}
|
||||
{#each sprint.items as item (item.id)}
|
||||
{#if !item.acquiredTime}
|
||||
<ItemSubSection compact item={item} />
|
||||
{#each (sprint.items||[]) as item (item.id)}
|
||||
{#if !compact || !item.acquiredTime}
|
||||
<ItemSubSection compact={compact} item={item} />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
<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 { projectPrettyId } from "$lib/utils/prettyIds";
|
||||
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}>
|
||||
<Section anchor={projectPrettyId(sprint)} 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>
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
||||
|
||||
export let sprint: Sprint;
|
||||
const {scope} = getScopeContext();
|
||||
const {scope, lastHistoryRange} = getScopeContext();
|
||||
|
||||
function gotoSprint() {
|
||||
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`)
|
||||
goto(`/${scopePrettyId($scope)}/sprints/${$lastHistoryRange}#${projectPrettyId(sprint)}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
<Option open={{name: "sprint.edit", sprint}}>Edit</Option>
|
||||
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
|
||||
</OptionsRow>
|
||||
<SprintBody sprint={sprint} />
|
||||
<SprintBody compact sprint={sprint} />
|
||||
</SubSection>
|
||||
@@ -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 { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
@@ -22,6 +23,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
const {project, reloadProject} = getProjectContext();
|
||||
const {reloadItemList} = getItemListContext();
|
||||
const {reloadSprintList} = getSprintListContext();
|
||||
const {reloadItemLists} = getItemMultiListContext();
|
||||
|
||||
let item: ItemInput
|
||||
let itemId: number
|
||||
@@ -29,6 +31,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
let openedDate: Date
|
||||
let requirementName: string
|
||||
let op: string
|
||||
let initOnList: number[] = [];
|
||||
|
||||
let error: string
|
||||
let loading: boolean
|
||||
@@ -38,9 +41,11 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
case "item.create":
|
||||
initCreate($scope, $currentModal.requirement)
|
||||
break;
|
||||
case "item.edit":
|
||||
initEdit($currentModal.item)
|
||||
case "item.edit": {
|
||||
const reqId = $currentModal.item.requirementId;
|
||||
initEdit($currentModal.item, $project.requirements.find(r => r.id === reqId), $scope)
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
loading = false;
|
||||
@@ -52,6 +57,8 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
let stats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
|
||||
if (stats == null) {
|
||||
stats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0}))
|
||||
} else {
|
||||
initOnList = requirement.stats.filter(s => s.required > 0).map(s => s.id);
|
||||
}
|
||||
|
||||
item = {
|
||||
@@ -69,14 +76,19 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
show = true;
|
||||
}
|
||||
|
||||
function initEdit(current: Item) {
|
||||
function initEdit(current: Item, requirement?: Requirement, scope?: Scope) {
|
||||
const req = $project.requirements?.find(r => r.id === current.requirementId);
|
||||
let ctxStats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
|
||||
if (ctxStats == null) {
|
||||
ctxStats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0}))
|
||||
}
|
||||
const inputStats = current.stats.map(s => ({statId: s.id, acquired: s.acquired, required: s.required}));
|
||||
|
||||
item = {
|
||||
name: current.name,
|
||||
description: current.description,
|
||||
requirementId: current.requirementId,
|
||||
stats: current.stats.map(s => ({statId: s.id, acquired: s.acquired, required: s.required})),
|
||||
stats: ctxStats.map(s => inputStats.find(s2 => s2.statId === s.statId) || s),
|
||||
acquiredTime: formatFormTime(current.acquiredTime),
|
||||
scheduledDate: current.scheduledDate,
|
||||
};
|
||||
@@ -134,6 +146,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
|
||||
await reloadItemList();
|
||||
await reloadSprintList();
|
||||
reloadItemLists();
|
||||
|
||||
closeModal();
|
||||
} catch(err) {
|
||||
@@ -165,7 +178,13 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
<input disabled name="req" type="text" value={requirementName} />
|
||||
{/if}
|
||||
<label for="stats">Stats</label>
|
||||
<StatInput showAcquired={op === "Edit" && !!item.acquiredTime} showRequired hideUnseen={!!item.requirementId} bind:value={item.stats} />
|
||||
<StatInput
|
||||
zeroDeletes showRequired
|
||||
initOnList={initOnList}
|
||||
showAcquired={op === "Edit" && !!item.acquiredTime}
|
||||
hideUnseen={!!item.requirementId}
|
||||
bind:value={item.stats}
|
||||
/>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -8,6 +8,7 @@
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import StatInput from "$lib/components/controls/StatInput.svelte";
|
||||
import StatusSelect from "$lib/components/controls/StatusSelect.svelte";
|
||||
import Checkbox from "$lib/components/layout/Checkbox.svelte";
|
||||
import type { ProjectEntry, Requirement, RequirementInput } from "$lib/models/project";
|
||||
import Status from "$lib/models/status";
|
||||
import { statDiff } from "$lib/utils/stat";
|
||||
@@ -49,23 +50,25 @@ import { statDiff } from "$lib/utils/stat";
|
||||
description: "",
|
||||
stats: [],
|
||||
status: Status.Available,
|
||||
isCoarse: false,
|
||||
}
|
||||
|
||||
projectId = project.id;
|
||||
show = true;
|
||||
}
|
||||
|
||||
function initEdit(prev: Requirement, project: ProjectEntry) {
|
||||
function initEdit(current: Requirement, project: ProjectEntry) {
|
||||
requirement = {
|
||||
name: prev.name,
|
||||
description: prev.description,
|
||||
status: prev.status,
|
||||
stats: prev.stats.map(s => ({acquired: s.acquired, required: s.required, statId: s.id})),
|
||||
name: current.name,
|
||||
description: current.description,
|
||||
status: current.status,
|
||||
stats: current.stats.map(s => ({acquired: s.acquired, required: s.required, statId: s.id})),
|
||||
isCoarse: current.isCoarse,
|
||||
}
|
||||
|
||||
oldStats = [...requirement.stats];
|
||||
projectId = project.id;
|
||||
requirementId = prev.id;
|
||||
requirementId = current.id;
|
||||
show = true;
|
||||
}
|
||||
|
||||
@@ -118,6 +121,7 @@ import { statDiff } from "$lib/utils/stat";
|
||||
<StatusSelect bind:status={requirement.status} />
|
||||
<label for="stats">Stats</label>
|
||||
<StatInput showRequired bind:value={requirement.stats} />
|
||||
<Checkbox bind:checked={requirement.isCoarse} label="Hide 0-requirement stats." />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -13,6 +13,8 @@ export default interface Item {
|
||||
acquiredTime?: string
|
||||
scheduledDate?: string
|
||||
stats: StatProgress[]
|
||||
weightedAcquired: number
|
||||
weightedRequired: number
|
||||
}
|
||||
|
||||
export interface ItemInput {
|
||||
@@ -36,6 +38,8 @@ export interface ItemFilter {
|
||||
createdTime?: TimeInterval<Date | string>
|
||||
acquiredTime?: TimeInterval<Date | string>
|
||||
loose?: boolean
|
||||
unAcquired?: boolean
|
||||
unScheduled?: boolean
|
||||
}
|
||||
|
||||
export interface ItemGroup {
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface Requirement {
|
||||
totalAcquired: number
|
||||
totalRequired: number
|
||||
totalPlanned: number
|
||||
isCoarse: boolean
|
||||
stats: StatProgressWithPlanned[]
|
||||
items: Item[]
|
||||
}
|
||||
@@ -45,5 +46,6 @@ export interface RequirementInput {
|
||||
name: string
|
||||
description: string
|
||||
status: Status
|
||||
isCoarse: boolean
|
||||
stats: StatValueInput[]
|
||||
}
|
||||
@@ -2,7 +2,17 @@ import type { ItemGroup, ItemGroupReference } from "$lib/models/item";
|
||||
import type Item from "$lib/models/item";
|
||||
import { formatDate, formatDateTime, formatTime, formatWeekdayDate } from "./date";
|
||||
|
||||
export function groupItems(items: Item[], minDate?: string): ItemGroup[] {
|
||||
export interface GroupItemsOptions {
|
||||
hideCreated?: boolean
|
||||
hideScheduled?: boolean
|
||||
hideAcquired?: boolean
|
||||
}
|
||||
|
||||
export function groupItemOptionsQuery(opts: GroupItemsOptions): string {
|
||||
return `hideCreated=${opts.hideCreated},hideScheduled=${opts.hideScheduled},hideAcquired=${opts.hideAcquired}`;
|
||||
}
|
||||
|
||||
export function groupItems(items: Item[], minDate?: string, opts: GroupItemsOptions = {}): ItemGroup[] {
|
||||
let groups: Record<string, ItemGroup> = {};
|
||||
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
@@ -10,16 +20,22 @@ export function groupItems(items: Item[], minDate?: string): ItemGroup[] {
|
||||
const createKey = formatDate(item.createdTime)
|
||||
const createSortKey = `${createKey} ${formatTime(item.createdTime)}`
|
||||
|
||||
if (!item.acquiredTime || item.acquiredTime >= item.createdTime) {
|
||||
addItem(groups, createKey, i, createSortKey, item, "created");
|
||||
if (!opts.hideCreated) {
|
||||
if (!item.acquiredTime || item.acquiredTime >= item.createdTime) {
|
||||
addItem(groups, createKey, i, createSortKey, item, "created");
|
||||
}
|
||||
}
|
||||
|
||||
if (item.scheduledDate) {
|
||||
addItem(groups, item.scheduledDate, i, `${item.scheduledDate} 23:59:59`, item, "scheduled");
|
||||
if (!opts.hideScheduled) {
|
||||
if (item.scheduledDate) {
|
||||
addItem(groups, item.scheduledDate, i, `${item.scheduledDate} 23:59:59`, item, "scheduled");
|
||||
}
|
||||
}
|
||||
|
||||
if (item.acquiredTime) {
|
||||
addItem(groups, formatDate(item.acquiredTime), i, formatDateTime(item.acquiredTime), item, "acquired");
|
||||
if (!opts.hideAcquired) {
|
||||
if (item.acquiredTime) {
|
||||
addItem(groups, formatDate(item.acquiredTime), i, formatDateTime(item.acquiredTime), item, "acquired");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function shortestDecimal(n: number) {
|
||||
let s = n.toFixed(3);
|
||||
while (s.endsWith("0")) {
|
||||
s = s.slice(0, -1)
|
||||
}
|
||||
if (s.endsWith(".")) {
|
||||
s = s.slice(0, -1)
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
export default function deepEqual(a: Object, b: Object) {
|
||||
if (a === void(0)) {
|
||||
if (b !== void(0)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (a === null) {
|
||||
if (b !== null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a instanceof Date) {
|
||||
if (b instanceof Date) {
|
||||
b = b.toISOString();
|
||||
}
|
||||
a = a.toISOString();
|
||||
|
||||
return a === b;
|
||||
}
|
||||
|
||||
if (Array.isArray(a)) {
|
||||
if (!Array.isArray(b) || b.length !== a.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof(a) === "object") {
|
||||
if (typeof(b) !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key in a) {
|
||||
if (!a.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return a === b;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { TimeInterval } from "../models/common";
|
||||
import { addDays, addMonths, addYears, formatDate, startOfDay, startOfMonth, startOfWeek, startOfYear } from "./date";
|
||||
|
||||
export default function parseInterval(s: string, date: Date): TimeInterval<Date> {
|
||||
const [verb, args] = s.split(":")
|
||||
const [verb, args] = s.split(";")[0].split(":")
|
||||
|
||||
switch (verb) {
|
||||
case "":
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import parseInterval, { datesOf, intervalLabel } from "$lib/utils/timeinterval";
|
||||
import { groupItems } from "$lib/utils/items";
|
||||
import { groupItemOptionsQuery, groupItems, type GroupItemsOptions } from "$lib/utils/items";
|
||||
import type Item from "$lib/models/item";
|
||||
import type { ItemFilter, ItemGroup } from "$lib/models/item";
|
||||
|
||||
export const load: Load = async({ fetch, params }) => {
|
||||
export const load: Load = async({ fetch, params, session }) => {
|
||||
const scopeId = parseInt(params.scope.split("-")[0]);
|
||||
|
||||
const interval = parseInterval(params.interval, new Date());
|
||||
@@ -22,22 +22,26 @@
|
||||
props: {
|
||||
items, filter,
|
||||
intervalString: params.interval,
|
||||
groups: groupItems(items, datesOf(interval)?.min),
|
||||
groups: groupItems(items, datesOf(interval)?.min, session.groupOptions || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { session } from "$app/stores";
|
||||
import { browser } from "$app/env";
|
||||
import Main from "$lib/components/layout/Main.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import DeletionModal from "$lib/modals/DeletionModal.svelte";
|
||||
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte";
|
||||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import HistorySelector from "$lib/components/common/HistorySelector.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import ItemListContext from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import HistoryGroupList from "$lib/components/history/HistoryGroupList.svelte";
|
||||
import ToggleIcon from "$lib/components/history/ToggleIcon.svelte";
|
||||
import Status from "$lib/models/status";
|
||||
|
||||
export let intervalString: string
|
||||
export let items: Item[]
|
||||
@@ -51,13 +55,38 @@
|
||||
|
||||
lastHistoryRange.set(target.value);
|
||||
|
||||
goto(`./${target.value}`);
|
||||
goto(`./${target.value}`, {replaceState: true});
|
||||
}
|
||||
|
||||
function triggerReload() {
|
||||
goto(`./${intervalString}`, {replaceState: true});
|
||||
}
|
||||
|
||||
$: {
|
||||
if (browser) {
|
||||
if ($session.groupOptions == null) {
|
||||
try {
|
||||
$session.groupOptions = JSON.parse(localStorage.getItem("sl3.history.group_options")||"{}");
|
||||
triggerReload();
|
||||
} catch(_) {
|
||||
$session.groupOptions = {};
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem("sl3.history.group_options", JSON.stringify($session.groupOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ItemListContext filter={filter} groups={groups} items={items}>
|
||||
<Main big>
|
||||
<HistorySelector value={intervalString} on:change={changeInterval} />
|
||||
<HistorySelector value={intervalString} on:change={changeInterval} >
|
||||
{#if $session.groupOptions != null}
|
||||
<ToggleIcon inverse name="lightbulb" bind:value={$session.groupOptions.hideCreated} on:click={triggerReload} />
|
||||
<ToggleIcon inverse name="check_slot" bind:value={$session.groupOptions.hideAcquired} on:click={triggerReload} />
|
||||
<ToggleIcon inverse name="calendar" bind:value={$session.groupOptions.hideScheduled} on:click={triggerReload} />
|
||||
{/if}
|
||||
</HistorySelector>
|
||||
<HistoryGroupList />
|
||||
</Main>
|
||||
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { Load } from "@sveltejs/kit/types/internal";
|
||||
|
||||
function generateItemFilters(now: Date): {scheduledFilter: ItemFilter, acquiredFilter: ItemFilter, looseFilter: ItemFilter} {
|
||||
return {
|
||||
scheduledFilter: {
|
||||
scheduledDate: datesOf(parseInterval("next:7d", new Date)),
|
||||
},
|
||||
acquiredFilter: {
|
||||
acquiredTime: parseInterval("today", new Date),
|
||||
},
|
||||
looseFilter: {
|
||||
loose: true,
|
||||
unAcquired: true,
|
||||
unScheduled: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(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 scheduledItems = (await sl3(fetch).listItems(scopeId, scheduledFilter));
|
||||
const acquiredItems = (await sl3(fetch).listItems(scopeId, acquiredFilter));
|
||||
const looseItems = (await sl3(fetch).listItems(scopeId, looseFilter));
|
||||
|
||||
const sprints = await sl3(fetch).listSprints(scopeId);
|
||||
|
||||
return {
|
||||
props: {scheduledItems, acquiredItems, sprints}
|
||||
props: {scheduledItems, acquiredItems, looseItems, sprints}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -48,13 +59,19 @@
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import ProjectCreateEditModal from "$lib/modals/ProjectCreateEditModal.svelte";
|
||||
import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte";
|
||||
import type { ItemFilter } from "$lib/models/item";
|
||||
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
|
||||
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
|
||||
|
||||
export let acquiredItems: Item[];
|
||||
export let scheduledItems: Item[];
|
||||
export let looseItems: Item[];
|
||||
export let sprints: Sprint[];
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {openModal} = getModalContext();
|
||||
const {now} = getTimeContext();
|
||||
|
||||
function openCreateProject() {
|
||||
openModal({name: "project.create"});
|
||||
@@ -73,53 +90,44 @@
|
||||
}
|
||||
</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} />
|
||||
<ItemMultiListContext lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
|
||||
<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>Create Item</CardHeader></Card>
|
||||
<Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card>
|
||||
<Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card>
|
||||
</Row>
|
||||
<ItemListRow title="Scheduled" key="scheduleItems" />
|
||||
<ItemListRow title="Today" key="acquiredItems" showAcquiredTime />
|
||||
<ItemListRow title="Loose" key="looseItems" />
|
||||
</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>
|
||||
{/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>
|
||||
</Column>
|
||||
</Columns>
|
||||
|
||||
<ItemCreateModal />
|
||||
<ItemAcquireModal />
|
||||
<StatCreateEditModal />
|
||||
<DeletionModal />
|
||||
<SprintCreateUpdateModal />
|
||||
<ProjectCreateEditModal />
|
||||
<ScopeCreateUpdateModal />
|
||||
</SprintListContext>
|
||||
</ItemMultiListContext>
|
||||
@@ -19,11 +19,7 @@
|
||||
import { session } from '$app/stores';
|
||||
|
||||
Amplify.configure({
|
||||
Auth: {
|
||||
region: import.meta.env.VITE_STUFFLOG3_AWS_POOL_REGION,
|
||||
userPoolId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_ID,
|
||||
userPoolWebClientId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_CLIENT_ID,
|
||||
},
|
||||
Auth: $session.amplifyConfig,
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -25,4 +25,6 @@ type ItemFilter struct {
|
||||
RequirementIDs []int `json:"requirementIds,omitempty"`
|
||||
StatIDs []int `json:"statIds,omitempty"`
|
||||
Loose bool `json:"loose,omitempty"`
|
||||
UnScheduled bool `json:"unScheduled,omitempty"`
|
||||
UnAcquired bool `json:"unAcquired,omitempty"`
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ type RequirementUpdate struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Status *Status `json:"status"`
|
||||
IsCoarse *bool `json:"isCoarse"`
|
||||
}
|
||||
|
||||
@@ -14,6 +14,35 @@ func (s Status) Name() string {
|
||||
return StatusLabels[s]
|
||||
}
|
||||
|
||||
func (s Status) Less(s2 Status) bool {
|
||||
if s == Active && s2 != Active {
|
||||
return true
|
||||
}
|
||||
if s != Active && s2 == Active {
|
||||
return false
|
||||
}
|
||||
if s == Background && s2 != Background {
|
||||
return true
|
||||
}
|
||||
if s != Background && s2 == Background {
|
||||
return false
|
||||
}
|
||||
if s == Available && s2 != Available {
|
||||
return true
|
||||
}
|
||||
if s != Available && s2 == Available {
|
||||
return false
|
||||
}
|
||||
if s == Blocked && s2 != Blocked {
|
||||
return true
|
||||
}
|
||||
if s != Blocked && s2 == Blocked {
|
||||
return false
|
||||
}
|
||||
|
||||
return s < s2
|
||||
}
|
||||
|
||||
const (
|
||||
Blocked Status = iota
|
||||
Available
|
||||
|
||||
@@ -67,13 +67,17 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
|
||||
squirrel.Lt{"i.created_time": filter.CreatedTime.Max},
|
||||
})
|
||||
}
|
||||
if filter.AcquiredTime != nil {
|
||||
if filter.UnAcquired {
|
||||
sq = sq.Where("i.acquired_time IS NULL")
|
||||
} else if filter.AcquiredTime != nil {
|
||||
dateOr = append(dateOr, squirrel.And{
|
||||
squirrel.GtOrEq{"i.acquired_time": filter.AcquiredTime.Min},
|
||||
squirrel.Lt{"i.acquired_time": filter.AcquiredTime.Max},
|
||||
})
|
||||
}
|
||||
if filter.ScheduledDate != nil {
|
||||
if filter.UnScheduled {
|
||||
sq = sq.Where("i.scheduled_date IS NULL")
|
||||
} else if filter.ScheduledDate != nil {
|
||||
dateOr = append(dateOr, squirrel.And{
|
||||
squirrel.GtOrEq{"i.scheduled_date": filter.ScheduledDate.Min.AsTime()},
|
||||
squirrel.Lt{"i.scheduled_date": filter.ScheduledDate.Max.AsTime()},
|
||||
|
||||
@@ -47,6 +47,7 @@ type ProjectRequirement struct {
|
||||
Name string
|
||||
Status int
|
||||
Description string
|
||||
IsCoarse bool
|
||||
}
|
||||
|
||||
type ProjectRequirementStat struct {
|
||||
|
||||
@@ -156,8 +156,8 @@ func (q *Queries) InsertProject(ctx context.Context, arg InsertProjectParams) (s
|
||||
}
|
||||
|
||||
const insertProjectRequirement = `-- name: InsertProjectRequirement :execresult
|
||||
INSERT INTO project_requirement (scope_id, project_id, name, status, description)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type InsertProjectRequirementParams struct {
|
||||
@@ -166,6 +166,7 @@ type InsertProjectRequirementParams struct {
|
||||
Name string
|
||||
Status int
|
||||
Description string
|
||||
IsCoarse bool
|
||||
}
|
||||
|
||||
func (q *Queries) InsertProjectRequirement(ctx context.Context, arg InsertProjectRequirementParams) (sql.Result, error) {
|
||||
@@ -175,11 +176,12 @@ func (q *Queries) InsertProjectRequirement(ctx context.Context, arg InsertProjec
|
||||
arg.Name,
|
||||
arg.Status,
|
||||
arg.Description,
|
||||
arg.IsCoarse,
|
||||
)
|
||||
}
|
||||
|
||||
const listProjectRequirements = `-- name: ListProjectRequirements :many
|
||||
SELECT id, scope_id, project_id, name, status, description FROM project_requirement WHERE project_id = ?
|
||||
SELECT id, scope_id, project_id, name, status, description, is_coarse FROM project_requirement WHERE project_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) ListProjectRequirements(ctx context.Context, projectID int) ([]ProjectRequirement, error) {
|
||||
@@ -198,6 +200,7 @@ func (q *Queries) ListProjectRequirements(ctx context.Context, projectID int) ([
|
||||
&i.Name,
|
||||
&i.Status,
|
||||
&i.Description,
|
||||
&i.IsCoarse,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -326,7 +329,8 @@ const updateProjectRequirement = `-- name: UpdateProjectRequirement :exec
|
||||
UPDATE project_requirement
|
||||
SET name = ?,
|
||||
status = ?,
|
||||
description = ?
|
||||
description = ?,
|
||||
is_coarse = ?
|
||||
WHERE id = ? AND scope_id = ?
|
||||
`
|
||||
|
||||
@@ -334,6 +338,7 @@ type UpdateProjectRequirementParams struct {
|
||||
Name string
|
||||
Status int
|
||||
Description string
|
||||
IsCoarse bool
|
||||
ID int
|
||||
ScopeID int
|
||||
}
|
||||
@@ -343,6 +348,7 @@ func (q *Queries) UpdateProjectRequirement(ctx context.Context, arg UpdateProjec
|
||||
arg.Name,
|
||||
arg.Status,
|
||||
arg.Description,
|
||||
arg.IsCoarse,
|
||||
arg.ID,
|
||||
arg.ScopeID,
|
||||
)
|
||||
|
||||
@@ -139,7 +139,7 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
|
||||
return []entities.Requirement{}, []entities.RequirementStat{}, nil
|
||||
}
|
||||
|
||||
query, args, err := squirrel.Select("id, scope_id, project_id, name, status, description").
|
||||
query, args, err := squirrel.Select("id, scope_id, project_id, name, status, description, is_coarse").
|
||||
From("project_requirement").
|
||||
Where(squirrel.Eq{"scope_id": scopeID}).
|
||||
Where(squirrel.Eq{"id": requirementIDs}).
|
||||
@@ -162,6 +162,7 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
|
||||
&requirement.Name,
|
||||
&requirement.Status,
|
||||
&requirement.Description,
|
||||
&requirement.IsCoarse,
|
||||
); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -217,6 +218,7 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int)
|
||||
ProjectID: row.ProjectID,
|
||||
Name: row.Name,
|
||||
Description: row.Description,
|
||||
IsCoarse: row.IsCoarse,
|
||||
Status: models.Status(row.Status),
|
||||
})
|
||||
}
|
||||
@@ -240,6 +242,7 @@ func (r *projectRepository) CreateRequirement(ctx context.Context, requirement e
|
||||
Name: requirement.Name,
|
||||
Status: int(requirement.Status),
|
||||
Description: requirement.Description,
|
||||
IsCoarse: requirement.IsCoarse,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -261,6 +264,7 @@ func (r *projectRepository) UpdateRequirement(ctx context.Context, requirement e
|
||||
Name: requirement.Name,
|
||||
Status: int(requirement.Status),
|
||||
Description: requirement.Description,
|
||||
IsCoarse: requirement.IsCoarse,
|
||||
ID: requirement.ID,
|
||||
ScopeID: requirement.ScopeID,
|
||||
})
|
||||
|
||||
@@ -23,14 +23,15 @@ DELETE FROM project WHERE id = ? AND scope_id = ?;
|
||||
SELECT * FROM project_requirement WHERE project_id = ?;
|
||||
|
||||
-- name: InsertProjectRequirement :execresult
|
||||
INSERT INTO project_requirement (scope_id, project_id, name, status, description)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse)
|
||||
VALUES (?, ?, ?, ?, ?, ?);
|
||||
|
||||
-- name: UpdateProjectRequirement :exec
|
||||
UPDATE project_requirement
|
||||
SET name = ?,
|
||||
status = ?,
|
||||
description = ?
|
||||
description = ?,
|
||||
is_coarse = ?
|
||||
WHERE id = ? AND scope_id = ?;
|
||||
|
||||
-- name: DeleteProjectRequirement :exec
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE project_requirement ADD COLUMN is_coarse BOOLEAN NOT NULL DEFAULT 0;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE project_requirement DROP COLUMN IF EXISTS is_coarse;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE item CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE project CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE project_requirement CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE scope CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE sprint CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE stat CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
SELECT 'down SQL query';
|
||||
-- +goose StatementEnd
|
||||
@@ -3,14 +3,15 @@ package items
|
||||
import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
entities.Item
|
||||
|
||||
Requirement *entities.Requirement `json:"requirement,omitempty"`
|
||||
Stats []ResultStat `json:"stats"`
|
||||
Requirement *entities.Requirement `json:"requirement,omitempty"`
|
||||
Stats []ResultStat `json:"stats"`
|
||||
WeightedAcquired float64 `json:"weightedAcquired"`
|
||||
WeightedRequired float64 `json:"weightedRequired"`
|
||||
}
|
||||
|
||||
func (r *Result) Stat(statID int) *ResultStat {
|
||||
@@ -53,13 +54,23 @@ func (r *Result) AddStat(scope scopes.Result, stat entities.ItemStat) {
|
||||
Acquired: stat.Acquired,
|
||||
Required: stat.Required,
|
||||
})
|
||||
r.SortStats()
|
||||
r.SortStats(scope)
|
||||
r.RecalculateWeighted()
|
||||
}
|
||||
|
||||
func (r *Result) SortStats() {
|
||||
sort.Slice(r.Stats, func(i, j int) bool {
|
||||
return r.Stats[i].Name < r.Stats[j].Name
|
||||
})
|
||||
func (r *Result) SortStats(scope scopes.Result) {
|
||||
sortedStats := make([]ResultStat, 0, len(r.Stats))
|
||||
|
||||
for _, scopeStat := range scope.Stats {
|
||||
for _, itemStat := range r.Stats {
|
||||
if itemStat.ID == scopeStat.ID {
|
||||
sortedStats = append(sortedStats, itemStat)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.Stats = sortedStats
|
||||
}
|
||||
|
||||
func (r *Result) RemoveStat(id int) {
|
||||
@@ -68,6 +79,18 @@ func (r *Result) RemoveStat(id int) {
|
||||
r.Stats = append(r.Stats[:i], r.Stats[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
r.RecalculateWeighted()
|
||||
}
|
||||
|
||||
func (r *Result) RecalculateWeighted() {
|
||||
r.WeightedAcquired = 0
|
||||
r.WeightedRequired = 0
|
||||
|
||||
for _, stat := range r.Stats {
|
||||
r.WeightedAcquired += float64(stat.Acquired) * stat.Weight
|
||||
r.WeightedRequired += float64(stat.Required) * stat.Weight
|
||||
}
|
||||
}
|
||||
|
||||
type ResultStat struct {
|
||||
@@ -112,7 +135,8 @@ func generateResult(item entities.Item, scope scopes.Result, progresses []entiti
|
||||
}
|
||||
}
|
||||
|
||||
res.SortStats()
|
||||
res.SortStats(scope)
|
||||
res.RecalculateWeighted()
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
+39
-11
@@ -6,6 +6,7 @@ import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -35,9 +36,9 @@ type Result struct {
|
||||
entities.Project
|
||||
OwnerName string `json:"ownerName"`
|
||||
StatusName string `json:"statusName"`
|
||||
TotalAcquired int `json:"totalAcquired"`
|
||||
TotalRequired int `json:"totalRequired"`
|
||||
TotalPlanned int `json:"totalPlanned"`
|
||||
TotalAcquired float64 `json:"totalAcquired"`
|
||||
TotalRequired float64 `json:"totalRequired"`
|
||||
TotalPlanned float64 `json:"totalPlanned"`
|
||||
Requirements []RequirementResult `json:"requirements"`
|
||||
}
|
||||
|
||||
@@ -57,9 +58,10 @@ type RequirementResult struct {
|
||||
Description string `json:"description"`
|
||||
Status models.Status `json:"status"`
|
||||
StatusName string `json:"statusName"`
|
||||
TotalAcquired int `json:"totalAcquired"`
|
||||
TotalRequired int `json:"totalRequired"`
|
||||
TotalPlanned int `json:"totalPlanned"`
|
||||
TotalAcquired float64 `json:"totalAcquired"`
|
||||
TotalRequired float64 `json:"totalRequired"`
|
||||
TotalPlanned float64 `json:"totalPlanned"`
|
||||
IsCoarse bool `json:"isCoarse"`
|
||||
Stats []RequirementResultStat `json:"stats"`
|
||||
Items []items.Result `json:"items"`
|
||||
|
||||
@@ -140,6 +142,16 @@ func generateResult(
|
||||
res.Requirements = append(res.Requirements, resReq)
|
||||
}
|
||||
|
||||
sort.Slice(res.Requirements, func(i, j int) bool {
|
||||
ri := res.Requirements[i]
|
||||
rj := res.Requirements[j]
|
||||
if ri.Status != rj.Status {
|
||||
return ri.Status.Less(rj.Status)
|
||||
}
|
||||
|
||||
return ri.ID < rj.ID
|
||||
})
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
@@ -150,6 +162,7 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
|
||||
Description: req.Description,
|
||||
Status: req.Status,
|
||||
StatusName: scope.StatusName(req.Status),
|
||||
IsCoarse: req.IsCoarse,
|
||||
Stats: make([]RequirementResultStat, 0, 8),
|
||||
Items: make([]items.Result, 0, 8),
|
||||
|
||||
@@ -194,6 +207,21 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
|
||||
resReq.Items = append(resReq.Items, item)
|
||||
}
|
||||
|
||||
// Sort items so that they're in order of created, with acquired items being in order of acquired below.
|
||||
sort.Slice(resReq.Items, func(i, j int) bool {
|
||||
ii := resReq.Items[i]
|
||||
ij := resReq.Items[j]
|
||||
if ii.AcquiredTime != nil && ij.AcquiredTime != nil {
|
||||
return ii.AcquiredTime.Before(*ij.AcquiredTime)
|
||||
} else if ii.AcquiredTime != nil && ij.AcquiredTime == nil {
|
||||
return false
|
||||
} else if ii.AcquiredTime == nil && ij.AcquiredTime != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return ii.CreatedTime.Before(ij.CreatedTime)
|
||||
})
|
||||
|
||||
for _, stat := range scope.Stats {
|
||||
if rs := resStats[stat.ID]; rs != nil && rs.Required > 0 {
|
||||
resReq.Stats = append(resReq.Stats, *rs)
|
||||
@@ -210,14 +238,14 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
|
||||
totalPlanned := 0.0
|
||||
for _, stat := range resReq.Stats {
|
||||
if stat.Required > 0 {
|
||||
totalAcquired += float64(stat.Acquired) * stat.Weight
|
||||
totalAcquired += math.Min(float64(stat.Acquired), float64(stat.Required)) * stat.Weight
|
||||
totalRequired += float64(stat.Required) * stat.Weight
|
||||
totalPlanned += float64(stat.Planned) * stat.Weight
|
||||
totalPlanned += math.Min(float64(stat.Planned), float64(stat.Required)) * stat.Weight
|
||||
}
|
||||
}
|
||||
resReq.TotalRequired += int(totalRequired)
|
||||
resReq.TotalAcquired += int(math.Min(totalAcquired, totalRequired))
|
||||
resReq.TotalPlanned += int(math.Min(totalPlanned, totalRequired))
|
||||
resReq.TotalRequired += totalRequired
|
||||
resReq.TotalAcquired += math.Min(totalAcquired, totalRequired)
|
||||
resReq.TotalPlanned += math.Min(totalPlanned, totalRequired)
|
||||
|
||||
return resReq
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user