Compare commits

..
6 Commits
Author SHA1 Message Date
gisle 75dd79127e blurgh.
continuous-integration/drone/push Build is passing
2022-07-02 16:38:57 +02:00
gisle da6cdfdb0d add item times.
continuous-integration/drone/push Build is passing
2022-07-02 16:24:01 +02:00
gisle 7eb5f07299 fix item stat sorting to respect scope stat order.
continuous-integration/drone/push Build is passing
2022-07-02 14:04:46 +02:00
gisle d114f0d38c fix coarse affecting stat options in created item, change sorting of requirements. 2022-07-02 13:43:57 +02:00
gisle a3336b183c add coarse option for project requirement. 2022-07-02 13:30:12 +02:00
gisle d8a269328f fix sprints maybe?
continuous-integration/drone/push Build is passing
2022-07-02 10:36:08 +02:00
19 changed files with 112 additions and 52 deletions
+4
View File
@@ -36,6 +36,7 @@ type Requirement struct {
ProjectID int `json:"projectId"` ProjectID int `json:"projectId"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
IsCoarse bool `json:"isCoarse"`
Status models.Status `json:"status"` Status models.Status `json:"status"`
} }
@@ -49,6 +50,9 @@ func (requirement *Requirement) Update(update models.RequirementUpdate) {
if update.Status != nil && update.Status.Valid() { if update.Status != nil && update.Status.Valid() {
requirement.Status = *update.Status requirement.Status = *update.Status
} }
if update.IsCoarse != nil {
requirement.IsCoarse = *update.IsCoarse
}
} }
type RequirementStat struct { type RequirementStat struct {
@@ -40,6 +40,8 @@
import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb"; import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight"; import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
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 { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot";
const icons = { const icons = {
"clock": faClock, "clock": faClock,
@@ -62,6 +64,8 @@
"search": faSearch, "search": faSearch,
"chevron_right": faChevronRight, "chevron_right": faChevronRight,
"chevron_down": faChevronDown, "chevron_down": faChevronDown,
"trash": faTrash,
"check_slot": faCheckToSlot,
}; };
export type IconName = keyof typeof icons; export type IconName = keyof typeof icons;
@@ -51,21 +51,6 @@
div.header div.header
display: flex 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 div.right
color: $color-entry2-transparent color: $color-entry2-transparent
margin-left: auto margin-left: auto
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
export let centered = false; export let centered = false;
export let vertical = false;
</script> </script>
<div class="option-row" class:centered> <div class="option-row" class:vertical class:centered>
<slot></slot> <slot></slot>
</div> </div>
@@ -10,10 +11,18 @@
div.option-row { div.option-row {
margin-left: -0.4ch; margin-left: -0.4ch;
margin-right: -0.4ch; margin-right: -0.4ch;
display: flex;
flex-direction: row;
} }
div.option-row.centered { div.option-row.centered {
text-align: center; text-align: center;
padding-bottom: 0.5em; padding-bottom: 0.5em;
} }
div.vertical {
display: flex;
flex-direction: column;
font-size: 1em;
}
</style> </style>
@@ -54,7 +54,6 @@
opacity: 0.6 opacity: 0.6
margin-right: 1.5ch margin-right: 1.5ch
font-size: 0.75em font-size: 0.75em
margin-top: auto
span.event span.event
font-size: 1.33em font-size: 1.33em
@@ -64,7 +63,8 @@
display: none display: none
&.blankEvent &.blankEvent
margin-right: 0.5ch margin-top: 0.35em
margin-right: 0.75ch
span.event span.event
display: none display: none
@@ -78,6 +78,6 @@
margin-top: auto margin-top: auto
&.noProgress &.noProgress
margin-top: -0.25em margin-top: 0em
margin-bottom: auto margin-bottom: auto
</style> </style>
@@ -8,40 +8,47 @@
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 type { IconName } from "../layout/Icon.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 item: Item;
export let event: "created" | "scheduled" | "acquired" | "none" = "none"; export let event: "created" | "scheduled" | "acquired" | "none" = "none";
export let compact = false; export let compact = false;
export let showAcquiredTime = false;
let icon: IconName let icon: IconName
let status: Status let status: Status
let eventText: string
$: { $: {
if (event === "none" || event === "acquired") { if (event === "none" || event === "acquired") {
if (item.acquiredTime) { if (item.acquiredTime) {
icon = "check"; icon = "check";
status = Status.Completed; status = Status.Completed;
eventText = (showAcquiredTime || event === "acquired") ? formatTime(item.acquiredTime) : void(0)
} }
} else if (event === "scheduled") { } else if (event === "scheduled") {
if (item.acquiredTime) { if (item.acquiredTime) {
icon = "hourglass"; icon = "hourglass";
status = Status.Blocked; status = Status.Blocked;
eventText = "scheduled"
} }
} else if (event === "created") { } else if (event === "created") {
icon = "lightbulb"; icon = "lightbulb";
status = Status.Available; status = Status.Available;
eventText = formatTime(item.acquiredTime)
} }
} }
</script> </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"> <OptionsRow slot="right">
{#if item.acquiredTime == null} {#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}
{#if !compact} {#if !compact}
<Option open={{name: "item.edit", item}}>Edit</Option> <Option open={{name: "item.edit", item}}><Icon name="pen" /></Option>
<Option open={{name: "item.delete", item}} color="red">Delete</Option> <Option open={{name: "item.delete", item}} color="red"><Icon name="trash" /></Option>
{/if} {/if}
</OptionsRow> </OptionsRow>
{#if !compact} {#if !compact}
@@ -24,7 +24,9 @@
</OptionsRow> </OptionsRow>
<LabeledProgressRow> <LabeledProgressRow>
{#each requirement.stats as stat (stat.id)} {#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} {/each}
</LabeledProgressRow> </LabeledProgressRow>
{#each requirement.items as item (item.id)} {#each requirement.items as item (item.id)}
@@ -33,7 +33,7 @@
</LabeledProgressRow> </LabeledProgressRow>
{/if} {/if}
{#if sprint.kind === SprintKind.Items} {#if sprint.kind === SprintKind.Items}
{#each sprint.items as item (item.id)} {#each (sprint.items||[]) as item (item.id)}
{#if !item.acquiredTime} {#if !item.acquiredTime}
<ItemSubSection compact item={item} /> <ItemSubSection compact item={item} />
{/if} {/if}
@@ -8,6 +8,7 @@
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import StatInput from "$lib/components/controls/StatInput.svelte"; import StatInput from "$lib/components/controls/StatInput.svelte";
import StatusSelect from "$lib/components/controls/StatusSelect.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 type { ProjectEntry, Requirement, RequirementInput } from "$lib/models/project";
import Status from "$lib/models/status"; import Status from "$lib/models/status";
import { statDiff } from "$lib/utils/stat"; import { statDiff } from "$lib/utils/stat";
@@ -49,23 +50,25 @@ import { statDiff } from "$lib/utils/stat";
description: "", description: "",
stats: [], stats: [],
status: Status.Available, status: Status.Available,
isCoarse: false,
} }
projectId = project.id; projectId = project.id;
show = true; show = true;
} }
function initEdit(prev: Requirement, project: ProjectEntry) { function initEdit(current: Requirement, project: ProjectEntry) {
requirement = { requirement = {
name: prev.name, name: current.name,
description: prev.description, description: current.description,
status: prev.status, status: current.status,
stats: prev.stats.map(s => ({acquired: s.acquired, required: s.required, statId: s.id})), stats: current.stats.map(s => ({acquired: s.acquired, required: s.required, statId: s.id})),
isCoarse: current.isCoarse,
} }
oldStats = [...requirement.stats]; oldStats = [...requirement.stats];
projectId = project.id; projectId = project.id;
requirementId = prev.id; requirementId = current.id;
show = true; show = true;
} }
@@ -118,6 +121,7 @@ import { statDiff } from "$lib/utils/stat";
<StatusSelect bind:status={requirement.status} /> <StatusSelect bind:status={requirement.status} />
<label for="stats">Stats</label> <label for="stats">Stats</label>
<StatInput showRequired bind:value={requirement.stats} /> <StatInput showRequired bind:value={requirement.stats} />
<Checkbox bind:checked={requirement.isCoarse} label="Hide 0-requirement stats." />
</ModalBody> </ModalBody>
</Modal> </Modal>
</form> </form>
+2
View File
@@ -37,6 +37,7 @@ export interface Requirement {
totalAcquired: number totalAcquired: number
totalRequired: number totalRequired: number
totalPlanned: number totalPlanned: number
isCoarse: boolean
stats: StatProgressWithPlanned[] stats: StatProgressWithPlanned[]
items: Item[] items: Item[]
} }
@@ -45,5 +46,6 @@ export interface RequirementInput {
name: string name: string
description: string description: string
status: Status status: Status
isCoarse: boolean
stats: StatValueInput[] stats: StatValueInput[]
} }
@@ -4,11 +4,6 @@
export const load: Load = async({params, url, fetch}) => { export const load: Load = async({params, url, fetch}) => {
const scopeId = parseInt(params.scope.split("-")[0]); const scopeId = parseInt(params.scope.split("-")[0]);
console.log(
datesOf(parseInterval("next:7d", new Date)),
parseInterval("today", new Date),
)
const scheduledItems = (await sl3(fetch).listItems(scopeId, { const scheduledItems = (await sl3(fetch).listItems(scopeId, {
scheduledDate: datesOf(parseInterval("next:7d", new Date)), scheduledDate: datesOf(parseInterval("next:7d", new Date)),
})) }))
@@ -92,7 +87,7 @@
{#if acquiredItems.length > 0} {#if acquiredItems.length > 0}
<Row title="Today"> <Row title="Today">
{#each acquiredItems as item (item.id)} {#each acquiredItems as item (item.id)}
<ItemSubSection item={item} /> <ItemSubSection showAcquiredTime item={item} />
{/each} {/each}
</Row> </Row>
{/if} {/if}
+1
View File
@@ -11,4 +11,5 @@ type RequirementUpdate struct {
Name *string `json:"name"` Name *string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
Status *Status `json:"status"` Status *Status `json:"status"`
IsCoarse *bool `json:"isCoarse"`
} }
+1
View File
@@ -47,6 +47,7 @@ type ProjectRequirement struct {
Name string Name string
Status int Status int
Description string Description string
IsCoarse bool
} }
type ProjectRequirementStat struct { type ProjectRequirementStat struct {
+10 -4
View File
@@ -156,8 +156,8 @@ func (q *Queries) InsertProject(ctx context.Context, arg InsertProjectParams) (s
} }
const insertProjectRequirement = `-- name: InsertProjectRequirement :execresult const insertProjectRequirement = `-- name: InsertProjectRequirement :execresult
INSERT INTO project_requirement (scope_id, project_id, name, status, description) INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
` `
type InsertProjectRequirementParams struct { type InsertProjectRequirementParams struct {
@@ -166,6 +166,7 @@ type InsertProjectRequirementParams struct {
Name string Name string
Status int Status int
Description string Description string
IsCoarse bool
} }
func (q *Queries) InsertProjectRequirement(ctx context.Context, arg InsertProjectRequirementParams) (sql.Result, error) { 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.Name,
arg.Status, arg.Status,
arg.Description, arg.Description,
arg.IsCoarse,
) )
} }
const listProjectRequirements = `-- name: ListProjectRequirements :many 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) { 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.Name,
&i.Status, &i.Status,
&i.Description, &i.Description,
&i.IsCoarse,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -326,7 +329,8 @@ const updateProjectRequirement = `-- name: UpdateProjectRequirement :exec
UPDATE project_requirement UPDATE project_requirement
SET name = ?, SET name = ?,
status = ?, status = ?,
description = ? description = ?,
is_coarse = ?
WHERE id = ? AND scope_id = ? WHERE id = ? AND scope_id = ?
` `
@@ -334,6 +338,7 @@ type UpdateProjectRequirementParams struct {
Name string Name string
Status int Status int
Description string Description string
IsCoarse bool
ID int ID int
ScopeID int ScopeID int
} }
@@ -343,6 +348,7 @@ func (q *Queries) UpdateProjectRequirement(ctx context.Context, arg UpdateProjec
arg.Name, arg.Name,
arg.Status, arg.Status,
arg.Description, arg.Description,
arg.IsCoarse,
arg.ID, arg.ID,
arg.ScopeID, arg.ScopeID,
) )
+5 -1
View File
@@ -139,7 +139,7 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
return []entities.Requirement{}, []entities.RequirementStat{}, nil 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"). From("project_requirement").
Where(squirrel.Eq{"scope_id": scopeID}). Where(squirrel.Eq{"scope_id": scopeID}).
Where(squirrel.Eq{"id": requirementIDs}). Where(squirrel.Eq{"id": requirementIDs}).
@@ -162,6 +162,7 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
&requirement.Name, &requirement.Name,
&requirement.Status, &requirement.Status,
&requirement.Description, &requirement.Description,
&requirement.IsCoarse,
); err != nil { ); err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -217,6 +218,7 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int)
ProjectID: row.ProjectID, ProjectID: row.ProjectID,
Name: row.Name, Name: row.Name,
Description: row.Description, Description: row.Description,
IsCoarse: row.IsCoarse,
Status: models.Status(row.Status), Status: models.Status(row.Status),
}) })
} }
@@ -240,6 +242,7 @@ func (r *projectRepository) CreateRequirement(ctx context.Context, requirement e
Name: requirement.Name, Name: requirement.Name,
Status: int(requirement.Status), Status: int(requirement.Status),
Description: requirement.Description, Description: requirement.Description,
IsCoarse: requirement.IsCoarse,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -261,6 +264,7 @@ func (r *projectRepository) UpdateRequirement(ctx context.Context, requirement e
Name: requirement.Name, Name: requirement.Name,
Status: int(requirement.Status), Status: int(requirement.Status),
Description: requirement.Description, Description: requirement.Description,
IsCoarse: requirement.IsCoarse,
ID: requirement.ID, ID: requirement.ID,
ScopeID: requirement.ScopeID, ScopeID: requirement.ScopeID,
}) })
+4 -3
View File
@@ -23,14 +23,15 @@ DELETE FROM project WHERE id = ? AND scope_id = ?;
SELECT * FROM project_requirement WHERE project_id = ?; SELECT * FROM project_requirement WHERE project_id = ?;
-- name: InsertProjectRequirement :execresult -- name: InsertProjectRequirement :execresult
INSERT INTO project_requirement (scope_id, project_id, name, status, description) INSERT INTO project_requirement (scope_id, project_id, name, status, description, is_coarse)
VALUES (?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?);
-- name: UpdateProjectRequirement :exec -- name: UpdateProjectRequirement :exec
UPDATE project_requirement UPDATE project_requirement
SET name = ?, SET name = ?,
status = ?, status = ?,
description = ? description = ?,
is_coarse = ?
WHERE id = ? AND scope_id = ?; WHERE id = ? AND scope_id = ?;
-- name: DeleteProjectRequirement :exec -- 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
+15 -7
View File
@@ -3,7 +3,6 @@ package items
import ( import (
"git.aiterp.net/stufflog3/stufflog3/entities" "git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes" "git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"sort"
) )
type Result struct { type Result struct {
@@ -53,13 +52,22 @@ func (r *Result) AddStat(scope scopes.Result, stat entities.ItemStat) {
Acquired: stat.Acquired, Acquired: stat.Acquired,
Required: stat.Required, Required: stat.Required,
}) })
r.SortStats() r.SortStats(scope)
} }
func (r *Result) SortStats() { func (r *Result) SortStats(scope scopes.Result) {
sort.Slice(r.Stats, func(i, j int) bool { sortedStats := make([]ResultStat, 0, len(r.Stats))
return r.Stats[i].Name < r.Stats[j].Name
}) 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) { func (r *Result) RemoveStat(id int) {
@@ -112,7 +120,7 @@ func generateResult(item entities.Item, scope scopes.Result, progresses []entiti
} }
} }
res.SortStats() res.SortStats(scope)
return res return res
} }
+18
View File
@@ -6,6 +6,7 @@ import (
"git.aiterp.net/stufflog3/stufflog3/usecases/items" "git.aiterp.net/stufflog3/stufflog3/usecases/items"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes" "git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"math" "math"
"sort"
"time" "time"
) )
@@ -60,6 +61,7 @@ type RequirementResult struct {
TotalAcquired int `json:"totalAcquired"` TotalAcquired int `json:"totalAcquired"`
TotalRequired int `json:"totalRequired"` TotalRequired int `json:"totalRequired"`
TotalPlanned int `json:"totalPlanned"` TotalPlanned int `json:"totalPlanned"`
IsCoarse bool `json:"isCoarse"`
Stats []RequirementResultStat `json:"stats"` Stats []RequirementResultStat `json:"stats"`
Items []items.Result `json:"items"` Items []items.Result `json:"items"`
@@ -150,6 +152,7 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
Description: req.Description, Description: req.Description,
Status: req.Status, Status: req.Status,
StatusName: scope.StatusName(req.Status), StatusName: scope.StatusName(req.Status),
IsCoarse: req.IsCoarse,
Stats: make([]RequirementResultStat, 0, 8), Stats: make([]RequirementResultStat, 0, 8),
Items: make([]items.Result, 0, 8), Items: make([]items.Result, 0, 8),
@@ -194,6 +197,21 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
resReq.Items = append(resReq.Items, item) 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 { for _, stat := range scope.Stats {
if rs := resStats[stat.ID]; rs != nil && rs.Required > 0 { if rs := resStats[stat.ID]; rs != nil && rs.Required > 0 {
resReq.Stats = append(resReq.Stats, *rs) resReq.Stats = append(resReq.Stats, *rs)