From eeba5bc9c89b6a6922ee114762eb53b884995d58 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 17 Jan 2021 19:01:13 +0100 Subject: [PATCH] add new fields to logs and goals. --- api/goal.go | 20 +++++++ api/log.go | 27 ++++++++++ cmd/stufflog2-local/main.go | 1 + database/postgres/goals.go | 11 ++-- database/postgres/logs.go | 14 +++-- ..._goal_columns_ct_unweighed_and_item_id.sql | 15 ++++++ ..._log_columns_amount_and_secondary_item.sql | 15 ++++++ models/goal.go | 45 +++++++++++----- models/log.go | 53 +++++++++++++++---- services/loader.go | 52 +++++++++++++----- .../src/components/GroupItemSelect.svelte | 31 +++++++++++ svelte-ui/src/components/ItemLink.svelte | 21 ++++++-- svelte-ui/src/components/ItemSelect.svelte | 6 ++- svelte-ui/src/components/LogEntry.svelte | 5 +- svelte-ui/src/components/Modal.svelte | 3 -- svelte-ui/src/components/TaskEntry.svelte | 10 +--- svelte-ui/src/forms/GoalForm.svelte | 28 +++++++++- svelte-ui/src/forms/LogForm.svelte | 24 ++++++++- svelte-ui/src/models/goal.ts | 10 ++++ svelte-ui/src/models/log.ts | 12 ++++- 20 files changed, 339 insertions(+), 64 deletions(-) create mode 100644 migrations/postgres/20210117171454_add_goal_columns_ct_unweighed_and_item_id.sql create mode 100644 migrations/postgres/20210117172203_add_log_columns_amount_and_secondary_item.sql create mode 100644 svelte-ui/src/components/GroupItemSelect.svelte diff --git a/api/goal.go b/api/goal.go index 7a61943..c1dfd65 100644 --- a/api/goal.go +++ b/api/goal.go @@ -85,6 +85,16 @@ func Goal(g *gin.RouterGroup, db database.Database) { } goal.GroupID = group.ID + if goal.ItemID != nil { + item, err := l.FindItem(c.Request.Context(), *goal.ItemID) + if err != nil { + return nil, slerrors.BadRequest("Item could not be found.") + } + if item.GroupID != goal.GroupID { + return nil, slerrors.BadRequest("Item is not in group.") + } + } + err = db.Goals().Insert(c.Request.Context(), goal) if err != nil { return nil, err @@ -117,6 +127,16 @@ func Goal(g *gin.RouterGroup, db database.Database) { return nil, slerrors.BadRequest("Start time must be before end time.") } + if goal.ItemID != nil && update.ItemID != nil { + item, err := l.FindItem(c.Request.Context(), *goal.ItemID) + if err != nil { + return nil, slerrors.BadRequest("Item could not be found.") + } + if item.GroupID != goal.GroupID { + return nil, slerrors.BadRequest("Item is not in group.") + } + } + err = db.Goals().Update(c.Request.Context(), goal.Goal) if err != nil { return nil, err diff --git a/api/log.go b/api/log.go index 6d1921e..37ce2d6 100644 --- a/api/log.go +++ b/api/log.go @@ -66,6 +66,19 @@ func Log(g *gin.RouterGroup, db database.Database) { } else { log.LoggedTime = log.LoggedTime.UTC() } + if log.ItemAmount < 0 { + return nil, slerrors.BadRequest("Invalid item amount (min: 0).") + } + if log.SecondaryItemAmount < 0 { + return nil, slerrors.BadRequest("Invalid secondary item amount (min: 0).") + } + + if log.SecondaryItemID != nil { + _, err := l.FindItem(c.Request.Context(), *log.SecondaryItemID) + if err != nil { + return nil, slerrors.BadRequest("Item could not be found.") + } + } err = db.Logs().Insert(c.Request.Context(), log) if err != nil { @@ -91,6 +104,20 @@ func Log(g *gin.RouterGroup, db database.Database) { } log.Update(update) + + if log.SecondaryItemID != nil && update.SecondaryItemID != nil { + _, err := l.FindItem(c.Request.Context(), *log.SecondaryItemID) + if err != nil { + return nil, slerrors.BadRequest("Item could not be found.") + } + } + if log.ItemAmount < 0 { + return nil, slerrors.BadRequest("Invalid item amount (min: 0).") + } + if log.SecondaryItemAmount < 0 { + return nil, slerrors.BadRequest("Invalid secondary item amount (min: 0).") + } + err = db.Logs().Update(c.Request.Context(), log.Log) if err != nil { return nil, err diff --git a/cmd/stufflog2-local/main.go b/cmd/stufflog2-local/main.go index c7f3185..e4e00fc 100644 --- a/cmd/stufflog2-local/main.go +++ b/cmd/stufflog2-local/main.go @@ -36,6 +36,7 @@ func main() { server := gin.New() if useDummyUuid == "yes" { + log.Println("Using dummy UUID") server.Use(auth.DummyMiddleware(dummyUuid)) } else { server.Use(auth.TrustingJwtParserMiddleware()) diff --git a/database/postgres/goals.go b/database/postgres/goals.go index 7833cea..845fe66 100644 --- a/database/postgres/goals.go +++ b/database/postgres/goals.go @@ -73,9 +73,11 @@ func (r *goalRepository) List(ctx context.Context, filter models.GoalFilter) ([] func (r *goalRepository) Insert(ctx context.Context, goal models.Goal) error { _, err := r.db.NamedExecContext(ctx, ` INSERT INTO goal ( - goal_id, user_id, group_id, amount, start_time, end_time, name, description + goal_id, user_id, group_id, amount, start_time, end_time, name, description, + composition_mode, unweighted, item_id ) VALUES ( - :goal_id, :user_id, :group_id, :amount, :start_time, :end_time, :name, :description + :goal_id, :user_id, :group_id, :amount, :start_time, :end_time, :name, :description, + :composition_mode, :unweighted, :item_id ) `, &goal) if err != nil { @@ -92,7 +94,10 @@ func (r *goalRepository) Update(ctx context.Context, goal models.Goal) error { start_time=:start_time, end_time=:end_time, name=:name, - description=:description + description=:description, + composition_mode=:composition_mode, + unweighted=:unweighted, + item_id=:item_id WHERE goal_id=:goal_id `, &goal) if err != nil { diff --git a/database/postgres/logs.go b/database/postgres/logs.go index b364318..763da4c 100644 --- a/database/postgres/logs.go +++ b/database/postgres/logs.go @@ -34,7 +34,10 @@ func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*m sq = sq.Where(squirrel.Eq{"task_id": filter.TaskIDs}) } if len(filter.ItemIDs) > 0 { - sq = sq.Where(squirrel.Eq{"item_id": filter.ItemIDs}) + sq = sq.Where(squirrel.Or{ + squirrel.Eq{"item_id": filter.ItemIDs}, + squirrel.Eq{"secondary_item_id": filter.ItemIDs}, + }) } if filter.MinTime != nil { sq = sq.Where(squirrel.GtOrEq{ @@ -69,9 +72,9 @@ func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*m func (r *logRepository) Insert(ctx context.Context, log models.Log) error { _, err := r.db.NamedExecContext(ctx, ` INSERT INTO log ( - log_id, user_id, task_id, item_id, logged_time, description + log_id, user_id, task_id, item_id, logged_time, description, item_amount, secondary_item_id, secondary_item_amount ) VALUES ( - :log_id, :user_id, :task_id, :item_id, :logged_time, :description + :log_id, :user_id, :task_id, :item_id, :logged_time, :description, :item_amount, :secondary_item_id, :secondary_item_amount ) `, &log) if err != nil { @@ -85,7 +88,10 @@ func (r *logRepository) Update(ctx context.Context, log models.Log) error { _, err := r.db.NamedExecContext(ctx, ` UPDATE log SET logged_time=:logged_time, - description=:description + description=:description, + item_amount=:item_amount, + secondary_item_id=:secondary_item_id, + secondary_item_amount=:secondary_item_amount WHERE log_id=:log_id `, &log) if err != nil { diff --git a/migrations/postgres/20210117171454_add_goal_columns_ct_unweighed_and_item_id.sql b/migrations/postgres/20210117171454_add_goal_columns_ct_unweighed_and_item_id.sql new file mode 100644 index 0000000..441116e --- /dev/null +++ b/migrations/postgres/20210117171454_add_goal_columns_ct_unweighed_and_item_id.sql @@ -0,0 +1,15 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE goal + ADD COLUMN composition_mode TEXT NOT NULL DEFAULT 'item', + ADD COLUMN unweighted BOOL NOT NULL DEFAULT false, + ADD COLUMN item_id CHAR(16) DEFAULT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE goal + DROP COLUMN composition_mode, + DROP COLUMN unweighted, + DROP COLUMN item_id; +-- +goose StatementEnd diff --git a/migrations/postgres/20210117172203_add_log_columns_amount_and_secondary_item.sql b/migrations/postgres/20210117172203_add_log_columns_amount_and_secondary_item.sql new file mode 100644 index 0000000..5e9f343 --- /dev/null +++ b/migrations/postgres/20210117172203_add_log_columns_amount_and_secondary_item.sql @@ -0,0 +1,15 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE log + ADD COLUMN item_amount INT NOT NULL DEFAULT 1, + ADD COLUMN secondary_item_id CHAR(16) DEFAULT NULL, + ADD COLUMN secondary_item_amount INT NOT NULL DEFAULT 0; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE log + DROP COLUMN item_amount, + DROP COLUMN secondary_item_id, + DROP COLUMN secondary_item_amount; +-- +goose StatementEnd diff --git a/models/goal.go b/models/goal.go index 009228a..683d532 100644 --- a/models/goal.go +++ b/models/goal.go @@ -6,14 +6,17 @@ import ( ) type Goal struct { - ID string `json:"id" db:"goal_id"` - UserID string `json:"-" db:"user_id"` - GroupID string `json:"groupId" db:"group_id"` - StartTime time.Time `json:"startTime" db:"start_time"` - EndTime time.Time `json:"endTime" db:"end_time"` - Amount int `json:"amount" db:"amount"` - Name string `json:"name" db:"name"` - Description string `json:"description" db:"description"` + ID string `json:"id" db:"goal_id"` + UserID string `json:"-" db:"user_id"` + GroupID string `json:"groupId" db:"group_id"` + ItemID *string `json:"itemId" db:"item_id"` + StartTime time.Time `json:"startTime" db:"start_time"` + EndTime time.Time `json:"endTime" db:"end_time"` + Amount int `json:"amount" db:"amount"` + Unweighted bool `json:"unweighted" db:"unweighted"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + CompositionMode string `json:"compositionMode" db:"composition_mode"` } func (goal *Goal) Update(update GoalUpdate) { @@ -32,14 +35,30 @@ func (goal *Goal) Update(update GoalUpdate) { if update.Description != nil { goal.Description = *update.Description } + if update.Unweighted != nil { + goal.Unweighted = *update.Unweighted + } + if update.CompositionMode != nil { + goal.CompositionMode = *update.CompositionMode + } + if update.ItemID != nil { + goal.ItemID = update.ItemID + } + if update.ClearItemID { + goal.ItemID = nil + } } type GoalUpdate struct { - StartTime *time.Time `json:"startTime"` - EndTime *time.Time `json:"endTime"` - Amount *int `json:"amount"` - Name *string `json:"name"` - Description *string `json:"description"` + StartTime *time.Time `json:"startTime"` + EndTime *time.Time `json:"endTime"` + Amount *int `json:"amount"` + Name *string `json:"name"` + Description *string `json:"description"` + ItemID *string `json:"itemId"` + Unweighted *bool `json:"unweighted"` + CompositionMode *string `json:"compositionMode"` + ClearItemID bool `json:"clearItemID"` } type GoalResult struct { diff --git a/models/log.go b/models/log.go index 4cd2f46..800b85c 100644 --- a/models/log.go +++ b/models/log.go @@ -6,12 +6,28 @@ import ( ) type Log struct { - ID string `json:"id" db:"log_id"` - UserID string `json:"-" db:"user_id"` - TaskID string `json:"taskId" db:"task_id"` - ItemID string `json:"itemId" db:"item_id"` - LoggedTime time.Time `json:"loggedTime" db:"logged_time"` - Description string `json:"description" db:"description"` + ID string `json:"id" db:"log_id"` + UserID string `json:"-" db:"user_id"` + TaskID string `json:"taskId" db:"task_id"` + ItemID string `json:"itemId" db:"item_id"` + ItemAmount int `json:"itemAmount" db:"item_amount"` + SecondaryItemID *string `json:"secondaryItemId" db:"secondary_item_id"` + SecondaryItemAmount int `json:"secondaryItemAmount" db:"secondary_item_amount"` + LoggedTime time.Time `json:"loggedTime" db:"logged_time"` + Description string `json:"description" db:"description"` +} + +func (log *Log) Amount(itemID string) int { + result := 0 + + if log.ItemID == itemID { + result += log.ItemAmount + } + if log.SecondaryItemID != nil && *log.SecondaryItemID == itemID { + result += log.SecondaryItemAmount + } + + return result } func (log *Log) Update(update LogUpdate) { @@ -21,17 +37,34 @@ func (log *Log) Update(update LogUpdate) { if update.Description != nil { log.Description = *update.Description } + if update.ItemAmount != nil { + log.ItemAmount = *update.ItemAmount + } + if update.SecondaryItemID != nil { + log.SecondaryItemID = update.SecondaryItemID + } + if update.ClearSecondaryItem { + log.SecondaryItemID = nil + } + if update.SecondaryItemAmount != nil { + log.SecondaryItemAmount = *update.SecondaryItemAmount + } } type LogUpdate struct { - LoggedTime *time.Time `json:"loggedTime"` - Description *string `json:"description"` + LoggedTime *time.Time `json:"loggedTime"` + Description *string `json:"description"` + ItemAmount *int `json:"itemAmount"` + SecondaryItemID *string `json:"secondaryItemId"` + SecondaryItemAmount *int `json:"secondaryItemAmount"` + ClearSecondaryItem bool `json:"clearSecondaryItem"` } type LogResult struct { Log - Task *Task `json:"task"` - Item *Item `json:"item"` + Task *Task `json:"task"` + Item *Item `json:"item"` + SecondaryItem *Item `json:"secondaryItem"` } type LogFilter struct { diff --git a/services/loader.go b/services/loader.go index d51eb8b..0bad31d 100644 --- a/services/loader.go +++ b/services/loader.go @@ -128,6 +128,9 @@ func (l *Loader) FindLog(ctx context.Context, id string) (*models.LogResult, err result.Task, _ = l.DB.Tasks().Find(ctx, id) result.Item, _ = l.DB.Items().Find(ctx, log.ItemID) + if log.SecondaryItemID != nil { + result.SecondaryItem, _ = l.DB.Items().Find(ctx, *log.SecondaryItemID) + } return result, nil } @@ -144,6 +147,9 @@ func (l *Loader) ListLogs(ctx context.Context, filter models.LogFilter) ([]*mode for _, log := range logs { taskIDs.Add(log.TaskID) itemIDs.Add(log.ItemID) + if log.SecondaryItemID != nil { + itemIDs.Add(*log.SecondaryItemID) + } } tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ UserID: auth.UserID(ctx), @@ -177,7 +183,9 @@ func (l *Loader) ListLogs(ctx context.Context, filter models.LogFilter) ([]*mode for _, item := range items { if item.ID == log.ItemID { results[i].Item = item - break + } + if log.SecondaryItemID != nil && item.ID == *log.SecondaryItemID { + results[i].SecondaryItem = item } } } @@ -502,16 +510,22 @@ func (l *Loader) populateGoals(ctx context.Context, goal *models.Goal) (*models. MinTime: &goal.StartTime, MaxTime: &goal.EndTime, }) + if err != nil { + return nil, err + } // Get tasks - taskIDs := make([]string, 0, len(result.Logs)) + taskIDs := stringset.New() for _, log := range logs { - taskIDs = append(taskIDs, log.TaskID) + taskIDs.Add(log.TaskID) } tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ UserID: userID, - IDs: taskIDs, + IDs: taskIDs.Strings(), }) + if err != nil { + return nil, err + } // Apply logs result.Logs = make([]*models.LogResult, 0, len(logs)) @@ -523,20 +537,34 @@ func (l *Loader) populateGoals(ctx context.Context, goal *models.Goal) (*models. for _, task := range tasks { if task.ID == log.TaskID { resultLog.Task = task + break + } + } - for _, item := range result.Items { - if task.ItemID == item.ID { - item.CompletedAmount += 1 - result.CompletedAmount += item.GroupWeight - break - } + for _, item := range result.Items { + amount := log.Amount(item.ID) + if amount > 0 && (goal.ItemID == nil || *goal.ItemID == item.ID) { + item.CompletedAmount += amount - resultLog.Item = &item.Item + if goal.Unweighted { + result.CompletedAmount += amount + } else { + result.CompletedAmount += amount * item.GroupWeight } + } - break + if item.ID == log.ItemID { + resultLog.Item = &item.Item + if log.SecondaryItemID == nil { + break + } + } + + if log.SecondaryItemID != nil && item.ID == *log.SecondaryItemID { + resultLog.SecondaryItem = &item.Item } } + result.Logs = append(result.Logs, resultLog) } } diff --git a/svelte-ui/src/components/GroupItemSelect.svelte b/svelte-ui/src/components/GroupItemSelect.svelte new file mode 100644 index 0000000..570ca1a --- /dev/null +++ b/svelte-ui/src/components/GroupItemSelect.svelte @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/svelte-ui/src/components/ItemLink.svelte b/svelte-ui/src/components/ItemLink.svelte index 4a8fd58..a870c01 100644 --- a/svelte-ui/src/components/ItemLink.svelte +++ b/svelte-ui/src/components/ItemLink.svelte @@ -3,9 +3,14 @@ import Icon from "./Icon.svelte"; export let item: Item = null; + export let amount: number = null; + export let noPadding: boolean = false; -
+
+ {#if amount != null} +
{amount}x
+ {/if}
@@ -22,13 +27,21 @@ margin-bottom: 0em; font-size: 0.75em; } - div.item a { + a { color: inherit; } - div.item div.item-icon { + div.item-icon { padding: 0.25em 0.5ch 0.25em 0; } - div.item div.item-name { + div.item-name { padding: 0.125em; } + div.item-amount { + padding: 0.125em; + padding-right: 1ch; + } + + div.item.noPadding { + margin-top: 0em; + } \ No newline at end of file diff --git a/svelte-ui/src/components/ItemSelect.svelte b/svelte-ui/src/components/ItemSelect.svelte index 1de9a58..ffd8c83 100644 --- a/svelte-ui/src/components/ItemSelect.svelte +++ b/svelte-ui/src/components/ItemSelect.svelte @@ -4,6 +4,7 @@ export let value = ""; export let name = ""; export let disabled = false; + export let optional = false; $: { if ($groupStore.stale && !$groupStore.loading) { @@ -12,7 +13,7 @@ } $: { - if ($groupStore.groups.length > 0 && value === "") { + if ($groupStore.groups.length > 0 && value === "" && !optional) { const nonEmpty = $groupStore.groups.find(g => g.items.length > 0); if (nonEmpty != null) { value = nonEmpty.items[0].id; @@ -22,6 +23,9 @@