clean up svelte ui big time, and add beginnings of item stuff.
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+130
@@ -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})
|
||||
})
|
||||
}
|
||||
+2
-11
@@ -78,21 +78,12 @@ func Period(g *gin.RouterGroup, db database.Database, scoring *services.ScoringS
|
||||
}
|
||||
|
||||
period.GenerateIDs()
|
||||
period.RemoveDuplicateTags()
|
||||
period.UserID = user.ID
|
||||
period.Logs = make([]models.PeriodLog, 0)
|
||||
period.Logs = make([]models.Log, 0)
|
||||
period.ShouldReScore = false
|
||||
|
||||
if period.Tags == nil {
|
||||
period.Tags = make([]string, 0)
|
||||
}
|
||||
if period.Goals == nil {
|
||||
period.Goals = make([]models.PeriodGoal, 0)
|
||||
}
|
||||
for i := range period.Goals {
|
||||
if period.Goals[i].SubGoals == nil {
|
||||
period.Goals[i].SubGoals = make([]models.PeriodSubGoal, 0)
|
||||
}
|
||||
period.Goals = make([]models.Goal, 0)
|
||||
}
|
||||
|
||||
err = db.Periods().Insert(c.Request.Context(), period)
|
||||
|
||||
@@ -13,6 +13,7 @@ type Database interface {
|
||||
UserSessions() repositories.UserSessionRepository
|
||||
Activities() repositories.ActivityRepository
|
||||
Periods() repositories.PeriodRepository
|
||||
Items() repositories.ItemRepository
|
||||
}
|
||||
|
||||
// Init gets you database based on the configuration provided.
|
||||
|
||||
@@ -15,6 +15,7 @@ type Database struct {
|
||||
userSessions repositories.UserSessionRepository
|
||||
activities repositories.ActivityRepository
|
||||
periods repositories.PeriodRepository
|
||||
items repositories.ItemRepository
|
||||
}
|
||||
|
||||
func (database *Database) Users() repositories.UserRepository {
|
||||
@@ -33,6 +34,10 @@ func (database *Database) Periods() repositories.PeriodRepository {
|
||||
return database.periods
|
||||
}
|
||||
|
||||
func (database *Database) Items() repositories.ItemRepository {
|
||||
return database.items
|
||||
}
|
||||
|
||||
func Init(cfg config.Database) (*Database, error) {
|
||||
opts := *bbolt.DefaultOptions
|
||||
opts.Timeout = time.Second * 5
|
||||
@@ -57,12 +62,17 @@ func Init(cfg config.Database) (*Database, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := newItemRepository(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
database := &Database{
|
||||
users: users,
|
||||
userSessions: userSessions,
|
||||
periods: periods,
|
||||
activities: activities,
|
||||
items: items,
|
||||
}
|
||||
|
||||
return database, nil
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -64,6 +64,7 @@ func main() {
|
||||
api.User(router.Group("/api/user"), auth)
|
||||
api.Activity(router.Group("/api/activity"), db, auth)
|
||||
api.Period(router.Group("/api/period"), db, scoring, auth)
|
||||
api.Items(router.Group("/api/item"), db, auth)
|
||||
|
||||
// Setup UI
|
||||
if uiRoot != "" {
|
||||
@@ -71,6 +72,9 @@ func main() {
|
||||
router.GET("/activities/", func(c *gin.Context) {
|
||||
http.ServeFile(c.Writer, c.Request, path.Join(uiRoot, "index.html"))
|
||||
})
|
||||
router.GET("/items/", func(c *gin.Context) {
|
||||
http.ServeFile(c.Writer, c.Request, path.Join(uiRoot, "index.html"))
|
||||
})
|
||||
}
|
||||
|
||||
// Start listening
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
+42
-120
@@ -16,9 +16,8 @@ type Period struct {
|
||||
|
||||
ShouldReScore bool `json:"-" msgpack:"-"`
|
||||
|
||||
Tags []string `json:"tags"`
|
||||
Goals []PeriodGoal `json:"goals"`
|
||||
Logs []PeriodLog `json:"logs"`
|
||||
Goals []Goal `json:"goals"`
|
||||
Logs []Log `json:"logs"`
|
||||
}
|
||||
|
||||
// GenerateIDs generates IDs. It should only be used initially.
|
||||
@@ -26,10 +25,6 @@ func (period *Period) GenerateIDs() {
|
||||
period.ID = generate.ID("P", 12)
|
||||
for i := range period.Goals {
|
||||
period.Goals[i].ID = generate.ID("PG", 16)
|
||||
|
||||
for j := range period.Goals[i].SubGoals {
|
||||
period.Goals[i].SubGoals[j].ID = generate.ID("PGS", 20)
|
||||
}
|
||||
}
|
||||
for i := range period.Logs {
|
||||
period.Logs[i].ID = generate.ID("PL", 16)
|
||||
@@ -46,7 +41,6 @@ func (period *Period) Clear() {
|
||||
|
||||
period.ShouldReScore = false
|
||||
|
||||
period.Tags = period.Tags[:0]
|
||||
period.Goals = period.Goals[:0]
|
||||
period.Logs = period.Logs[:0]
|
||||
}
|
||||
@@ -55,11 +49,9 @@ func (period *Period) Clear() {
|
||||
func (period *Period) Copy() *Period {
|
||||
periodCopy := *period
|
||||
|
||||
periodCopy.Tags = make([]string, len(period.Tags))
|
||||
copy(periodCopy.Tags, period.Tags)
|
||||
periodCopy.Goals = make([]PeriodGoal, len(period.Goals))
|
||||
periodCopy.Goals = make([]Goal, len(period.Goals))
|
||||
copy(periodCopy.Goals, period.Goals)
|
||||
periodCopy.Logs = make([]PeriodLog, len(period.Logs))
|
||||
periodCopy.Logs = make([]Log, len(period.Logs))
|
||||
copy(periodCopy.Logs, period.Logs)
|
||||
|
||||
return &periodCopy
|
||||
@@ -87,29 +79,6 @@ func (period *Period) ApplyUpdate(update PeriodUpdate) (changed bool, err error)
|
||||
period.Name = *update.SetName
|
||||
}
|
||||
|
||||
if update.RemoveTag != nil {
|
||||
for i, existingTag := range period.Tags {
|
||||
if *update.AddTag == existingTag {
|
||||
period.Tags = append(period.Tags[:i], period.Tags[i+1:]...)
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if update.AddTag != nil {
|
||||
found := false
|
||||
for _, existingTag := range period.Tags {
|
||||
if *update.AddTag == existingTag {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
changed = true
|
||||
period.Tags = append(period.Tags, *update.AddTag)
|
||||
}
|
||||
}
|
||||
|
||||
if update.AddGoal != nil {
|
||||
goal := *update.AddGoal
|
||||
goal.ID = generate.ID("PG", 16)
|
||||
@@ -125,9 +94,6 @@ func (period *Period) ApplyUpdate(update PeriodUpdate) (changed bool, err error)
|
||||
if update.AddLog != nil && update.AddLog.GoalID == "NEW" {
|
||||
update.AddLog.GoalID = goal.ID
|
||||
}
|
||||
for i := range goal.SubGoals {
|
||||
goal.SubGoals[i].ID = generate.ID("PGS", 20)
|
||||
}
|
||||
|
||||
changed = true
|
||||
}
|
||||
@@ -139,7 +105,6 @@ func (period *Period) ApplyUpdate(update PeriodUpdate) (changed bool, err error)
|
||||
}
|
||||
|
||||
goal.PointCount = update.ReplaceGoal.PointCount
|
||||
goal.SubGoals = update.ReplaceGoal.SubGoals
|
||||
|
||||
changed = true
|
||||
period.ShouldReScore = true
|
||||
@@ -176,6 +141,30 @@ func (period *Period) ApplyUpdate(update PeriodUpdate) (changed bool, err error)
|
||||
period.Logs = append(period.Logs, log)
|
||||
changed = true
|
||||
}
|
||||
if update.ReplaceLog != nil {
|
||||
log := *update.ReplaceLog
|
||||
|
||||
found := false
|
||||
for i, log2 := range period.Logs {
|
||||
if log.ID == log2.ID {
|
||||
found = true
|
||||
|
||||
goal := period.Goal(log.GoalID)
|
||||
if goal == nil {
|
||||
err = slerrors.NotFound("Goal")
|
||||
return
|
||||
}
|
||||
|
||||
period.Logs[i] = log
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
err = slerrors.NotFound("Log")
|
||||
return
|
||||
}
|
||||
}
|
||||
if update.RemoveLog != nil {
|
||||
found := false
|
||||
for i, log := range period.Logs {
|
||||
@@ -197,7 +186,7 @@ func (period *Period) ApplyUpdate(update PeriodUpdate) (changed bool, err error)
|
||||
}
|
||||
|
||||
func (period *Period) RemoveBrokenLogs() {
|
||||
goodLogs := make([]PeriodLog, 0, len(period.Logs))
|
||||
goodLogs := make([]Log, 0, len(period.Logs))
|
||||
|
||||
for _, log := range period.Logs {
|
||||
goal := period.Goal(log.GoalID)
|
||||
@@ -205,34 +194,13 @@ func (period *Period) RemoveBrokenLogs() {
|
||||
continue
|
||||
}
|
||||
|
||||
if log.SubGoalID != "" {
|
||||
if goal.SubGoal(log.SubGoalID) == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
goodLogs = append(goodLogs, log)
|
||||
}
|
||||
|
||||
period.Logs = goodLogs
|
||||
}
|
||||
|
||||
func (period *Period) RemoveDuplicateTags() {
|
||||
deleteList := make([]int, 0, 2)
|
||||
|
||||
found := make(map[string]bool)
|
||||
for i, tag := range period.Tags {
|
||||
if found[tag] {
|
||||
deleteList = append(deleteList, i-len(deleteList))
|
||||
}
|
||||
found[tag] = true
|
||||
}
|
||||
for _, index := range deleteList {
|
||||
period.Tags = append(period.Tags[:index], period.Tags[index+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (period *Period) Goal(id string) *PeriodGoal {
|
||||
func (period *Period) Goal(id string) *Goal {
|
||||
for i, goal := range period.Goals {
|
||||
if goal.ID == id {
|
||||
return &period.Goals[i]
|
||||
@@ -263,79 +231,33 @@ func (period *Period) DayHadActivity(date time.Time, activityID string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// A PeriodGoal is a declared investment into the goal.
|
||||
type PeriodGoal struct {
|
||||
ID string `json:"id"`
|
||||
ActivityID string `json:"activityId"`
|
||||
PointCount int `json:"pointCount"`
|
||||
|
||||
SubGoals []PeriodSubGoal `json:"subGoals"`
|
||||
}
|
||||
|
||||
func (goal *PeriodGoal) SubGoal(id string) *PeriodSubGoal {
|
||||
if goal == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, subGoal := range goal.SubGoals {
|
||||
if subGoal.ID == id {
|
||||
return &goal.SubGoals[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// A PeriodSubGoal is a specific sub-goal that should either punish or boost performance. For example, it could represent
|
||||
// a project or technique that should take priority, or a guilty pleasure that should definitely not be.
|
||||
type PeriodSubGoal struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Multiplier float64 `json:"multiplier"`
|
||||
}
|
||||
|
||||
// PeriodLog 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 PeriodLog 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"`
|
||||
}
|
||||
|
||||
// PeriodUpdate describes a change to a period
|
||||
type PeriodUpdate struct {
|
||||
SetFrom *time.Time `json:"setFrom"`
|
||||
SetTo *time.Time `json:"setTo"`
|
||||
SetName *string `json:"setName"`
|
||||
|
||||
AddLog *PeriodLog `json:"addLog"`
|
||||
RemoveLog *string `json:"removeLog"`
|
||||
AddGoal *PeriodGoal `json:"addGoal"`
|
||||
ReplaceGoal *PeriodGoal `json:"replaceGoal"`
|
||||
RemoveGoal *string `json:"removeGoal"`
|
||||
AddTag *string `json:"addTag"`
|
||||
RemoveTag *string `json:"removeTag"`
|
||||
AddLog *Log `json:"addLog"`
|
||||
ReplaceLog *Log `json:"replaceGoal"`
|
||||
RemoveLog *string `json:"removeLog"`
|
||||
AddGoal *Goal `json:"addGoal"`
|
||||
ReplaceGoal *Goal `json:"replaceGoal"`
|
||||
RemoveGoal *string `json:"removeGoal"`
|
||||
}
|
||||
|
||||
type Score struct {
|
||||
ActivityScore float64 `json:"activityScore"`
|
||||
Amount int `json:"amount"`
|
||||
SubGoalMultiplier *float64 `json:"subGoalMultiplier"`
|
||||
DailyBonus int `json:"dailyBonus"`
|
||||
ActivityScore float64 `json:"activityScore"`
|
||||
Amount int `json:"amount"`
|
||||
ItemMultiplier *float64 `json:"subGoalMultiplier"`
|
||||
DailyBonus int `json:"dailyBonus"`
|
||||
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
func (s *Score) Calc() {
|
||||
subGoalMultiplier := 1.0
|
||||
if s.SubGoalMultiplier != nil {
|
||||
subGoalMultiplier = *s.SubGoalMultiplier
|
||||
if s.ItemMultiplier != nil {
|
||||
subGoalMultiplier = *s.ItemMultiplier
|
||||
}
|
||||
|
||||
s.Total = int64(s.DailyBonus) + int64(s.ActivityScore*float64(s.Amount)*subGoalMultiplier)
|
||||
|
||||
+2
-12
@@ -13,11 +13,11 @@ type ScoringService struct {
|
||||
db database.Database
|
||||
}
|
||||
|
||||
func (s *ScoringService) ScoreOne(ctx context.Context, period models.Period, log models.PeriodLog) (*models.Score, error) {
|
||||
func (s *ScoringService) ScoreOne(ctx context.Context, period models.Period, log models.Log) (*models.Score, error) {
|
||||
return s.scoreOne(ctx, period, log)
|
||||
}
|
||||
|
||||
func (s *ScoringService) scoreOne(ctx context.Context, period models.Period, log models.PeriodLog) (*models.Score, error) {
|
||||
func (s *ScoringService) scoreOne(ctx context.Context, period models.Period, log models.Log) (*models.Score, error) {
|
||||
goal := period.Goal(log.GoalID)
|
||||
if goal == nil {
|
||||
return nil, slerrors.NotFound("Goal")
|
||||
@@ -37,16 +37,6 @@ func (s *ScoringService) scoreOne(ctx context.Context, period models.Period, log
|
||||
Amount: log.Amount,
|
||||
}
|
||||
|
||||
if log.SubGoalID != "" {
|
||||
subGoal := goal.SubGoal(log.SubGoalID)
|
||||
if subGoal == nil {
|
||||
return nil, slerrors.NotFound("Sub-Goal")
|
||||
}
|
||||
|
||||
multiplier := subGoal.Multiplier
|
||||
score.SubGoalMultiplier = &multiplier
|
||||
}
|
||||
|
||||
if activity.DailyBonus > 0 {
|
||||
isFirst := true
|
||||
for _, log2 := range period.Logs {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import LogPage from "./routes/LogPage";
|
||||
import ActivitiesPage from "./routes/ActivitiesPage";
|
||||
import ItemPage from "./routes/ItemPage";
|
||||
|
||||
import LoginModal from "./modals/LoginModal";
|
||||
import CreateActivityModal from "./modals/CreateActivityModal";
|
||||
@@ -31,7 +32,9 @@
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/">Stufflog</MenuItem><MenuItem href="/activities/">Activities</MenuItem>
|
||||
<MenuItem href="/">Stufflog</MenuItem>
|
||||
<MenuItem href="/activities/">Activities</MenuItem>
|
||||
<MenuItem href="/items/">Items</MenuItem>
|
||||
|
||||
<div slot="right">
|
||||
{#if ($auth.checked && $auth.user !== null)}
|
||||
@@ -43,6 +46,7 @@
|
||||
<Router>
|
||||
<Route path="/" component={LogPage} />
|
||||
<Route path="/activities/" component={ActivitiesPage} />
|
||||
<Route path="/items/" component={ItemPage} />
|
||||
</Router>
|
||||
|
||||
<Modal name="login" component={LoginModal} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ky from "ky";
|
||||
import Activity, {ActivityUpdate} from "../models/activity";
|
||||
import Period, {PeriodUpdate} from "../models/period";
|
||||
import Item, {ItemUpdate} from "../models/item";
|
||||
|
||||
function json(data) {
|
||||
return {
|
||||
@@ -33,7 +34,7 @@ export class StuffLogAPI {
|
||||
/**
|
||||
* List activities
|
||||
*
|
||||
* @returns {Activity[]}
|
||||
* @returns {Promise<Activity[]>}
|
||||
*/
|
||||
async listActivities() {
|
||||
const data = await k.get("/api/activity/").json();
|
||||
@@ -42,7 +43,7 @@ export class StuffLogAPI {
|
||||
/**
|
||||
* List periods
|
||||
*
|
||||
* @returns {{periods: Period[], activities: Activity[]}}
|
||||
* @returns {Promise<{>periods: Period[], activities: Activity[]}}
|
||||
*/
|
||||
async listPeriods() {
|
||||
const data = await k.get("/api/period/").json();
|
||||
@@ -53,7 +54,7 @@ export class StuffLogAPI {
|
||||
* Find activity
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Activity}
|
||||
* @returns {Promise<Activity>}
|
||||
*/
|
||||
async findActivity(id) {
|
||||
const data = await k.get(`/api/activity/${id}`).json();
|
||||
@@ -63,7 +64,7 @@ export class StuffLogAPI {
|
||||
* Find period
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Period}
|
||||
* @returns {Promise<Period>}
|
||||
*/
|
||||
async findPeriod(id) {
|
||||
const data = await k.get(`/api/period/${id}`).json();
|
||||
@@ -73,7 +74,7 @@ export class StuffLogAPI {
|
||||
* Create a new activity
|
||||
*
|
||||
* @param {Activity} activity
|
||||
* @returns {Activity}
|
||||
* @returns {Promise<Activity>}
|
||||
*/
|
||||
async postActivity(activity) {
|
||||
const data = await k.post(`/api/activity/`, json(activity)).json();
|
||||
@@ -83,7 +84,7 @@ export class StuffLogAPI {
|
||||
* Create a new period
|
||||
*
|
||||
* @param {Period} period
|
||||
* @returns {Period}
|
||||
* @returns {Promise<Period>}
|
||||
*/
|
||||
async postPeriod(period) {
|
||||
const data = await k.post(`/api/period/`, json(period)).json();
|
||||
@@ -94,7 +95,7 @@ export class StuffLogAPI {
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {ActivityUpdate[]} updates
|
||||
* @returns {Activity}
|
||||
* @returns {Promise<Activity>}
|
||||
*/
|
||||
async patchActivity(id, ...updates) {
|
||||
const data = await k.patch(`/api/activity/${id}`, json(updates)).json();
|
||||
@@ -105,7 +106,7 @@ export class StuffLogAPI {
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {PeriodUpdate[]} updates
|
||||
* @returns {Period}
|
||||
* @returns {Promise<Period>}
|
||||
*/
|
||||
async patchPeriod(id, ...updates) {
|
||||
const data = await k.patch(`/api/period/${id}`, json(updates)).json();
|
||||
@@ -115,23 +116,78 @@ export class StuffLogAPI {
|
||||
* Update an activity
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Activity}
|
||||
* @returns {Promise<Activity>}
|
||||
*/
|
||||
async deleteActivity(id) {
|
||||
const data = await k.delete(`/api/activity/${id}`).json();
|
||||
return new Activity(data.activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an period
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Period}
|
||||
* @returns {Promise<Period>}
|
||||
*/
|
||||
async deletePeriod(id) {
|
||||
const data = await k.delete(`/api/period/${id}`).json();
|
||||
return new Period(data.period);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Promise<Item[]>}
|
||||
*/
|
||||
async listItems() {
|
||||
const data = await k.get(`/api/item/`).json();
|
||||
return data.items
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Promise<Item>}
|
||||
*/
|
||||
async getItem(id) {
|
||||
const data = await k.get(`/api/item/${id}`).json();
|
||||
return data.item
|
||||
}
|
||||
|
||||
/**
|
||||
* Post an item
|
||||
*
|
||||
* @param {Item} item
|
||||
* @returns {Promise<Item>}
|
||||
*/
|
||||
async postItem(item) {
|
||||
const data = await k.post(`/api/item/`, json(item)).json();
|
||||
return data.item
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an item
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {ItemUpdate} updates
|
||||
* @returns {Promise<Item>}
|
||||
*/
|
||||
async updateItem(id, ...updates) {
|
||||
const data = await k.patch(`/api/item/${id}`, json(updates)).json();
|
||||
return data.item
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an item
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Promise<Item>}
|
||||
*/
|
||||
async deleteItem(id) {
|
||||
const data = await k.delete(`/api/item/${id}`).json();
|
||||
return data.item
|
||||
}
|
||||
}
|
||||
|
||||
const slApi = new StuffLogAPI();
|
||||
|
||||
@@ -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>
|
||||
@@ -14,6 +14,8 @@
|
||||
import { faCodeBranch } from "@fortawesome/free-solid-svg-icons/faCodeBranch";
|
||||
import { faGuitar } from "@fortawesome/free-solid-svg-icons/faGuitar";
|
||||
import { faMusic } from "@fortawesome/free-solid-svg-icons/faMusic";
|
||||
import { faArchive } from "@fortawesome/free-solid-svg-icons/faArchive";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck";
|
||||
|
||||
const icons = {
|
||||
"plus": faPlus,
|
||||
@@ -29,6 +31,8 @@
|
||||
"code": faCode,
|
||||
"code_branch": faCodeBranch,
|
||||
"guitar": faGuitar,
|
||||
"archive": faArchive,
|
||||
"check": faCheck,
|
||||
};
|
||||
|
||||
export const iconNames = Object.keys(icons);
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
import Icon from 'fa-svelte'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus'
|
||||
|
||||
export let header = "";
|
||||
export let icon = null;
|
||||
export let top = false;
|
||||
</script>
|
||||
|
||||
<div on:click class="addboi">
|
||||
<div on:click class="addboi" class:top>
|
||||
<Icon class="addboi-icon" icon={faPlus} />
|
||||
<slot></slot>
|
||||
</div>
|
||||
@@ -26,6 +25,11 @@
|
||||
|
||||
transition: 250ms;
|
||||
}
|
||||
div.addboi.top {
|
||||
margin-bottom: 0.5em;
|
||||
padding: 0.25em 0;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
div.addboi:hover {
|
||||
color: #777;
|
||||
|
||||
@@ -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>
|
||||
@@ -16,5 +16,7 @@
|
||||
background-color: #222;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<style>
|
||||
a.menu-item {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
background-color: #222;
|
||||
margin: 0;
|
||||
padding: 0.25em 1ch;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
v = 0;
|
||||
}
|
||||
|
||||
if (i >= red && v === 0) {
|
||||
if (i >= gold && v === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
gold: i >= gold && i < red,
|
||||
red: i >= red,
|
||||
value: v,
|
||||
empty: v === 0,
|
||||
full: v === 1000,
|
||||
})
|
||||
}
|
||||
@@ -64,7 +65,7 @@
|
||||
</script>
|
||||
|
||||
{#each bars as bar}
|
||||
<div class="bar" class:red={bar.red} class:green={bar.green} class:gold={bar.gold} class:full={bar.full}>
|
||||
<div class="bar" class:red={bar.red} class:green={bar.green} class:gold={bar.gold} class:empty={bar.empty} class:full={bar.full}>
|
||||
<div class="content" style={`height: ${bar.value / 10}%; top: ${(1000 - bar.value) / 10}%`}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -11,7 +11,6 @@
|
||||
let error = null;
|
||||
let activityId = "";
|
||||
let pointCount = "1000";
|
||||
let subGoals = [{name:"",multiplier:"1.0"}];
|
||||
|
||||
function addPeriodGoal() {
|
||||
error = null;
|
||||
@@ -26,28 +25,9 @@
|
||||
return
|
||||
}
|
||||
|
||||
const parsedSubGoals = [];
|
||||
for (const subGoal of subGoals) {
|
||||
if (subGoal.name === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedMultiplier = parseFloat(subGoal.multiplier);
|
||||
if (Number.isNaN(parsedMultiplier) || parsedMultiplier < 0) {
|
||||
error = `Sub goal ${subGoal.name} needs a numeric multiplier.`;
|
||||
return
|
||||
}
|
||||
|
||||
parsedSubGoals.push({
|
||||
name: subGoal.name,
|
||||
multiplier: parsedMultiplier,
|
||||
})
|
||||
}
|
||||
|
||||
const addGoal = {
|
||||
activityId: activityId,
|
||||
pointCount: parsedPointCount,
|
||||
subGoals: parsedSubGoals,
|
||||
}
|
||||
|
||||
stufflog.updatePeriod(period.id, {addGoal}).then(() => {
|
||||
@@ -58,24 +38,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function updateSubGoalForm(subGoals) {
|
||||
let last = -1;
|
||||
for (let i = 0; i < subGoals.length; ++i) {
|
||||
if (subGoals[i].name != "") {
|
||||
last = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (last == subGoals.length - 1) {
|
||||
return [...subGoals, {name: "", value: "1.0"}];
|
||||
} else if (last + 2 < subGoals.length && subGoals.length > 1) {
|
||||
return subGoals.slice(0, last + 2);
|
||||
} else {
|
||||
return subGoals;
|
||||
}
|
||||
}
|
||||
|
||||
$: subGoals = updateSubGoalForm(subGoals);
|
||||
$: if (activityId === "" && $stufflog.activities.length > 0) {
|
||||
activityId = $stufflog.activities[0].id;
|
||||
}
|
||||
@@ -91,10 +53,6 @@
|
||||
</select>
|
||||
<label>Points</label>
|
||||
<input class="nolast" type="string" bind:value={pointCount} />
|
||||
<label>Subgoals</label>
|
||||
{#each subGoals as subGoal}
|
||||
<SubGoalInput bind:name={subGoal.name} bind:value={subGoal.multiplier} />
|
||||
{/each}
|
||||
|
||||
<p>
|
||||
The amount of points must be in an increment of 1000, which should be
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
let goal = null;
|
||||
let activity = null;
|
||||
let subActivity = null;
|
||||
let dateInput = dateStr(period.from);
|
||||
let dateInput = dateStr(new Date);
|
||||
let goalId = "";
|
||||
let subGoalId = "";
|
||||
let subActivityId = "";
|
||||
let amount = "200";
|
||||
let description = "";
|
||||
let dateInPeriod = false;
|
||||
|
||||
function addPeriodLog() {
|
||||
error = null;
|
||||
@@ -56,9 +57,8 @@
|
||||
subActivityId = "";
|
||||
}
|
||||
}
|
||||
$: if (goal != null && !goal.subGoals.find(s => s.id === subGoalId)) {
|
||||
subGoalId = "";
|
||||
}
|
||||
|
||||
$: dateInPeriod = Date.parse(dateInput) > Date.parse(period.from) && Date.parse(dateInput) < Date.parse(period.to)
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Add ${period.name} Log`} error={error} closable on:close={() => modal.close()}>
|
||||
@@ -66,6 +66,10 @@
|
||||
<label>Date</label>
|
||||
<input type="date" bind:value={dateInput} />
|
||||
|
||||
{#if (!dateInPeriod)}
|
||||
<p class="warning">Date is outside period!</p>
|
||||
{/if}
|
||||
|
||||
<label>Goal</label>
|
||||
<select bind:value={goalId}>
|
||||
{#each period.goals as goal (goal.id)}
|
||||
@@ -82,17 +86,6 @@
|
||||
</select>
|
||||
{/if}
|
||||
|
||||
{#if (goal != null && goal.subGoals.length > 0)}
|
||||
<label>Sub-Goal</label>
|
||||
<select bind:value={subGoalId}>
|
||||
<option value="">None</option>
|
||||
|
||||
{#each goal.subGoals as subGoal (subGoal.id)}
|
||||
<option value={subGoal.id}>{subGoal.name} ({subGoal.multiplier.toFixed(2)})</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
|
||||
{#if (subActivity != null)}
|
||||
<label>{pluralize(capitalize(subActivity.unitName))}</label>
|
||||
<input type="number" bind:value={amount} />
|
||||
@@ -103,9 +96,14 @@
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Goal</button>
|
||||
<button type="submit">Add Log</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
|
||||
<style>
|
||||
p.warning {
|
||||
color: #FB1;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -14,18 +14,20 @@
|
||||
export let log = {};
|
||||
export let activity = {};
|
||||
export let subActivity = {};
|
||||
export let subGoal = null;
|
||||
export let item = null;
|
||||
|
||||
let activityPoints = 0;
|
||||
let subGoalBonus = 0;
|
||||
let itemBonus = 0;
|
||||
let roundingError = 0;
|
||||
let title = ""
|
||||
|
||||
$: activityPoints = log.score.amount * log.score.activityScore;
|
||||
$: subGoalBonus = subGoal.multiplier ? activityPoints * (subGoal.multiplier - 1) : 0;
|
||||
$: roundingError = (log.score.total) - (Math.floor(activityPoints) + Math.floor(subGoalBonus) + log.score.dailyBonus);
|
||||
$: itemBonus = item ? activityPoints * (item.multiplier - 1) : 0;
|
||||
$: roundingError = (log.score.total) - (Math.floor(activityPoints) + Math.floor(itemBonus) + log.score.dailyBonus);
|
||||
$: title = `${dateStr(log.date)} - ${(activity||{name:"(Unknown)"}).name} ${(subActivity||{name:"(Unknown)"}).name}`
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`${dateStr(log.date)} - ${activity.name} ${subActivity.name}`} closable on:close={() => modal.close()}>
|
||||
<ModalFrame title={title} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => modal.close()}>
|
||||
{#if log.description !== ""}
|
||||
<Property label="Description">
|
||||
@@ -35,12 +37,16 @@
|
||||
|
||||
<Property label="Activity Points">
|
||||
{Math.floor(activityPoints + roundingError)} points
|
||||
({log.score.amount} {pluralize(subActivity.unitName, log.score.amount)})
|
||||
{#if (subActivity != null)}
|
||||
<span>
|
||||
({log.score.amount} {pluralize(subActivity.unitName, log.score.amount)})
|
||||
</span>
|
||||
{/if}
|
||||
</Property>
|
||||
{#if subGoal.multiplier != null}
|
||||
{#if item != null}
|
||||
<Property label="Sub-Goal">
|
||||
{Math.floor(subGoalBonus)} points
|
||||
({subGoal.name})
|
||||
{Math.floor(itemBonus)} points
|
||||
({item.name})
|
||||
</Property>
|
||||
{/if}
|
||||
{#if log.score.dailyBonus != 0}
|
||||
|
||||
@@ -27,7 +27,7 @@ let error = null;
|
||||
<form on:submit|preventDefault={() => removePeriodLog()}>
|
||||
<p>
|
||||
Are you sure you want to remove log <b>{log.description}</b>
|
||||
(<b>{dateStr(log.date)}</b>, <b>{activity.name}</b>) from
|
||||
(<b>{dateStr(log.date)}</b>, <b>{activity ? activity.name : "(Unknown)"}</b>) from
|
||||
period <b>{period.name}</b>?
|
||||
</p>
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
export let activity = {};
|
||||
export let subActivtiy = {};
|
||||
export let subActivity = {};
|
||||
|
||||
let error = null;
|
||||
|
||||
function removeSubActivity() {
|
||||
stufflog.updateActivity(activity.id, {removeSub: subActivtiy.id}).then(() => {
|
||||
stufflog.updateActivity(activity.id, {removeSub: subActivity.id}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
@@ -23,7 +23,7 @@
|
||||
<ModalFrame title={`Remove ${activity.name} Sub-Activity`} error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => removeSubActivity()}>
|
||||
<p>
|
||||
Are you sure you want to remove sub-acitvity <b>{subActivtiy.name || "(unnamed)"}</b> from
|
||||
Are you sure you want to remove sub-acitvity <b>{subActivity.name || "(Unnamed)"}</b> from
|
||||
activity <b>{activity.name}</b>?
|
||||
</p>
|
||||
|
||||
|
||||
Vendored
+12
@@ -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 = {}
|
||||
@@ -10,55 +10,42 @@
|
||||
|
||||
import Boi from "../components/Boi.svelte";
|
||||
import AddBoi from "../components/AddBoi.svelte";
|
||||
import Property from "../components/Property.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import Col from "../components/Col.svelte";
|
||||
import Link from "../components/Link.svelte";
|
||||
|
||||
import Table from "../components/Table.svelte";
|
||||
import SubActivityTable from "../components/tables/SubActivityTable";
|
||||
|
||||
function openModal(modalName, id, subId) {
|
||||
function onActivityOption(modalName, id) {
|
||||
const activity = get(stufflog).activities.find(a => a.id === id);
|
||||
const subActivity = (activity || {subActivities:[]}).subActivities.find(s => s.id === subId);
|
||||
|
||||
modal.open(modalName, {activity, subActivity})
|
||||
modal.open(modalName, {activity})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<AddBoi top on:click={() => modal.open("activity.create")}>Activity</AddBoi>
|
||||
{#each $stufflog.activities as activity (activity.id)}
|
||||
<Boi header={activity.name} icon={activity.icon}>
|
||||
<Row>
|
||||
<Col size=3><Property label="ID" value={activity.id} /></Col>
|
||||
<Col size=3><Property label="Daily Bonus" value={activity.dailyBonus} /></Col>
|
||||
<Col size=6>
|
||||
<Property label="Options">
|
||||
<Link on:click={() => openModal("activity.edit", activity.id)}>Edit Activity</Link>,
|
||||
<Link on:click={() => openModal("subactivity.add", activity.id)}>Add Sub-Activity</Link>,
|
||||
<Link on:click={() => openModal("activity.delete", activity.id)}>Delete Activity</Link>
|
||||
</Property>
|
||||
</Col>
|
||||
</Row>
|
||||
<table>
|
||||
<Table headers={["ID", "Daily Bonus", "Options"]} percentages={[25, 25, 50]}>
|
||||
<tr>
|
||||
<th class="th-name">Sub-Activity</th>
|
||||
<th class="th-value">Value</th>
|
||||
<th class="th-options">Options</th>
|
||||
<td>{activity.id}</td>
|
||||
<td>{activity.dailyBonus}</td>
|
||||
<td>
|
||||
<Link on:click={() => onActivityOption("activity.edit", activity.id)}>Edit Activity</Link>,
|
||||
<Link on:click={() => onActivityOption("subactivity.add", activity.id)}>Add Sub-Activity</Link>,
|
||||
<Link on:click={() => onActivityOption("activity.delete", activity.id)}>Delete Activity</Link>
|
||||
</td>
|
||||
</tr>
|
||||
{#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={() => openModal("subactivity.edit", activity.id, subActivtiy.id)}>Edit</Link>,
|
||||
<Link on:click={() => openModal("subactivity.remove", activity.id, subActivtiy.id)}>Delete</Link>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</Table>
|
||||
|
||||
<SubActivityTable
|
||||
activity={activity}
|
||||
on:option={(e) => modal.open(e.detail.name, e.detail)}
|
||||
/>
|
||||
</Boi>
|
||||
{:else}
|
||||
<div class="empty">No data.</div>
|
||||
{/each}
|
||||
<AddBoi on:click={() => modal.open("activity.create")}>Activity</AddBoi>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -67,25 +54,4 @@
|
||||
max-width: 90%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
th, td {
|
||||
padding: 0em 1ch;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
th.th-name {
|
||||
width: 50%;
|
||||
}
|
||||
th.th-value {
|
||||
width: 25%;
|
||||
}
|
||||
th.th-options {
|
||||
width: 25%;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -1,117 +1,56 @@
|
||||
<script context="module">
|
||||
const UNKNOWN = (Object.freeze||(o => o))({
|
||||
name: "(Unknown)",
|
||||
subGoals: [],
|
||||
subActivities: [],
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { get } from "svelte/store";
|
||||
import pluralize from "pluralize";
|
||||
|
||||
import Boi from "../components/Boi.svelte";
|
||||
import AddBoi from "../components/AddBoi.svelte";
|
||||
import Property from "../components/Property.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import Col from "../components/Col.svelte";
|
||||
import Link from "../components/Link.svelte";
|
||||
import ActivityIcon from "../components/ActivityIcon.svelte";
|
||||
import PointsBar from "../components/PointsBar.svelte";
|
||||
|
||||
import Table from "../components/Table.svelte";
|
||||
import GoalTable from "../components/tables/GoalTable.svelte";
|
||||
import LogTable from "../components/tables/LogTable.svelte";
|
||||
|
||||
import stufflog from "../stores/stufflog";
|
||||
import modal from "../stores/modal";
|
||||
import items from "../stores/items";
|
||||
|
||||
import dateStr from "../utils/dateStr";
|
||||
|
||||
let activityMap = {};
|
||||
let scores = {};
|
||||
|
||||
function openGoalModal(modalName, id, goalId, subGoalId) {
|
||||
const period = get(stufflog).periods.find(a => a.id === id);
|
||||
const goal = period ? period.goals.find(g => g.id === goalId) : null;
|
||||
const subGoal = goal ? goal.subGoals.find(s => s.id === subGoalId) : null;
|
||||
const activity = goal ? get(stufflog).activities.find(a => a.id === goal.activityId) : null
|
||||
|
||||
modal.open(modalName, {period, goal, subGoal, activity})
|
||||
function getPeriod(id) {
|
||||
return get(stufflog).periods.find(a => a.id === id)
|
||||
}
|
||||
|
||||
function openLogModal(modalName, id, logId) {
|
||||
const period = get(stufflog).periods.find(a => a.id === id);
|
||||
const row = period ? period.table.find(r => r.log.id === logId) : {};
|
||||
|
||||
modal.open(modalName, {period, ...row})
|
||||
}
|
||||
|
||||
$: activityMap = $stufflog.activities.reduce((p, v) => ({...p, [v.id]: v}), {});
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<AddBoi top on:click={() => modal.open("period.create")}>Period</AddBoi>
|
||||
{#each $stufflog.periods as period (period.id)}
|
||||
<Boi header={period.name}>
|
||||
<Row>
|
||||
<Col size=3><Property label="From" value={dateStr(period.from)} /></Col>
|
||||
<Col size=3><Property label="To" value={dateStr(period.to)} /></Col>
|
||||
<Col size=6>
|
||||
<Property label="Options">
|
||||
<Link on:click={() => openGoalModal("period.edit", period.id)}>Edit Period</Link>,
|
||||
<Link on:click={() => openGoalModal("periodgoal.add", period.id)}>Add Goal</Link>,
|
||||
<Table headers={["From", "To", "Options"]} percentages={[25, 25, 50]}>
|
||||
<td>{dateStr(period.from)}</td>
|
||||
<td>{dateStr(period.to)}</td>
|
||||
<td>
|
||||
<Link on:click={() => modal.open("period.edit", {period: getPeriod(period.id)})}>Edit Period</Link>,
|
||||
<Link on:click={() => modal.open("periodgoal.add", {period: getPeriod(period.id)})}>Add Goal</Link>,
|
||||
{#if period.goals.length > 0}
|
||||
<Link on:click={() => openGoalModal("periodlog.add", period.id)}>Add Log</Link>,
|
||||
<Link on:click={() => modal.open("periodlog.add", {period: getPeriod(period.id)})}>Add Log</Link>,
|
||||
{/if}
|
||||
<Link on:click={() => openGoalModal("period.delete", period.id)}>Delete Period</Link>
|
||||
</Property>
|
||||
</Col>
|
||||
</Row>
|
||||
<table>
|
||||
<tr>
|
||||
<th class="th-name">Activity</th>
|
||||
<th class="th-points">Points</th>
|
||||
<th class="th-options">Options</th>
|
||||
</tr>
|
||||
{#each period.goals as goal (goal.id)}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="icon"><ActivityIcon name={(activityMap[goal.activityId] || {name: "(Unknown)"}).icon} /></div>
|
||||
<div class="name">{(activityMap[goal.activityId] || {name: "(Unknown)"}).name}</div>
|
||||
</td>
|
||||
<td><PointsBar value={period.scores[goal.id]} goal={goal.pointCount} /></td>
|
||||
<td class="td-options">
|
||||
<Link on:click={() => openGoalModal("periodgoal.remove", period.id, goal.id)}>Delete</Link>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<th class="th-date">Date</th>
|
||||
<th class="th-log">Goal</th>
|
||||
<th class="th-log">Sub-Activity</th>
|
||||
<th class="th-points2">Amount</th>
|
||||
<th class="th-points2">Points</th>
|
||||
<th class="th-options">Options</th>
|
||||
</tr>
|
||||
{#each period.table as row (row.log.id)}
|
||||
<tr>
|
||||
<td>{dateStr(row.log.date)}</td>
|
||||
<td class:dark={row.activity.name.startsWith("(")}>
|
||||
<div class="icon"><ActivityIcon name={row.activity.icon} /></div>
|
||||
<div class="name">{row.activity.name} {row.subActivity.name}</div>
|
||||
</td>
|
||||
<td class:dark={row.subGoal.name.startsWith("(")}>{row.subGoal.name}</td>
|
||||
<td>{row.log.amount} {pluralize(row.subActivity.unitName, row.log.amount)}</td>
|
||||
<td>
|
||||
<Link on:click={() => openLogModal("periodlog.info", period.id, row.log.id)}>{row.log.score.total}</Link>
|
||||
</td>
|
||||
<td class="td-options">
|
||||
<Link on:click={() => openLogModal("periodlog.remove", period.id, row.log.id)}>Delete</Link>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
<Link on:click={() => modal.open("period.delete", {period: getPeriod(period.id)})}>Delete Period</Link>
|
||||
</td>
|
||||
</Table>
|
||||
|
||||
<GoalTable
|
||||
activities={$stufflog.activities}
|
||||
period={period}
|
||||
on:option={(e) => modal.open(e.detail.name, e.detail)}
|
||||
/>
|
||||
<LogTable
|
||||
activities={$stufflog.activities}
|
||||
items={$items}
|
||||
period={period}
|
||||
on:option={(e) => modal.open(e.detail.name, e.detail)}
|
||||
/>
|
||||
</Boi>
|
||||
{/each}
|
||||
<AddBoi on:click={() => modal.open("period.create")}>Period</AddBoi>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -120,49 +59,4 @@
|
||||
max-width: 90%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
padding: 0em 1ch;
|
||||
}
|
||||
table th {
|
||||
text-align: left;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
table th.th-name {
|
||||
width: 25%;
|
||||
}
|
||||
table th.th-date {
|
||||
width: 25%;
|
||||
}
|
||||
table th.th-points {
|
||||
width: 62.5%;
|
||||
}
|
||||
table th.th-options, table td.td-options {
|
||||
width: 12.5%;
|
||||
text-align: right;
|
||||
}
|
||||
table th.th-log {
|
||||
width: calc(37.5%/2);
|
||||
}
|
||||
table th.th-points2 {
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
table td.dark {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
table td div.icon {
|
||||
position: relative;
|
||||
top: 0.175em;
|
||||
display: inline-block;
|
||||
}
|
||||
table td div.name {
|
||||
display: inline-block;
|
||||
}
|
||||
</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();
|
||||
@@ -7,6 +7,7 @@ function createModalStore() {
|
||||
subscribe,
|
||||
|
||||
open(name, data = {}) {
|
||||
console.log("Opening modal", name, data)
|
||||
set({name, data});
|
||||
},
|
||||
|
||||
|
||||
@@ -18,25 +18,6 @@ function calculateTotalScore(period) {
|
||||
)}), {})
|
||||
}
|
||||
|
||||
function generateLogTable(activities, period) {
|
||||
const newTable = [];
|
||||
|
||||
for (const log of period.logs) {
|
||||
const goal = period.goals.find(g => g.id === log.goalId) || UNKNOWN_GOAL;
|
||||
const subGoal = log.subGoalId
|
||||
? goal.subGoals.find(s => s.id === log.subGoalId) || UNKNOWN_SUB_GOAL
|
||||
: NO_SUB_GOAL;
|
||||
const activity = activities.find(a => a.id === goal.activityId) || UNKNOWN_ACTIVITY;
|
||||
const subActivity = activity.subActivities.find(s => s.id === log.subActivityId) || UNKNOWN_SUB_ACTIVITY;
|
||||
|
||||
newTable.push({log, goal, subGoal, activity, subActivity})
|
||||
}
|
||||
|
||||
newTable.sort((a, b) => a.log.date - b.log.date);
|
||||
|
||||
return newTable;
|
||||
}
|
||||
|
||||
function replaceActivity(d, activity) {
|
||||
const data = {
|
||||
...d,
|
||||
@@ -49,7 +30,6 @@ function replaceActivity(d, activity) {
|
||||
data.periods = data.periods.map(p => ({
|
||||
...p,
|
||||
scores: calculateTotalScore(p),
|
||||
table: generateLogTable(data.activities, p),
|
||||
}));
|
||||
|
||||
return data;
|
||||
@@ -63,7 +43,6 @@ function replacePeriod(d, period) {
|
||||
{
|
||||
...period,
|
||||
scores: calculateTotalScore(period),
|
||||
table: generateLogTable(d.activities, period),
|
||||
},
|
||||
].sort((a,b) => b.from - a.from),
|
||||
};
|
||||
@@ -100,7 +79,6 @@ function createStufflogStore() {
|
||||
periods: d.periods.map(p => ({
|
||||
...p,
|
||||
scores: calculateTotalScore(p),
|
||||
table: generateLogTable(activities, p),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
@@ -117,7 +95,6 @@ function createStufflogStore() {
|
||||
periods: periods.sort((a,b) => b.from - a.from).map(p => ({
|
||||
...p,
|
||||
scores: calculateTotalScore(p),
|
||||
table: generateLogTable(d.activities, p),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user