Gisle Aune
5 years ago
42 changed files with 1051 additions and 504 deletions
-
130api/item.go
-
13api/period.go
-
1database/database.go
-
10database/drivers/bolt/db.go
-
197database/drivers/bolt/item.go
-
15database/repositories/item.go
-
4main.go
-
9models/goal.go
-
45models/item.go
-
17models/log.go
-
152models/period.go
-
14services/scoring.go
-
6svelte-ui/src/App.svelte
-
78svelte-ui/src/api/stufflog.js
-
23svelte-ui/src/components/ActivityAmount.svelte
-
26svelte-ui/src/components/ActivityDisplay.svelte
-
4svelte-ui/src/components/ActivityIcon.svelte
-
10svelte-ui/src/components/AddBoi.svelte
-
51svelte-ui/src/components/Col.svelte
-
20svelte-ui/src/components/ItemDisplay.svelte
-
2svelte-ui/src/components/Menu.svelte
-
2svelte-ui/src/components/MenuItem.svelte
-
5svelte-ui/src/components/PointsBar.svelte
-
8svelte-ui/src/components/Row.svelte
-
51svelte-ui/src/components/Table.svelte
-
38svelte-ui/src/components/tables/GoalTable.svelte
-
30svelte-ui/src/components/tables/ItemTable.svelte
-
66svelte-ui/src/components/tables/LogTable.svelte
-
30svelte-ui/src/components/tables/SubActivityTable.svelte
-
42svelte-ui/src/modals/AddPeriodGoalModal.svelte
-
30svelte-ui/src/modals/AddPeriodLogModal.svelte
-
22svelte-ui/src/modals/InfoPeriodLogModal.svelte
-
2svelte-ui/src/modals/RemovePeriodLogModal.svelte
-
6svelte-ui/src/modals/RemoveSubActivityModal.svelte
-
12svelte-ui/src/models/item.d.ts
-
1svelte-ui/src/models/item.js
-
70svelte-ui/src/routes/ActivitiesPage.svelte
-
49svelte-ui/src/routes/ItemPage.svelte
-
164svelte-ui/src/routes/LogPage.svelte
-
58svelte-ui/src/stores/items.js
-
1svelte-ui/src/stores/modal.js
-
23svelte-ui/src/stores/stufflog.js
@ -0,0 +1,130 @@ |
|||||
|
package api |
||||
|
|
||||
|
import ( |
||||
|
"github.com/gin-gonic/gin" |
||||
|
"github.com/gisle/stufflog/database" |
||||
|
"github.com/gisle/stufflog/models" |
||||
|
"github.com/gisle/stufflog/services" |
||||
|
"github.com/gisle/stufflog/slerrors" |
||||
|
"sort" |
||||
|
) |
||||
|
|
||||
|
func Items(g *gin.RouterGroup, db database.Database, auth *services.AuthService) { |
||||
|
type resObj struct { |
||||
|
Item *models.Item `json:"item,omitempty"` |
||||
|
Items []*models.Item `json:"items,omitempty"` |
||||
|
} |
||||
|
|
||||
|
g.Use(auth.GinSessionMiddleware(true)) |
||||
|
|
||||
|
// GET / – List own items
|
||||
|
g.GET("/", func(c *gin.Context) { |
||||
|
user := auth.UserFromContext(c) |
||||
|
|
||||
|
items, err := db.Items().ListUser(c.Request.Context(), *user) |
||||
|
if err != nil { |
||||
|
slerrors.GinRespond(c, err) |
||||
|
return |
||||
|
} |
||||
|
if items == nil { |
||||
|
items = []*models.Item{} |
||||
|
} |
||||
|
|
||||
|
sort.Sort(models.ItemsByName(items)) |
||||
|
|
||||
|
c.JSON(200, resObj{Items: items}) |
||||
|
}) |
||||
|
|
||||
|
// GET /:id – Find an item
|
||||
|
g.GET("/:id", func(c *gin.Context) { |
||||
|
user := auth.UserFromContext(c) |
||||
|
|
||||
|
item, err := db.Items().FindID(c.Request.Context(), c.Param("id")) |
||||
|
if err != nil { |
||||
|
slerrors.GinRespond(c, err) |
||||
|
return |
||||
|
} |
||||
|
if item.UserID != user.ID { |
||||
|
slerrors.GinRespond(c, slerrors.NotFound("Item")) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
c.JSON(200, resObj{Item: item}) |
||||
|
}) |
||||
|
|
||||
|
// POST / – Create an item
|
||||
|
g.POST("/", func(c *gin.Context) { |
||||
|
user := auth.UserFromContext(c) |
||||
|
item := models.Item{} |
||||
|
|
||||
|
err := c.BindJSON(&item) |
||||
|
if err != nil { |
||||
|
slerrors.GinRespond(c, &slerrors.SLError{Code: 400, Text: err.Error()}) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
item.GenerateID() |
||||
|
item.UserID = user.ID |
||||
|
|
||||
|
err = db.Items().Insert(c.Request.Context(), item) |
||||
|
if err != nil { |
||||
|
slerrors.GinRespond(c, err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
c.JSON(200, resObj{Item: &item}) |
||||
|
}) |
||||
|
|
||||
|
// PATCH /:id – Update an item
|
||||
|
g.PATCH("/:id", func(c *gin.Context) { |
||||
|
user := auth.UserFromContext(c) |
||||
|
updates := make([]*models.ItemUpdate, 0, 8) |
||||
|
|
||||
|
err := c.BindJSON(&updates) |
||||
|
if err != nil { |
||||
|
slerrors.GinRespond(c, &slerrors.SLError{Code: 400, Text: err.Error()}) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
item, err := db.Items().FindID(c.Request.Context(), c.Param("id")) |
||||
|
if err != nil { |
||||
|
slerrors.GinRespond(c, err) |
||||
|
return |
||||
|
} |
||||
|
if item.UserID != user.ID { |
||||
|
slerrors.GinRespond(c, slerrors.NotFound("Item")) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
item, err = db.Items().Update(c.Request.Context(), *item, updates) |
||||
|
if err != nil { |
||||
|
slerrors.GinRespond(c, err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
c.JSON(200, resObj{Item: item}) |
||||
|
}) |
||||
|
|
||||
|
// DELETE /:id – Delete an item
|
||||
|
g.DELETE("/:id", func(c *gin.Context) { |
||||
|
user := auth.UserFromContext(c) |
||||
|
|
||||
|
item, err := db.Items().FindID(c.Request.Context(), c.Param("id")) |
||||
|
if err != nil { |
||||
|
slerrors.GinRespond(c, err) |
||||
|
return |
||||
|
} |
||||
|
if item.UserID != user.ID { |
||||
|
slerrors.GinRespond(c, slerrors.NotFound("Period")) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
err = db.Items().Remove(c.Request.Context(), *item) |
||||
|
if err != nil { |
||||
|
slerrors.GinRespond(c, err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
c.JSON(200, resObj{Item: item}) |
||||
|
}) |
||||
|
} |
@ -0,0 +1,197 @@ |
|||||
|
package bolt |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"github.com/gisle/stufflog/database/repositories" |
||||
|
"github.com/gisle/stufflog/models" |
||||
|
"github.com/gisle/stufflog/slerrors" |
||||
|
"github.com/vmihailenco/msgpack/v4" |
||||
|
"go.etcd.io/bbolt" |
||||
|
) |
||||
|
|
||||
|
var bnItems = []byte("Item") |
||||
|
|
||||
|
type itemRepository struct { |
||||
|
db *bbolt.DB |
||||
|
userIdIdx *index |
||||
|
} |
||||
|
|
||||
|
func (r *itemRepository) FindID(ctx context.Context, id string) (*models.Item, error) { |
||||
|
item := new(models.Item) |
||||
|
err := r.db.View(func(tx *bbolt.Tx) error { |
||||
|
value := tx.Bucket(bnItems).Get(unsafeStringToBytes(id)) |
||||
|
if value == nil { |
||||
|
return slerrors.NotFound("Period") |
||||
|
} |
||||
|
err := msgpack.Unmarshal(value, item) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
return nil |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return item, nil |
||||
|
} |
||||
|
|
||||
|
func (r *itemRepository) List(ctx context.Context) ([]*models.Item, error) { |
||||
|
items := make([]*models.Item, 0, 16) |
||||
|
err := r.db.View(func(tx *bbolt.Tx) error { |
||||
|
cursor := tx.Bucket(bnItems).Cursor() |
||||
|
|
||||
|
for key, value := cursor.First(); key != nil; key, value = cursor.Next() { |
||||
|
item := new(models.Item) |
||||
|
err := msgpack.Unmarshal(value, item) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
items = append(items, item) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return items, nil |
||||
|
} |
||||
|
|
||||
|
func (r *itemRepository) ListUser(ctx context.Context, user models.User) ([]*models.Item, error) { |
||||
|
items := make([]*models.Item, 0, 16) |
||||
|
err := r.db.View(func(tx *bbolt.Tx) error { |
||||
|
bucket := tx.Bucket(bnItems) |
||||
|
|
||||
|
ids, err := r.userIdIdx.WithTx(tx).Get(user.ID) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
for _, id := range ids { |
||||
|
value := bucket.Get(id) |
||||
|
if value == nil { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
item := new(models.Item) |
||||
|
err := msgpack.Unmarshal(value, item) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
items = append(items, item) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return items, nil |
||||
|
} |
||||
|
|
||||
|
func (r *itemRepository) Insert(ctx context.Context, item models.Item) error { |
||||
|
value, err := msgpack.Marshal(&item) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return r.db.Update(func(tx *bbolt.Tx) error { |
||||
|
err := tx.Bucket(bnItems).Put(unsafeStringToBytes(item.ID), value) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
err = r.index(tx, &item) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func (r *itemRepository) Update(ctx context.Context, item models.Item, updates []*models.ItemUpdate) (*models.Item, error) { |
||||
|
for _, update := range updates { |
||||
|
err := item.ApplyUpdate(*update) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
value, err := msgpack.Marshal(&item) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
err = r.db.Update(func(tx *bbolt.Tx) error { |
||||
|
return tx.Bucket(bnItems).Put(unsafeStringToBytes(item.ID), value) |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &item, nil |
||||
|
} |
||||
|
|
||||
|
func (r *itemRepository) Remove(ctx context.Context, item models.Item) error { |
||||
|
return r.db.Update(func(tx *bbolt.Tx) error { |
||||
|
err := tx.Bucket(bnItems).Delete(unsafeStringToBytes(item.ID)) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
err = r.unIndex(tx, &item) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func (r *itemRepository) index(tx *bbolt.Tx, item *models.Item) error { |
||||
|
idBytes := unsafeStringToBytes(item.ID) |
||||
|
|
||||
|
err := r.userIdIdx.WithTx(tx).Set(idBytes, item.UserID) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (r *itemRepository) unIndex(tx *bbolt.Tx, item *models.Item) error { |
||||
|
idBytes := unsafeStringToBytes(item.ID) |
||||
|
|
||||
|
err := r.userIdIdx.WithTx(tx).Set(idBytes) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func newItemRepository(db *bbolt.DB) (repositories.ItemRepository, error) { |
||||
|
err := db.Update(func(tx *bbolt.Tx) error { |
||||
|
_, err := tx.CreateBucketIfNotExists(bnItems) |
||||
|
return err |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
userIdIdx, err := newModelIndex(db, "Item", "UserID") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &itemRepository{ |
||||
|
db: db, |
||||
|
userIdIdx: userIdIdx, |
||||
|
}, nil |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
package repositories |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"github.com/gisle/stufflog/models" |
||||
|
) |
||||
|
|
||||
|
type ItemRepository interface { |
||||
|
FindID(ctx context.Context, id string) (*models.Item, error) |
||||
|
List(ctx context.Context) ([]*models.Item, error) |
||||
|
ListUser(ctx context.Context, user models.User) ([]*models.Item, error) |
||||
|
Insert(ctx context.Context, item models.Item) error |
||||
|
Update(ctx context.Context, item models.Item, updates []*models.ItemUpdate) (*models.Item, error) |
||||
|
Remove(ctx context.Context, item models.Item) error |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
package models |
||||
|
|
||||
|
// A Goal is a declared investment into the goal.
|
||||
|
type Goal struct { |
||||
|
ID string `json:"id"` |
||||
|
ActivityID string `json:"activityId"` |
||||
|
ItemID string `json:"itemId"` |
||||
|
PointCount int `json:"pointCount"` |
||||
|
} |
@ -0,0 +1,45 @@ |
|||||
|
package models |
||||
|
|
||||
|
import "github.com/gisle/stufflog/internal/generate" |
||||
|
|
||||
|
type Item struct { |
||||
|
ID string `json:"id" msgpack:"id"` |
||||
|
UserID string `json:"userId" msgpack:"userId"` |
||||
|
Active bool `json:"active" msgpack:"active"` |
||||
|
Name string `json:"name" msgpack:"name"` |
||||
|
Multiplier float64 `json:"multiplier" msgpack:"multiplier"` |
||||
|
} |
||||
|
|
||||
|
func (item *Item) GenerateID() { |
||||
|
item.ID = generate.ID("I", 16) |
||||
|
} |
||||
|
|
||||
|
func (item *Item) ApplyUpdate(update ItemUpdate) error { |
||||
|
if update.SetActive != nil { |
||||
|
item.Active = *update.SetActive |
||||
|
} |
||||
|
if update.SetName != nil { |
||||
|
item.Name = *update.SetName |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
type ItemUpdate struct { |
||||
|
SetName *string `json:"setName"` |
||||
|
SetActive *bool `json:"setActive"` |
||||
|
} |
||||
|
|
||||
|
type ItemsByName []*Item |
||||
|
|
||||
|
func (items ItemsByName) Len() int { |
||||
|
return len(items) |
||||
|
} |
||||
|
|
||||
|
func (items ItemsByName) Less(i, j int) bool { |
||||
|
return items[i].Name < items[j].Name |
||||
|
} |
||||
|
|
||||
|
func (items ItemsByName) Swap(i, j int) { |
||||
|
items[i], items[j] = items[j], items[i] |
||||
|
} |
@ -0,0 +1,17 @@ |
|||||
|
package models |
||||
|
|
||||
|
import "time" |
||||
|
|
||||
|
// Log is a logged performance of an activity during the period. SubGoalID is optional, but GoalID is not. The
|
||||
|
// points is the points calculated on the time of submission and does not need to reflect the latest.
|
||||
|
type Log struct { |
||||
|
Date time.Time `json:"date"` |
||||
|
ID string `json:"id"` |
||||
|
SubActivityID string `json:"subActivityId"` |
||||
|
GoalID string `json:"goalId"` |
||||
|
SubGoalID string `json:"subGoalId"` |
||||
|
Description string `json:"description"` |
||||
|
SubmitTime time.Time `json:"submitTime"` |
||||
|
Amount int `json:"amount"` |
||||
|
Score *Score `json:"score"` |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
<script> |
||||
|
import pluralize from "pluralize"; |
||||
|
|
||||
|
import ActivityIcon from "./ActivityIcon.svelte"; |
||||
|
|
||||
|
export let amount = 0 |
||||
|
export let subActivity = null |
||||
|
</script> |
||||
|
|
||||
|
{#if subActivity != null} |
||||
|
<span>{amount} {pluralize(subActivity.unitName, amount)}</span> |
||||
|
{:else} |
||||
|
<span class="unknown">{amount} (unknown unit)</span> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
span { |
||||
|
display: inline-block; |
||||
|
} |
||||
|
span.unknown { |
||||
|
color: #555; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,26 @@ |
|||||
|
<script> |
||||
|
import ActivityIcon from "./ActivityIcon.svelte"; |
||||
|
|
||||
|
export let activity = null |
||||
|
export let subActivity = null |
||||
|
</script> |
||||
|
|
||||
|
{#if activity != null && !activity.deleted} |
||||
|
<div class="icon"><ActivityIcon name={activity.icon} /></div> |
||||
|
<span class="name">{activity.name}{(subActivity && !subActivity.deleted) ? " " + subActivity.name : ""}</span> |
||||
|
{:else} |
||||
|
<div class="icon unknown"><ActivityIcon name="question" /></div> |
||||
|
<span class="name unknown">(Unknown)</span> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
div.icon { |
||||
|
position: relative; |
||||
|
top: 0.175em; |
||||
|
display: inline-block; |
||||
|
} |
||||
|
|
||||
|
div.unknown, span.unknown { |
||||
|
color: #555; |
||||
|
} |
||||
|
</style> |
@ -1,51 +0,0 @@ |
|||||
<script> |
|
||||
export let size = 1; |
|
||||
</script> |
|
||||
|
|
||||
<div class="col col-{size}"><slot></slot></div> |
|
||||
|
|
||||
<style> |
|
||||
div.col { |
|
||||
display: inline-block; |
|
||||
box-sizing: border-box; |
|
||||
padding: 0; |
|
||||
margin: 0; |
|
||||
} |
|
||||
|
|
||||
div.col-1 { |
|
||||
width: calc((100% / 12) * 1) |
|
||||
} |
|
||||
div.col-2 { |
|
||||
width: calc((100% / 12) * 2) |
|
||||
} |
|
||||
div.col-3 { |
|
||||
width: calc((100% / 12) * 3) |
|
||||
} |
|
||||
div.col-4 { |
|
||||
width: calc((100% / 12) * 4) |
|
||||
} |
|
||||
div.col-5 { |
|
||||
width: calc((100% / 12) * 5) |
|
||||
} |
|
||||
div.col-6 { |
|
||||
width: calc((100% / 12) * 6) |
|
||||
} |
|
||||
div.col-7 { |
|
||||
width: calc((100% / 12) * 7) |
|
||||
} |
|
||||
div.col-8 { |
|
||||
width: calc((100% / 12) * 8) |
|
||||
} |
|
||||
div.col-9 { |
|
||||
width: calc((100% / 12) * 9) |
|
||||
} |
|
||||
div.col-10 { |
|
||||
width: calc((100% / 12) * 10) |
|
||||
} |
|
||||
div.col-11 { |
|
||||
width: calc((100% / 12) * 11) |
|
||||
} |
|
||||
div.col-12 { |
|
||||
width: 100% |
|
||||
} |
|
||||
</style> |
|
@ -0,0 +1,20 @@ |
|||||
|
<script> |
||||
|
import ActivityIcon from "./ActivityIcon.svelte"; |
||||
|
|
||||
|
export let item = null |
||||
|
export let id = "" |
||||
|
</script> |
||||
|
|
||||
|
{#if item != null} |
||||
|
<span>{item.name}</span> |
||||
|
{:else if id != ""} |
||||
|
<span class="unknown">(Deleted)</span> |
||||
|
{:else} |
||||
|
<span class="unknown">(None)</span> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
span.unknown { |
||||
|
color: #555; |
||||
|
} |
||||
|
</style> |
@ -1,8 +0,0 @@ |
|||||
<div class="row"><slot></slot></div> |
|
||||
|
|
||||
<style> |
|
||||
div.row { |
|
||||
display: flex; |
|
||||
flex-wrap: wrap; |
|
||||
} |
|
||||
</style> |
|
@ -0,0 +1,51 @@ |
|||||
|
<script> |
||||
|
export let headers = []; |
||||
|
export let percentages = []; |
||||
|
|
||||
|
let zipped = []; |
||||
|
|
||||
|
$: zipped = headers.map((h, i) => ({header: h, pct: percentages[i]})) |
||||
|
$: if (percentages.length !== headers.length) { |
||||
|
throw new Error(`Mismatching headers (${headers.join(",")}) and percentages (${percentages.join(",")})`) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<table> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
{#each zipped as h} |
||||
|
<th style={`width: ${h.pct}%;`}>{h.header}</th> |
||||
|
{/each} |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<slot></slot> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
|
||||
|
<style> |
||||
|
table { |
||||
|
width: 100%; |
||||
|
padding: 0.5em 0.75ch; |
||||
|
} |
||||
|
|
||||
|
table :global(th), table :global(td) { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
table :global(th) { |
||||
|
text-align: left; |
||||
|
font-size: 0.75em; |
||||
|
} |
||||
|
|
||||
|
table :global(td.ellipsis) { |
||||
|
max-width: 1px; /* Don't ask me why this works... */ |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
table :global(td:last-of-type), table :global(th:last-of-type) { |
||||
|
text-align: right; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,38 @@ |
|||||
|
<script> |
||||
|
import { createEventDispatcher } from 'svelte'; |
||||
|
import pluralize from "pluralize"; |
||||
|
|
||||
|
import Link from "../Link.svelte"; |
||||
|
import Table from "../Table.svelte"; |
||||
|
import ActivityIcon from "../ActivityIcon.svelte"; |
||||
|
import PointsBar from "../PointsBar.svelte"; |
||||
|
import ActivityDisplay from "../ActivityDisplay.svelte"; |
||||
|
|
||||
|
export let period = {goals: []}; |
||||
|
export let activities = []; |
||||
|
|
||||
|
const dispatch = createEventDispatcher(); |
||||
|
let activityMap = {}; |
||||
|
|
||||
|
function onOption(name, goalId) { |
||||
|
const goal = period.goals.find(g => g.id === goalId); |
||||
|
const activity = activities.find(a => a.id === goal.activityId) |
||||
|
|
||||
|
dispatch("option", {name, period, goal, activity}) |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<Table headers={["Activity", "Points", "Options"]} percentages={[25, 55, 20]}> |
||||
|
{#each period.goals as goal (goal.id)} |
||||
|
<tr> |
||||
|
<td class="ellipsis"> |
||||
|
<ActivityDisplay activity={activities.find(a => a.id === goal.activityId)} /> |
||||
|
</td> |
||||
|
<td><PointsBar value={period.scores[goal.id]} goal={goal.pointCount} /></td> |
||||
|
<td class="td-options"> |
||||
|
<Link on:click={() => onOption("periodgoal.remove", goal.id)}>Delete</Link> |
||||
|
</td> |
||||
|
</tr> |
||||
|
{/each} |
||||
|
</Table> |
@ -0,0 +1,30 @@ |
|||||
|
<script> |
||||
|
import { createEventDispatcher } from 'svelte'; |
||||
|
|
||||
|
import Link from "../Link.svelte"; |
||||
|
import Table from "../Table.svelte"; |
||||
|
|
||||
|
export let items = []; |
||||
|
|
||||
|
const dispatch = createEventDispatcher(); |
||||
|
|
||||
|
function onOption(name, id) { |
||||
|
const item = items.find(i => i.id === id) |
||||
|
|
||||
|
dispatch("option", {name, item}) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Table headers={["Name", "Multiplier", "Options"]} percentages={[60, 5, 35]}> |
||||
|
{#each items as item (item.id)} |
||||
|
<tr> |
||||
|
<td class="ellipsis">{item.name}</td> |
||||
|
<td>{item.multiplier.toFixed(2)}</td> |
||||
|
<td> |
||||
|
<Link on:click={() => onOption("item.stats", item.id)}>Stats</Link>, |
||||
|
<Link on:click={() => onOption("item.edit", item.id)}>Edit</Link>, |
||||
|
<Link on:click={() => onOption("item.remove", item.id)}>Delete</Link> |
||||
|
</td> |
||||
|
</tr> |
||||
|
{/each} |
||||
|
</Table> |
@ -0,0 +1,66 @@ |
|||||
|
<script> |
||||
|
import pluralize from "pluralize"; |
||||
|
|
||||
|
import Link from "../Link.svelte"; |
||||
|
import Table from "../Table.svelte"; |
||||
|
|
||||
|
import { createEventDispatcher } from 'svelte'; |
||||
|
import ActivityIcon from "../ActivityIcon.svelte"; |
||||
|
import ActivityDisplay from "../ActivityDisplay.svelte"; |
||||
|
import ActivityAmount from "../ActivityAmount.svelte"; |
||||
|
import ItemDisplay from "../ItemDisplay.svelte"; |
||||
|
import PointsBar from "../PointsBar.svelte"; |
||||
|
|
||||
|
import dateStr from "../../utils/dateStr"; |
||||
|
|
||||
|
export let period = {goals: []}; |
||||
|
export let activities = []; |
||||
|
export let items = []; |
||||
|
|
||||
|
const dispatch = createEventDispatcher(); |
||||
|
|
||||
|
let subActivityMap = {}; |
||||
|
|
||||
|
function onOption(name, logId) { |
||||
|
const log = period.logs.find(l => l.id === logId); |
||||
|
const goal = period.goals.find(g => g.id === log.goalId) |
||||
|
const activity = goal != null ? activities.find(a => a.id === goal.activityId) : null; |
||||
|
const subActivity = activity != null ? activity.subActivities.find(s => s.id === log.subActivityId) : null; |
||||
|
|
||||
|
dispatch("option", {name, period, log, goal, activity, subActivity}) |
||||
|
} |
||||
|
|
||||
|
$: subActivityMap = activities.map(a => a.subActivities).flat().reduce((p, v) => ({...p, [v.id]: v}), {}); |
||||
|
</script> |
||||
|
|
||||
|
<Table |
||||
|
headers={["Date", "Goal", "Item", "Amount", "Points", "Options"]} |
||||
|
percentages={[25, 25, 15, 15, 5, 15]} |
||||
|
> |
||||
|
{#each period.logs as log (log.id)} |
||||
|
<tr> |
||||
|
<td>{dateStr(log.date)}</td> |
||||
|
<td class="ellipsis"> |
||||
|
<ActivityDisplay |
||||
|
activity={activities.find(a => a.id === (period.goals.find(g => g.id === log.goalId) || {}).activityId)} |
||||
|
subActivity={subActivityMap[log.subActivityId]} |
||||
|
/> |
||||
|
</td> |
||||
|
<td> |
||||
|
<ItemDisplay id={log.itemId} item={items.find(i => i.id === log.itemId)} /> |
||||
|
</td> |
||||
|
<td> |
||||
|
<ActivityAmount |
||||
|
amount={log.amount} |
||||
|
subActivity={subActivityMap[log.subActivityId]} |
||||
|
/> |
||||
|
</td> |
||||
|
<td> |
||||
|
<Link on:click={() => onOption("periodlog.info", log.id)}>{log.score.total}</Link> |
||||
|
</td> |
||||
|
<td class="td-options"> |
||||
|
<Link on:click={() => onOption("periodlog.remove", log.id)}>Delete</Link> |
||||
|
</td> |
||||
|
</tr> |
||||
|
{/each} |
||||
|
</Table> |
@ -0,0 +1,30 @@ |
|||||
|
<script> |
||||
|
import { createEventDispatcher } from 'svelte'; |
||||
|
import pluralize from "pluralize"; |
||||
|
|
||||
|
import Link from "../Link.svelte"; |
||||
|
import Table from "../Table.svelte"; |
||||
|
|
||||
|
export let activity = {subActivities: []} |
||||
|
|
||||
|
const dispatch = createEventDispatcher(); |
||||
|
|
||||
|
function onOption(name, subId) { |
||||
|
const subActivity = activity.subActivities.find(s => s.id === subId); |
||||
|
|
||||
|
dispatch("option", {name, activity, subActivity}) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Table headers={["Sub-Activity", "Value", "Options"]} percentages={[50, 25, 25]}> |
||||
|
{#each activity.subActivities as subActivtiy (subActivtiy.id)} |
||||
|
<tr> |
||||
|
<td>{subActivtiy.name}</td> |
||||
|
<td>{subActivtiy.value} per {pluralize(subActivtiy.unitName, 1)}</td> |
||||
|
<td> |
||||
|
<Link on:click={() => onOption("subactivity.edit", subActivtiy.id)}>Edit</Link>, |
||||
|
<Link on:click={() => onOption("subactivity.remove", subActivtiy.id)}>Delete</Link> |
||||
|
</td> |
||||
|
</tr> |
||||
|
{/each} |
||||
|
</Table> |
@ -0,0 +1,12 @@ |
|||||
|
export default interface Item { |
||||
|
id: string |
||||
|
userId: string |
||||
|
active: boolean |
||||
|
name: string |
||||
|
multiplier: number |
||||
|
} |
||||
|
|
||||
|
export interface ItemUpdate { |
||||
|
setName: string |
||||
|
setActive: boolean |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
module.exports = {} |
@ -0,0 +1,49 @@ |
|||||
|
<script> |
||||
|
import {onMount} from "svelte"; |
||||
|
import {get} from "svelte/store"; |
||||
|
|
||||
|
import items from "../stores/items"; |
||||
|
import modal from "../stores/modal"; |
||||
|
|
||||
|
import Boi from "../components/Boi.svelte"; |
||||
|
import AddBoi from "../components/AddBoi.svelte"; |
||||
|
import Link from "../components/Link.svelte"; |
||||
|
import ItemTable from "../components/tables/ItemTable.svelte"; |
||||
|
|
||||
|
let activeItems = []; |
||||
|
let inactiveItems = []; |
||||
|
|
||||
|
onMount(() => { |
||||
|
items.listItems().catch(err => { |
||||
|
console.warn("Item fetch failed:", err) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
function openModal(modalName, id) { |
||||
|
const list = get(items) |
||||
|
const item = list.find(i => i.id === id) |
||||
|
|
||||
|
modal.open(modalName, {item}) |
||||
|
} |
||||
|
|
||||
|
$: activeItems = $items.filter(i => i.active) |
||||
|
$: inactiveItems = $items.filter(i => !i.active) |
||||
|
</script> |
||||
|
|
||||
|
<div class="page"> |
||||
|
<AddBoi top on:click={() => modal.open("item.create")}>Item</AddBoi> |
||||
|
<Boi header="Active Items" icon="cubes"> |
||||
|
<ItemTable items={activeItems} /> |
||||
|
</Boi> |
||||
|
<Boi header="Archived Items" icon="archive"> |
||||
|
<ItemTable items={inactiveItems} /> |
||||
|
</Boi> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
div.page { |
||||
|
width: 100ch; |
||||
|
max-width: 90%; |
||||
|
margin: auto; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,58 @@ |
|||||
|
import { writable } from "svelte/store"; |
||||
|
|
||||
|
import slApi from "../api/stufflog"; |
||||
|
|
||||
|
import stufflogStore from "./stufflog"; |
||||
|
|
||||
|
function createItemStore() { |
||||
|
const {set, update, subscribe} = writable([]) |
||||
|
|
||||
|
return { |
||||
|
subscribe, |
||||
|
|
||||
|
listItems() { |
||||
|
return slApi.listItems().then(items => { |
||||
|
set(items) |
||||
|
console.log(items) |
||||
|
return items |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
findItem(id) { |
||||
|
return slApi.getItem(id).then(item => { |
||||
|
update(s => replaceItem(s, item)) |
||||
|
return item |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
createItem(item) { |
||||
|
return slApi.postItem(item).then(item => { |
||||
|
update(s => replaceItem(s, item)) |
||||
|
return item |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
updateItem(id, ...updates) { |
||||
|
return slApi.updateItem(id, ...updates).then(item => { |
||||
|
update(s => replaceItem(s, item)) |
||||
|
return item |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
deleteItem(id) { |
||||
|
return slApi.deleteItem(id).then(item => { |
||||
|
update(s => s.items.filter(i => i.id !== item.id)) |
||||
|
return item |
||||
|
}) |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function replaceItem(s, item) { |
||||
|
return ([ |
||||
|
...s.items.filter(i => i.id !== item.id), |
||||
|
item |
||||
|
].sort((a, b) => a.name.localeCompare(b.name))) |
||||
|
} |
||||
|
|
||||
|
export default createItemStore(); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue