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