Browse Source

add more stuff

master
Gisle Aune 2 years ago
parent
commit
3f820275cd
  1. 1
      entities/stat.go
  2. 36
      frontend/src/lib/components/common/LabeledProgress.svelte
  3. 8
      frontend/src/lib/components/common/Progress.svelte
  4. 15
      frontend/src/lib/components/history/HistoryGroupSection.svelte
  5. 19
      frontend/src/lib/components/layout/Main.svelte
  6. 19
      frontend/src/lib/components/layout/Section.svelte
  7. 38
      frontend/src/lib/components/layout/SubSection.svelte
  8. 52
      frontend/src/lib/components/layout/Title.svelte
  9. 4
      frontend/src/lib/components/project/ItemSubSection.svelte
  10. 7
      frontend/src/lib/components/project/RequirementSection.svelte
  11. 24
      frontend/src/lib/components/scope/StatSubSection.svelte
  12. 4
      frontend/src/lib/utils/items.ts
  13. 17
      frontend/src/routes/[scope=prettyid]/history/[interval].svelte
  14. 1
      ports/mysql/mysqlcore/models.go
  15. 17
      ports/mysql/mysqlcore/stats.sql.go
  16. 9
      ports/mysql/queries/stats.sql
  17. 8
      ports/mysql/stats.go
  18. 9
      scripts/goose-mysql/20220626114006_stat_column_primary.sql
  19. 34
      usecases/projects/result.go
  20. 2
      usecases/scopes/result.go

1
entities/stat.go

@ -8,6 +8,7 @@ type Stat struct {
Name string `json:"name"`
Weight float64 `json:"weight"`
Description string `json:"description"`
Primary bool `json:"primary"`
AllowedAmounts []models.StatAllowedAmount `json:"allowedAmounts"`
}

36
frontend/src/lib/components/common/LabeledProgress.svelte

@ -35,20 +35,25 @@
<div class="progress" class:fullwidth>
<div class="header">
<span>{name}</span>
{#if percentageOnly}
<span class="ackreq">({((count / target) * 100).toFixed(0)}%)</span>
{:else}
<span class="ackreq">({Math.floor(count)} / {Math.ceil(target)})</span>
{#if target > 0}
{#if percentageOnly}
<span class="ackreq">({((count / target) * 100).toFixed(0)}%)</span>
{:else}
<span class="ackreq">({Math.floor(count)} / {Math.ceil(target)})</span>
{/if}
{/if}
</div>
<Progress
noTitle
green={green}
alwaysSmooth={alwaysSmooth}
count={count}
target={target}
/>
{#if target > 0}
<Progress
noTitle
green={green}
alwaysSmooth={alwaysSmooth}
count={count}
target={target}
/>
{:else}
<div class="number">{count}</div>
{/if}
{#if boatEnabled}
<Progress titlePercentageOnly alwaysSmooth thinner count={boat} target={1} />
{/if}
@ -76,6 +81,13 @@
flex-basis: calc(100% - 1ch);
}
div.number {
margin: 0;
padding: 0;
line-height: 0.79em;
color: $color-entry11;
}
div.header {
font-size: 0.9em;
margin-bottom: 0.1em;

8
frontend/src/lib/components/common/Progress.svelte

@ -48,12 +48,16 @@
if (green) {
if (status != null) {
offClass = `sc-${status}`
onClass = `sc-${status}`
} else {
offClass = "green"
onClass = "green"
}
offClass = "none"
if (count < target) {
offClass = "none";
}
}
if (gray) {
onClass = "gray"
@ -124,7 +128,7 @@
margin: 0;
box-sizing: border-box;
width: 100%;
height: 10px;
height: 12px;
margin: 0.5px 0;
transform: skew(-11.25deg, 0);
}

15
frontend/src/lib/components/history/HistoryGroupSection.svelte

@ -0,0 +1,15 @@
<script lang="ts">
import type Item from "$lib/models/item";
import type { ItemGroup } from "$lib/models/item";
import Section from "../layout/Section.svelte";
import ItemSubSection from "../project/ItemSubSection.svelte";
export let group: ItemGroup;
export let items: Item[];
</script>
<Section title={group.label}>
{#each group.list as ref (ref.idx)}
<ItemSubSection item={items[ref.idx]} event={ref.event} />
{/each}
</Section>

19
frontend/src/lib/components/layout/Main.svelte

@ -2,7 +2,8 @@
import Icon from "./Icon.svelte";
import type { IconName } from "./Icon.svelte";
import type Status from "$lib/models/status";
import StatusColor from "../common/StatusColor.svelte";
import StatusColor from "../common/StatusColor.svelte";
import Title from "./Title.svelte";
export let title = "";
export let big = false;
@ -10,17 +11,6 @@ import StatusColor from "../common/StatusColor.svelte";
export let noProgress = false;
export let icon: IconName = null;
export let status: Status = null;
let titleChunks: string[];
$: {
const pos = title.indexOf(":");
if (pos != 0) {
titleChunks = [title.slice(0, pos + 1), title.slice(pos + 1).trim()];
} else {
titleChunks = ["", title];
}
}
</script>
<div class="sl3-entry">
@ -32,10 +22,7 @@ import StatusColor from "../common/StatusColor.svelte";
</StatusColor>
{/if}
</div>
<h2 class:big class:small>
<span class="sub">{titleChunks[0]}</span>
<span>{titleChunks[1]}</span>
</h2>
<Title value={title} big={big} small={small} />
<div class="right" class:noProgress>
<slot name="right"></slot>
</div>

19
frontend/src/lib/components/layout/Section.svelte

@ -2,7 +2,8 @@
import Icon from "./Icon.svelte";
import type { IconName } from "./Icon.svelte";
import type Status from "$lib/models/status";
import StatusColor from "../common/StatusColor.svelte";
import StatusColor from "../common/StatusColor.svelte";
import Title from "./Title.svelte";
export let title = "";
export let big = false;
@ -10,17 +11,6 @@ import StatusColor from "../common/StatusColor.svelte";
export let noProgress = false;
export let icon: IconName = null;
export let status: Status = null;
let titleChunks: string[];
$: {
const pos = title.indexOf(":");
if (pos != 0) {
titleChunks = [title.slice(0, pos + 1), title.slice(pos + 1).trim()];
} else {
titleChunks = ["", title];
}
}
</script>
<div class="sl3-entry">
@ -32,10 +22,7 @@ import StatusColor from "../common/StatusColor.svelte";
</StatusColor>
{/if}
</div>
<h2 class:big class:small>
<span class="sub">{titleChunks[0]}</span>
<span>{titleChunks[1]}</span>
</h2>
<Title value={title} big={big} small={small} />
<div class="right" class:noProgress>
<slot name="right"></slot>
</div>

38
frontend/src/lib/components/layout/SubSection.svelte

@ -2,7 +2,8 @@
import Icon from "./Icon.svelte";
import type { IconName } from "./Icon.svelte";
import type Status from "$lib/models/status";
import StatusColor from "../common/StatusColor.svelte";
import StatusColor from "../common/StatusColor.svelte";
import Title from "./Title.svelte";
export let title = "";
export let subtitle = "";
@ -11,35 +12,24 @@ import StatusColor from "../common/StatusColor.svelte";
export let noProgress = false;
export let icon: IconName = null;
export let status: Status = null;
export let event: string = "";
let titleChunks: string[];
let blankEvent: boolean
$: {
const pos = title.indexOf(":");
if (pos != 0) {
titleChunks = [title.slice(0, pos + 1), title.slice(pos + 1).trim()];
} else {
titleChunks = ["", title];
}
}
$: blankEvent = !event || event === "none";
</script>
<div class="sl3-entry">
<div class="header">
<div class="entry-icon">
<div class="entry-icon" class:blankEvent>
{#if icon != null}
<StatusColor status={status}>
<Icon name={icon} />
<span class="event">{event}</span>
</StatusColor>
{/if}
</div>
<h2 class:big class:small>
<span class="sub">{titleChunks[0]}</span>
<span>{titleChunks[1]}</span>
{#if subtitle != ""}
<span class="sub">{subtitle}</span>
{/if}
</h2>
<Title value={title} big={big} small={small} sub={subtitle} />
<div class="right" class:noProgress>
<slot name="right"></slot>
</div>
@ -59,13 +49,23 @@ import StatusColor from "../common/StatusColor.svelte";
div.entry-icon
opacity: 0.6
margin-right: 0.5ch
margin-right: 1.5ch
font-size: 0.75em
margin-top: auto
span.event
font-size: 1.33em
font-weight: 800
&:empty
display: none
&.blankEvent
margin-right: 0.5ch
span.event
display: none
div.header
display: flex

52
frontend/src/lib/components/layout/Title.svelte

@ -0,0 +1,52 @@
<script lang="ts">
export let value = "";
export let sub = "";
export let small = false;
export let big = false;
let titleChunks: string[];
$: {
const pos = value.indexOf(":");
if (pos !== -1) {
titleChunks = [value.slice(0, pos + 1), value.slice(pos + 1).trim()];
} else {
titleChunks = ["", value];
}
const last = titleChunks[titleChunks.length - 1];
const pos2 = last.indexOf("(");
if (pos2 !== -1) {
titleChunks.pop();
titleChunks.push(last.slice(0, pos2), last.slice(pos2));
}
}
</script>
<h2 class:big class:small>
<span class="sub">{titleChunks[0]}</span>
<span>{titleChunks[1]}</span>
<span class="sub">{titleChunks[2]||""}</span>
{#if sub != ""}
<span class="sub">{sub}</span>
{/if}
</h2>
<style lang="sass">
@import "../../css/colors"
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
</style>

4
frontend/src/lib/components/project/ItemSubSection.svelte

@ -7,7 +7,7 @@
import Status from "$lib/models/status";
import Amount from "../common/Amount.svelte";
import AmountRow from "../common/AmountRow.svelte";
import type { IconName } from "../layout/Icon.svelte";
import type { IconName } from "../layout/Icon.svelte";
export let item: Item;
export let event: "created" | "scheduled" | "acquired" | "none" = "none";
@ -33,7 +33,7 @@ import type { IconName } from "../layout/Icon.svelte";
}
</script>
<SubSection small noProgress title={item.name} icon={icon} status={status}>
<SubSection small noProgress title={item.name} icon={icon} status={status} event={event}>
<Markdown source={item.description} />
<OptionsRow slot="right">
{#if item.acquiredTime == null}

7
frontend/src/lib/components/project/RequirementSection.svelte

@ -7,9 +7,8 @@
import type { Requirement } from "$lib/models/project";
import LabeledProgress from "../common/LabeledProgress.svelte";
import LabeledProgressRow from "../common/LabeledProgressRow.svelte";
import ProgressRow from "../common/LabeledProgressRow.svelte";
import { STATUS_ICONS } from "../common/StatusIcon.svelte";
import ItemEntry from "./ItemSubSection.svelte";
import ItemEntry from "./ItemSubSection.svelte";
export let requirement: Requirement;
</script>
@ -25,9 +24,7 @@ import ItemEntry from "./ItemSubSection.svelte";
</OptionsRow>
<LabeledProgressRow>
{#each requirement.stats as stat (stat.id)}
{#if stat.required > 0}
<LabeledProgress count={stat.acquired} target={stat.required} name={stat.name} />
{/if}
<LabeledProgress count={stat.acquired} target={stat.required} name={stat.name} />
{/each}
</LabeledProgressRow>
{#each requirement.items as item (item.id)}

24
frontend/src/lib/components/scope/StatSubSection.svelte

@ -1,24 +1,24 @@
<script lang="ts">
import type Stat from "$lib/models/stat";
import Amount from "../common/Amount.svelte";
import AmountRow from "../common/AmountRow.svelte";
import Markdown from "../common/Markdown.svelte";
import Option from "../layout/Option.svelte";
import OptionsRow from "../layout/OptionsRow.svelte";
import SubSection from "../layout/SubSection.svelte";
import Amount from "../common/Amount.svelte";
import AmountRow from "../common/AmountRow.svelte";
import Markdown from "../common/Markdown.svelte";
import Option from "../layout/Option.svelte";
import OptionsRow from "../layout/OptionsRow.svelte";
import SubSection from "../layout/SubSection.svelte";
export let stat: Stat;
</script>
<SubSection noProgress title={stat.name} subtitle="{stat.weight}x">
<SubSection noProgress title={stat.name}>
<OptionsRow slot="right">
<Option open={{name: "stat.edit", stat}}>Edit</Option>
<Option open={{name: "stat.delete", stat}} color="red">Delete</Option>
</OptionsRow>
<Markdown source={stat.description} />
<AmountRow>
{#each (stat.allowedAmounts || []) as amount (amount.value)}
<Amount label={amount.label} value={amount.value} />
{/each}
</AmountRow>
{#if stat.weight !== 1}
<AmountRow>
<Amount label="Weight" value={stat.weight} />
</AmountRow>
{/if}
</SubSection>

4
frontend/src/lib/utils/items.ts

@ -2,7 +2,7 @@ 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[]): ItemGroup[] {
export function groupItems(items: Item[], minDate?: string): ItemGroup[] {
let groups: Record<string, ItemGroup> = {};
for (let i = 0; i < items.length; ++i) {
@ -28,7 +28,7 @@ export function groupItems(items: Item[]): ItemGroup[] {
list: groups[k].list.sort((a, b) => {
return b.sortKey.localeCompare(a.sortKey);
}).map(r => ({...r, sortKey: void(0)})),
}));
})).filter(g => minDate == null || g.label > minDate);
}
function addItem(groups: Record<string, ItemGroup>, key: string, index: number, sortKey: string, item: Item, event: ItemGroupReference["event"]) {

17
frontend/src/routes/[scope=prettyid]/history/[interval].svelte

@ -19,32 +19,27 @@ import type { ItemGroup } from "$lib/models/item";
});
return {
props: { items, groups: groupItems(items), },
props: { items, groups: groupItems(items, datesOf(interval)?.min), },
};
}
</script>
<script lang="ts">
import ItemSubSection from "$lib/components/project/ItemSubSection.svelte"
import HistoryGroupSection from "$lib/components/history/HistoryGroupSection.svelte";
export let items: Item[]
export let groups: ItemGroup[]
</script>
<h1>History</h1>
{#each groups as group (group.label)}
<h2>{group.label}</h2>
{#each group.list as ref (ref.idx)}
<ItemSubSection item={items[ref.idx]} event={ref.event} />
<div>
{#each groups as group (group.label)}
<HistoryGroupSection group={group} items={items} />
{/each}
{/each}
</div>
<style lang="sass">
h1
margin-top: 1.5em
h2
text-align: center
font-weight: 100
</style>

1
ports/mysql/mysqlcore/models.go

@ -97,4 +97,5 @@ type Stat struct {
Description string
Weight float64
AllowedAmounts sqltypes.NullRawMessage
IsPrimary bool
}

17
ports/mysql/mysqlcore/stats.sql.go

@ -54,7 +54,7 @@ func (q *Queries) DeleteStat(ctx context.Context, arg DeleteStatParams) error {
}
const getStat = `-- name: GetStat :one
SELECT id, scope_id, name, description, weight, allowed_amounts FROM stat WHERE id = ? AND scope_id = ?
SELECT id, scope_id, name, description, weight, allowed_amounts, is_primary FROM stat WHERE id = ? AND scope_id = ?
`
type GetStatParams struct {
@ -72,13 +72,14 @@ func (q *Queries) GetStat(ctx context.Context, arg GetStatParams) (Stat, error)
&i.Description,
&i.Weight,
&i.AllowedAmounts,
&i.IsPrimary,
)
return i, err
}
const insertStat = `-- name: InsertStat :execresult
INSERT INTO stat (scope_id, name, description, weight, allowed_amounts)
VALUES (?, ?, ?, ?, ?)
INSERT INTO stat (scope_id, name, description, weight, allowed_amounts, is_primary)
VALUES (?, ?, ?, ?, ?, ?)
`
type InsertStatParams struct {
@ -87,6 +88,7 @@ type InsertStatParams struct {
Description string
Weight float64
AllowedAmounts sqltypes.NullRawMessage
IsPrimary bool
}
func (q *Queries) InsertStat(ctx context.Context, arg InsertStatParams) (sql.Result, error) {
@ -96,11 +98,12 @@ func (q *Queries) InsertStat(ctx context.Context, arg InsertStatParams) (sql.Res
arg.Description,
arg.Weight,
arg.AllowedAmounts,
arg.IsPrimary,
)
}
const listStats = `-- name: ListStats :many
SELECT id, scope_id, name, description, weight, allowed_amounts FROM stat WHERE scope_id = ? ORDER BY name
SELECT id, scope_id, name, description, weight, allowed_amounts, is_primary FROM stat WHERE scope_id = ? ORDER BY is_primary DESC, name
`
func (q *Queries) ListStats(ctx context.Context, scopeID int) ([]Stat, error) {
@ -119,6 +122,7 @@ func (q *Queries) ListStats(ctx context.Context, scopeID int) ([]Stat, error) {
&i.Description,
&i.Weight,
&i.AllowedAmounts,
&i.IsPrimary,
); err != nil {
return nil, err
}
@ -138,7 +142,8 @@ UPDATE stat
SET name = ?,
description = ?,
weight = ?,
allowed_amounts = ?
allowed_amounts = ?,
is_primary = ?
WHERE id = ? AND scope_id = ?
`
@ -147,6 +152,7 @@ type UpdateStatParams struct {
Description string
Weight float64
AllowedAmounts sqltypes.NullRawMessage
IsPrimary bool
ID int
ScopeID int
}
@ -157,6 +163,7 @@ func (q *Queries) UpdateStat(ctx context.Context, arg UpdateStatParams) error {
arg.Description,
arg.Weight,
arg.AllowedAmounts,
arg.IsPrimary,
arg.ID,
arg.ScopeID,
)

9
ports/mysql/queries/stats.sql

@ -2,18 +2,19 @@
SELECT * FROM stat WHERE id = ? AND scope_id = ?;
-- name: ListStats :many
SELECT * FROM stat WHERE scope_id = ? ORDER BY name;
SELECT * FROM stat WHERE scope_id = ? ORDER BY is_primary DESC, name;
-- name: InsertStat :execresult
INSERT INTO stat (scope_id, name, description, weight, allowed_amounts)
VALUES (?, ?, ?, ?, ?);
INSERT INTO stat (scope_id, name, description, weight, allowed_amounts, is_primary)
VALUES (?, ?, ?, ?, ?, ?);
-- name: UpdateStat :exec
UPDATE stat
SET name = ?,
description = ?,
weight = ?,
allowed_amounts = ?
allowed_amounts = ?,
is_primary = ?
WHERE id = ? AND scope_id = ?;
-- name: DeleteStat :exec

8
ports/mysql/stats.go

@ -42,6 +42,7 @@ func (r *statsRepository) Find(ctx context.Context, scopeID, statID int) (*entit
Weight: row.Weight,
Description: row.Description,
AllowedAmounts: allowedAmounts,
Primary: row.IsPrimary,
}, nil
}
@ -76,6 +77,7 @@ func (r *statsRepository) List(ctx context.Context, scopeIDs ...int) ([]entities
Weight: row.Weight,
Description: row.Description,
AllowedAmounts: allowedAmounts,
Primary: row.IsPrimary,
})
}
@ -83,7 +85,7 @@ func (r *statsRepository) List(ctx context.Context, scopeIDs ...int) ([]entities
}
rows, err := squirrel.
Select("id, scope_id, name, description, weight, allowed_amounts").
Select("id, scope_id, name, description, weight, allowed_amounts, is_primary").
From("stat").
Where(squirrel.Eq{"scope_id": scopeIDs}).
RunWith(r.db).
@ -104,6 +106,7 @@ func (r *statsRepository) List(ctx context.Context, scopeIDs ...int) ([]entities
&row.Description,
&row.Weight,
&row.AllowedAmounts,
&row.IsPrimary,
); err != nil {
return nil, err
}
@ -114,6 +117,7 @@ func (r *statsRepository) List(ctx context.Context, scopeIDs ...int) ([]entities
Name: row.Name,
Weight: row.Weight,
Description: row.Description,
Primary: row.IsPrimary,
AllowedAmounts: genutils.ParseJSONArray[models.StatAllowedAmount](row.AllowedAmounts.RawMessage),
})
}
@ -134,6 +138,7 @@ func (r *statsRepository) Insert(ctx context.Context, stat entities.Stat) (*enti
Name: stat.Name,
Description: stat.Description,
Weight: stat.Weight,
IsPrimary: stat.Primary,
AllowedAmounts: sqlJsonPtr(stat.AllowedAmounts),
})
if err != nil {
@ -157,6 +162,7 @@ func (r *statsRepository) Update(ctx context.Context, stat entities.Stat, update
Description: stat.Description,
Weight: stat.Weight,
AllowedAmounts: sqlJsonPtr(stat.AllowedAmounts),
IsPrimary: stat.Primary,
ID: stat.ID,
ScopeID: stat.ScopeID,
})

9
scripts/goose-mysql/20220626114006_stat_column_primary.sql

@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE stat ADD COLUMN is_primary BOOLEAN NOT NULL DEFAULT 0;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE stat DROP COLUMN IF EXISTS is_primary;
-- +goose StatementEnd

34
usecases/projects/result.go

@ -6,7 +6,6 @@ import (
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"math"
"sort"
"time"
)
@ -156,7 +155,8 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
Requirement: req,
}
statIndices := make(map[int]int)
resStats := make(map[int]*RequirementResultStat)
for _, reqStat := range requirementStats {
if reqStat.RequirementID != req.ID {
continue
@ -174,8 +174,7 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
}
}
resReq.Stats = append(resReq.Stats, resStat)
statIndices[reqStat.StatID] = len(resReq.Stats) - 1
resStats[resStat.ID] = &resStat
}
for _, item := range projectItems {
@ -185,9 +184,9 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
if item.AcquiredTime != nil {
for _, stat := range item.Stats {
if statIndex, ok := statIndices[stat.ID]; ok {
resReq.Stats[statIndex].Acquired += stat.Acquired
resReq.Stats[statIndex].Planned += stat.Required
if resStats[stat.ID] != nil {
resStats[stat.ID].Acquired += stat.Acquired
resStats[stat.ID].Planned += stat.Required
}
}
}
@ -195,17 +194,26 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
resReq.Items = append(resReq.Items, item)
}
sort.Slice(resReq.Stats, func(i, j int) bool {
return resReq.Stats[i].Name < resReq.Stats[j].Name
})
for _, stat := range scope.Stats {
if rs := resStats[stat.ID]; rs != nil && rs.Required > 0 {
resReq.Stats = append(resReq.Stats, *rs)
}
}
for _, stat := range scope.Stats {
if rs := resStats[stat.ID]; rs != nil && rs.Required == 0 {
resReq.Stats = append(resReq.Stats, *rs)
}
}
totalAcquired := 0.0
totalRequired := 0.0
totalPlanned := 0.0
for _, stat := range resReq.Stats {
totalAcquired += float64(stat.Acquired) * stat.Weight
totalRequired += float64(stat.Required) * stat.Weight
totalPlanned += float64(stat.Planned) * stat.Weight
if stat.Required > 0 {
totalAcquired += float64(stat.Acquired) * stat.Weight
totalRequired += float64(stat.Required) * stat.Weight
totalPlanned += float64(stat.Planned) * stat.Weight
}
}
resReq.TotalRequired += int(totalRequired)
resReq.TotalAcquired += int(math.Min(totalAcquired, totalRequired))

2
usecases/scopes/result.go

@ -17,7 +17,7 @@ func (r *Result) MemberName(id string) string {
return member.Name
}
return "???"
return ""
}
func (r *Result) Stat(id int) *ResultStat {

Loading…
Cancel
Save