diff --git a/backend/app/api/handlers/v1/v1_ctrl_statistics.go b/backend/app/api/handlers/v1/v1_ctrl_statistics.go index fdab4fc..34ec3ec 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_statistics.go +++ b/backend/app/api/handlers/v1/v1_ctrl_statistics.go @@ -9,6 +9,46 @@ import ( "github.com/hay-kot/homebox/backend/pkgs/server" ) +// HandleGroupGet godoc +// @Summary Get the current user's group statistics +// @Tags Statistics +// @Produce json +// @Success 200 {object} []repo.TotalsByOrganizer +// @Router /v1/groups/statistics/locations [GET] +// @Security Bearer +func (ctrl *V1Controller) HandleGroupStatisticsLocations() server.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + ctx := services.NewContext(r.Context()) + + stats, err := ctrl.repo.Groups.StatsLocationsByPurchasePrice(ctx, ctx.GID) + if err != nil { + return validate.NewRequestError(err, http.StatusInternalServerError) + } + + return server.Respond(w, http.StatusOK, stats) + } +} + +// HandleGroupGet godoc +// @Summary Get the current user's group statistics +// @Tags Statistics +// @Produce json +// @Success 200 {object} []repo.TotalsByOrganizer +// @Router /v1/groups/statistics/labels [GET] +// @Security Bearer +func (ctrl *V1Controller) HandleGroupStatisticsLabels() server.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + ctx := services.NewContext(r.Context()) + + stats, err := ctrl.repo.Groups.StatsLabelsByPurchasePrice(ctx, ctx.GID) + if err != nil { + return validate.NewRequestError(err, http.StatusInternalServerError) + } + + return server.Respond(w, http.StatusOK, stats) + } +} + // HandleGroupGet godoc // @Summary Get the current user's group statistics // @Tags Statistics @@ -20,7 +60,7 @@ func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { ctx := services.NewContext(r.Context()) - stats, err := ctrl.repo.Groups.GroupStatistics(ctx, ctx.GID) + stats, err := ctrl.repo.Groups.StatsGroup(ctx, ctx.GID) if err != nil { return validate.NewRequestError(err, http.StatusInternalServerError) } @@ -52,7 +92,7 @@ func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() server.HandlerFun endDate = time.Now() } - stats, err := ctrl.repo.Groups.GroupStatisticsPriceOverTime(ctx, ctx.GID, startDate, endDate) + stats, err := ctrl.repo.Groups.StatsPurchasePrice(ctx, ctx.GID, startDate, endDate) if err != nil { return validate.NewRequestError(err, http.StatusInternalServerError) } diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 3bf27ab..1a42aeb 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -80,6 +80,8 @@ func (a *app) mountRoutes(repos *repo.AllRepos) { a.server.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate(), userMW...) a.server.Get(v1Base("/groups/statistics"), v1Ctrl.HandleGroupStatistics(), userMW...) a.server.Get(v1Base("/groups/statistics/purchase-price"), v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...) + a.server.Get(v1Base("/groups/statistics/locations"), v1Ctrl.HandleGroupStatisticsLocations(), userMW...) + a.server.Get(v1Base("/groups/statistics/labels"), v1Ctrl.HandleGroupStatisticsLabels(), userMW...) // TODO: I don't like /groups being the URL for users a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), userMW...) diff --git a/backend/internal/data/repo/repo_group.go b/backend/internal/data/repo/repo_group.go index 25c54b4..5920a22 100644 --- a/backend/internal/data/repo/repo_group.go +++ b/backend/internal/data/repo/repo_group.go @@ -5,11 +5,14 @@ import ( "strings" "time" + "entgo.io/ent/dialect/sql" "github.com/google/uuid" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/groupinvitationtoken" "github.com/hay-kot/homebox/backend/internal/data/ent/item" + "github.com/hay-kot/homebox/backend/internal/data/ent/label" + "github.com/hay-kot/homebox/backend/internal/data/ent/location" ) type GroupRepository struct { @@ -64,6 +67,12 @@ type ( End time.Time `json:"end"` Entries []ValueOverTimeEntry `json:"entries"` } + + TotalsByOrganizer struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Total float64 `json:"total"` + } ) var ( @@ -93,7 +102,60 @@ func mapToGroupInvitation(g *ent.GroupInvitationToken) GroupInvitation { } } -func (r *GroupRepository) GroupStatisticsPriceOverTime(ctx context.Context, GID uuid.UUID, start, end time.Time) (*ValueOverTime, error) { +func (r *GroupRepository) StatsLocationsByPurchasePrice(ctx context.Context, GID uuid.UUID) ([]TotalsByOrganizer, error) { + var v []TotalsByOrganizer + + err := r.db.Location.Query(). + Where( + location.HasGroupWith(group.ID(GID)), + ). + GroupBy(location.FieldID, location.FieldName). + Aggregate(func(sq *sql.Selector) string { + t := sql.Table(item.Table) + sq.Join(t).On(sq.C(location.FieldID), t.C(item.LocationColumn)) + + return sql.As(sql.Sum(t.C(item.FieldPurchasePrice)), "total") + }). + Scan(ctx, &v) + + if err != nil { + return nil, err + } + + return v, err +} + +func (r *GroupRepository) StatsLabelsByPurchasePrice(ctx context.Context, GID uuid.UUID) ([]TotalsByOrganizer, error) { + var v []TotalsByOrganizer + + err := r.db.Debug().Label.Query(). + Where( + label.HasGroupWith(group.ID(GID)), + ). + GroupBy(label.FieldID, label.FieldName). + Aggregate(func(sq *sql.Selector) string { + itemTable := sql.Table(item.Table) + + // item to label is a many to many relation + // so we need to join the junction table + jt := sql.Table(label.ItemsTable) + + // join the junction table to the item table + sq.Join(jt).On(sq.C(label.FieldID), jt.C(label.ItemsPrimaryKey[0])) + sq.Join(itemTable).On(jt.C(label.ItemsPrimaryKey[1]), itemTable.C(item.FieldID)) + + return sql.As(sql.Sum(itemTable.C(item.FieldPurchasePrice)), "total") + }). + Scan(ctx, &v) + + if err != nil { + return nil, err + } + + return v, err +} + +func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, GID uuid.UUID, start, end time.Time) (*ValueOverTime, error) { // Get the Totals for the Start and End of the Given Time Period q := ` SELECT @@ -154,7 +216,7 @@ func (r *GroupRepository) GroupStatisticsPriceOverTime(ctx context.Context, GID return &stats, nil } -func (r *GroupRepository) GroupStatistics(ctx context.Context, GID uuid.UUID) (GroupStatistics, error) { +func (r *GroupRepository) StatsGroup(ctx context.Context, GID uuid.UUID) (GroupStatistics, error) { q := ` SELECT (SELECT COUNT(*) FROM users WHERE group_users = ?) AS total_users, diff --git a/backend/internal/data/repo/repo_group_test.go b/backend/internal/data/repo/repo_group_test.go index b608d16..4321fec 100644 --- a/backend/internal/data/repo/repo_group_test.go +++ b/backend/internal/data/repo/repo_group_test.go @@ -36,7 +36,7 @@ func Test_Group_GroupStatistics(t *testing.T) { useItems(t, 20) useLabels(t, 20) - stats, err := tRepos.Groups.GroupStatistics(context.Background(), tGroup.ID) + stats, err := tRepos.Groups.StatsGroup(context.Background(), tGroup.ID) assert.NoError(t, err) assert.Equal(t, 20, stats.TotalItems)