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