Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ab0a8555 | ||
|
|
22885734c7 | ||
|
|
795d49a942 |
@@ -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> {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -144,6 +146,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
|
||||
await reloadItemList();
|
||||
await reloadSprintList();
|
||||
reloadItemLists();
|
||||
|
||||
closeModal();
|
||||
} catch(err) {
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface ItemFilter {
|
||||
createdTime?: TimeInterval<Date | string>
|
||||
acquiredTime?: TimeInterval<Date | string>
|
||||
loose?: boolean
|
||||
unAcquired?: boolean
|
||||
unScheduled?: boolean
|
||||
}
|
||||
|
||||
export interface ItemGroup {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,19 +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]);
|
||||
|
||||
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 {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(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>
|
||||
@@ -43,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"});
|
||||
@@ -68,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 showAcquiredTime 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>
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()},
|
||||
|
||||
@@ -36,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"`
|
||||
}
|
||||
|
||||
@@ -58,9 +58,9 @@ 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"`
|
||||
@@ -243,9 +243,9 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
|
||||
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