add project groups to backend.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/gissleh/stufflog/internal/slerrors"
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
"github.com/gissleh/stufflog/models"
|
"github.com/gissleh/stufflog/models"
|
||||||
"github.com/gissleh/stufflog/services"
|
"github.com/gissleh/stufflog/services"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,6 +31,12 @@ func Project(g *gin.RouterGroup, db database.Database) {
|
|||||||
favorite := setting == "true"
|
favorite := setting == "true"
|
||||||
filter.Favorite = &favorite
|
filter.Favorite = &favorite
|
||||||
}
|
}
|
||||||
|
if setting := c.Query("ungrouped"); setting != "" {
|
||||||
|
filter.Ungrouped = setting == "true"
|
||||||
|
}
|
||||||
|
if setting := c.Query("groups"); setting != "" {
|
||||||
|
filter.ProjectGroupIDs = strings.Split(setting, ",")
|
||||||
|
}
|
||||||
|
|
||||||
return l.ListProjects(c, filter)
|
return l.ListProjects(c, filter)
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gissleh/stufflog/database"
|
||||||
|
"github.com/gissleh/stufflog/internal/auth"
|
||||||
|
"github.com/gissleh/stufflog/internal/generate"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/gissleh/stufflog/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProjectGroup(g *gin.RouterGroup, db database.Database) {
|
||||||
|
l := services.Loader{DB: db}
|
||||||
|
|
||||||
|
g.GET("/", handler("projectGroups", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.ListProjectGroups(c)
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.GET("/:id", handler("projectGroup", func(c *gin.Context) (interface{}, error) {
|
||||||
|
return l.FindProject(c, c.Param("id"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.POST("/", handler("projectGroup", func(c *gin.Context) (interface{}, error) {
|
||||||
|
group := models.ProjectGroup{}
|
||||||
|
err := c.BindJSON(&group)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
if group.Abbreviation == "" || group.Name == "" {
|
||||||
|
return nil, slerrors.BadRequest("Abbreviation and name cannot be left empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
group.ID = generate.ProjectGroupID()
|
||||||
|
group.UserID = auth.UserID(c)
|
||||||
|
if group.CategoryNames == nil {
|
||||||
|
group.CategoryNames = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.ProjectGroups().Insert(c.Request.Context(), group)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &group, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.PUT("/:id", handler("projectGroup", func(c *gin.Context) (interface{}, error) {
|
||||||
|
update := models.ProjectGroupUpdate{}
|
||||||
|
err := c.BindJSON(&update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, slerrors.BadRequest("Invalid JSON")
|
||||||
|
}
|
||||||
|
if (update.Name != nil && *update.Name == "") || (update.Abbreviation != nil && *update.Abbreviation == "") {
|
||||||
|
return nil, slerrors.BadRequest("Abbreviation and name cannot be left empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
group, err := db.ProjectGroups().Find(c, c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if group.UserID != auth.UserID(c) {
|
||||||
|
return nil, slerrors.NotFound("Project group")
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Update(update)
|
||||||
|
|
||||||
|
err = db.ProjectGroups().Update(c.Request.Context(), *group)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
g.DELETE("/:id", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||||
|
group, err := l.FindProjectGroup(c.Request.Context(), c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(group.Projects) > 0 {
|
||||||
|
return nil, slerrors.Forbidden("You cannot delete non-empty project group.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.ProjectGroups().Delete(c.Request.Context(), group.ProjectGroup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ func main() {
|
|||||||
api.Group(server.Group("/api/group"), db)
|
api.Group(server.Group("/api/group"), db)
|
||||||
api.Item(server.Group("/api/item"), db)
|
api.Item(server.Group("/api/item"), db)
|
||||||
api.Project(server.Group("/api/project"), db)
|
api.Project(server.Group("/api/project"), db)
|
||||||
|
api.ProjectGroup(server.Group("/api/projectgroup"), db)
|
||||||
api.Task(server.Group("/api/task"), db)
|
api.Task(server.Group("/api/task"), db)
|
||||||
api.Log(server.Group("/api/log"), db)
|
api.Log(server.Group("/api/log"), db)
|
||||||
api.Goal(server.Group("/api/goal"), db)
|
api.Goal(server.Group("/api/goal"), db)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ func main() {
|
|||||||
api.Group(server.Group("/api/group"), db)
|
api.Group(server.Group("/api/group"), db)
|
||||||
api.Item(server.Group("/api/item"), db)
|
api.Item(server.Group("/api/item"), db)
|
||||||
api.Project(server.Group("/api/project"), db)
|
api.Project(server.Group("/api/project"), db)
|
||||||
|
api.ProjectGroup(server.Group("/api/projectgroup"), db)
|
||||||
api.Task(server.Group("/api/task"), db)
|
api.Task(server.Group("/api/task"), db)
|
||||||
api.Log(server.Group("/api/log"), db)
|
api.Log(server.Group("/api/log"), db)
|
||||||
api.Goal(server.Group("/api/goal"), db)
|
api.Goal(server.Group("/api/goal"), db)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Database interface {
|
|||||||
Logs() models.LogRepository
|
Logs() models.LogRepository
|
||||||
Projects() models.ProjectRepository
|
Projects() models.ProjectRepository
|
||||||
Tasks() models.TaskRepository
|
Tasks() models.TaskRepository
|
||||||
|
ProjectGroups() models.ProjectGroupRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(ctx context.Context, driver string, connect string) (Database, error) {
|
func Open(ctx context.Context, driver string, connect string) (Database, error) {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ func (d *Database) Tasks() models.TaskRepository {
|
|||||||
return &taskRepository{db: d.db}
|
return &taskRepository{db: d.db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Database) ProjectGroups() models.ProjectGroupRepository {
|
||||||
|
return &projectGroupRepository{db: d.db}
|
||||||
|
}
|
||||||
|
|
||||||
func Setup(ctx context.Context, connect string) (*Database, error) {
|
func Setup(ctx context.Context, connect string) (*Database, error) {
|
||||||
db, err := sqlx.ConnectContext(ctx, "postgres", connect)
|
db, err := sqlx.ConnectContext(ctx, "postgres", connect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilte
|
|||||||
if filter.Favorite != nil {
|
if filter.Favorite != nil {
|
||||||
sq = sq.Where(squirrel.Eq{"favorite": *filter.Favorite})
|
sq = sq.Where(squirrel.Eq{"favorite": *filter.Favorite})
|
||||||
}
|
}
|
||||||
|
if filter.Ungrouped == true {
|
||||||
|
sq = sq.Where("project_group_id is NULL")
|
||||||
|
} else if filter.ProjectGroupIDs != nil {
|
||||||
|
sq = sq.Where(squirrel.Eq{"project_group_id": filter.ProjectGroupIDs})
|
||||||
|
}
|
||||||
|
|
||||||
sq = sq.OrderBy("active", "created_time DESC")
|
sq = sq.OrderBy("active", "created_time DESC")
|
||||||
|
|
||||||
@@ -86,9 +91,9 @@ func (r *projectRepository) Insert(ctx context.Context, project models.Project)
|
|||||||
|
|
||||||
_, err := r.db.NamedExecContext(ctx, `
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
INSERT INTO project(
|
INSERT INTO project(
|
||||||
project_id, user_id, name, description, icon, active, created_time, start_time, end_time, subtract_amount, status_tag, favorite, tags
|
project_id, user_id, project_group_id, name, description, icon, active, created_time, start_time, end_time, subtract_amount, status_tag, favorite, tags
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :start_time, :end_time, :subtract_amount, :status_tag, :favorite, :tags
|
:project_id, :user_id, :project_group_id, :name, :description, :icon, :active, :created_time, :start_time, :end_time, :subtract_amount, :status_tag, :favorite, :tags
|
||||||
)
|
)
|
||||||
`, toProjectDBO(project))
|
`, toProjectDBO(project))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -107,6 +112,7 @@ func (r *projectRepository) Update(ctx context.Context, project models.Project)
|
|||||||
UPDATE project SET
|
UPDATE project SET
|
||||||
name = :name,
|
name = :name,
|
||||||
description = :description,
|
description = :description,
|
||||||
|
project_group_id = :project_group_id,
|
||||||
icon = :icon,
|
icon = :icon,
|
||||||
active = :active,
|
active = :active,
|
||||||
start_time = :start_time,
|
start_time = :start_time,
|
||||||
@@ -144,6 +150,7 @@ func (r *projectRepository) Delete(ctx context.Context, project models.Project)
|
|||||||
type projectDBO struct {
|
type projectDBO struct {
|
||||||
ID string `json:"id" db:"project_id"`
|
ID string `json:"id" db:"project_id"`
|
||||||
UserID string `json:"-" db:"user_id"`
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
ProjectGroupID *string `json:"projectGroupID" db:"project_group_id"`
|
||||||
Name string `json:"name" db:"name"`
|
Name string `json:"name" db:"name"`
|
||||||
Description string `json:"description" db:"description"`
|
Description string `json:"description" db:"description"`
|
||||||
Icon string `json:"icon" db:"icon"`
|
Icon string `json:"icon" db:"icon"`
|
||||||
@@ -161,6 +168,7 @@ func toProjectDBO(project models.Project) *projectDBO {
|
|||||||
return &projectDBO{
|
return &projectDBO{
|
||||||
ID: project.ID,
|
ID: project.ID,
|
||||||
UserID: project.UserID,
|
UserID: project.UserID,
|
||||||
|
ProjectGroupID: project.GroupID,
|
||||||
Name: project.Name,
|
Name: project.Name,
|
||||||
Description: project.Description,
|
Description: project.Description,
|
||||||
Icon: project.Icon,
|
Icon: project.Icon,
|
||||||
@@ -179,6 +187,7 @@ func (d *projectDBO) ToProject() *models.Project {
|
|||||||
return &models.Project{
|
return &models.Project{
|
||||||
ID: d.ID,
|
ID: d.ID,
|
||||||
UserID: d.UserID,
|
UserID: d.UserID,
|
||||||
|
GroupID: d.ProjectGroupID,
|
||||||
Name: d.Name,
|
Name: d.Name,
|
||||||
Description: d.Description,
|
Description: d.Description,
|
||||||
Icon: d.Icon,
|
Icon: d.Icon,
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/gissleh/stufflog/internal/slerrors"
|
||||||
|
"github.com/gissleh/stufflog/models"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type projectGroupRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectGroupRepository) Find(ctx context.Context, id string) (*models.ProjectGroup, error) {
|
||||||
|
res := projectGroupDBO{}
|
||||||
|
err := r.db.GetContext(ctx, &res, "SELECT * FROM project_group WHERE project_group_id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, slerrors.NotFound("Project group")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.ToProjectGroup(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectGroupRepository) List(ctx context.Context, filter models.ProjectGroupFilter) ([]*models.ProjectGroup, error) {
|
||||||
|
res := make([]*projectGroupDBO, 0, 16)
|
||||||
|
err := r.db.SelectContext(ctx, &res, "SELECT * FROM project_group WHERE user_id=$1", filter.UserID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return []*models.ProjectGroup{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res2 := make([]*models.ProjectGroup, 0, len(res))
|
||||||
|
for _, pdo := range res {
|
||||||
|
res2 = append(res2, pdo.ToProjectGroup())
|
||||||
|
}
|
||||||
|
|
||||||
|
return res2, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectGroupRepository) Insert(ctx context.Context, group models.ProjectGroup) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
INSERT INTO project_group (
|
||||||
|
project_group_id, user_id, name, abbreviation, description, category_names
|
||||||
|
) VALUES (
|
||||||
|
:project_group_id, :user_id, :name, :abbreviation, :description, :category_names
|
||||||
|
)
|
||||||
|
`, toProjectGroupDBO(group))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectGroupRepository) Update(ctx context.Context, group models.ProjectGroup) error {
|
||||||
|
if group.CategoryNames == nil {
|
||||||
|
group.CategoryNames = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
UPDATE project_group SET
|
||||||
|
name = :name,
|
||||||
|
abbreviation = :abbreviation,
|
||||||
|
description = :description,
|
||||||
|
category_names = :category_names
|
||||||
|
WHERE project_group_id = :project_group_id
|
||||||
|
`, toProjectGroupDBO(group))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectGroupRepository) Delete(ctx context.Context, group models.ProjectGroup) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `DELETE FROM project_group WHERE project_group_id=$1`, group.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.db.ExecContext(ctx, "UPDATE project SET project_group_id=NULL where project_group_id=$1", group.ID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type projectGroupDBO struct {
|
||||||
|
ID string `db:"project_group_id"`
|
||||||
|
UserID string `db:"user_id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Description string `db:"description"`
|
||||||
|
Abbreviation string `db:"abbreviation"`
|
||||||
|
CategoryNames jsonMap `db:"category_names"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbo *projectGroupDBO) ToProjectGroup() *models.ProjectGroup {
|
||||||
|
return &models.ProjectGroup{
|
||||||
|
ID: dbo.ID,
|
||||||
|
UserID: dbo.UserID,
|
||||||
|
Name: dbo.Name,
|
||||||
|
Description: dbo.Description,
|
||||||
|
Abbreviation: dbo.Abbreviation,
|
||||||
|
CategoryNames: dbo.CategoryNames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProjectGroupDBO(group models.ProjectGroup) projectGroupDBO {
|
||||||
|
return projectGroupDBO{
|
||||||
|
ID: group.ID,
|
||||||
|
UserID: group.UserID,
|
||||||
|
Name: group.Name,
|
||||||
|
Description: group.Description,
|
||||||
|
Abbreviation: group.Abbreviation,
|
||||||
|
CategoryNames: group.CategoryNames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonMap map[string]string
|
||||||
|
|
||||||
|
func (m jsonMap) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *jsonMap) Scan(value interface{}) error {
|
||||||
|
b, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("type assertion to []byte failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(b, m)
|
||||||
|
}
|
||||||
@@ -39,6 +39,10 @@ func ProjectID() string {
|
|||||||
return id("P", 16)
|
return id("P", 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ProjectGroupID() string {
|
||||||
|
return id("C", 16)
|
||||||
|
}
|
||||||
|
|
||||||
func TaskID() string {
|
func TaskID() string {
|
||||||
return id("T", 16)
|
return id("T", 16)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE project_group (
|
||||||
|
project_group_id CHAR(16) NOT NULL PRIMARY KEY,
|
||||||
|
user_id CHAR(36) NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
abbreviation TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
category_names JSONB NOT NULL
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS project_group;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE project
|
||||||
|
ADD COLUMN project_group_id CHAR(16) DEFAULT NULL;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE project
|
||||||
|
DROP COLUMN IF EXISTS project_group_id;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
type Project struct {
|
type Project struct {
|
||||||
ID string `json:"id" db:"project_id"`
|
ID string `json:"id" db:"project_id"`
|
||||||
UserID string `json:"-" db:"user_id"`
|
UserID string `json:"-" db:"user_id"`
|
||||||
|
GroupID *string `json:"groupId" db:"project_group_id"`
|
||||||
Name string `json:"name" db:"name"`
|
Name string `json:"name" db:"name"`
|
||||||
Description string `json:"description" db:"description"`
|
Description string `json:"description" db:"description"`
|
||||||
Icon string `json:"icon" db:"icon"`
|
Icon string `json:"icon" db:"icon"`
|
||||||
@@ -22,6 +23,12 @@ type Project struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (project *Project) Update(update ProjectUpdate) {
|
func (project *Project) Update(update ProjectUpdate) {
|
||||||
|
if update.GroupID != nil {
|
||||||
|
project.GroupID = update.GroupID
|
||||||
|
if *project.GroupID == "" {
|
||||||
|
project.GroupID = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
if update.Name != nil {
|
if update.Name != nil {
|
||||||
project.Name = *update.Name
|
project.Name = *update.Name
|
||||||
}
|
}
|
||||||
@@ -73,6 +80,7 @@ func (project *Project) Update(update ProjectUpdate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProjectUpdate struct {
|
type ProjectUpdate struct {
|
||||||
|
GroupID *string `json:"groupId"`
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Icon *string `json:"icon"`
|
Icon *string `json:"icon"`
|
||||||
@@ -95,11 +103,13 @@ type ProjectResult struct {
|
|||||||
|
|
||||||
type ProjectFilter struct {
|
type ProjectFilter struct {
|
||||||
UserID string
|
UserID string
|
||||||
|
ProjectGroupIDs []string
|
||||||
Active *bool
|
Active *bool
|
||||||
Favorite *bool
|
Favorite *bool
|
||||||
Expiring bool
|
Expiring bool
|
||||||
IncludeSemiActive bool
|
IncludeSemiActive bool
|
||||||
IDs []string
|
IDs []string
|
||||||
|
Ungrouped bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectRepository interface {
|
type ProjectRepository interface {
|
||||||
|
|||||||
+60
-5
@@ -3,24 +3,79 @@ package models
|
|||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
type ProjectGroup struct {
|
type ProjectGroup struct {
|
||||||
ID string `json:"id" db:"id"`
|
ID string `json:"id" db:"project_group_id"`
|
||||||
UserID string `json:"-" db:"user_id"`
|
UserID string `json:"-" db:"user_id"`
|
||||||
Name string `json:"name" db:"name"`
|
Name string `json:"name" db:"name"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
Abbreviation string `json:"abbreviation" db:"abbreviation"`
|
Abbreviation string `json:"abbreviation" db:"abbreviation"`
|
||||||
CategoryNames map[string]string `json:"categoryNames" db:"category_names"`
|
CategoryNames map[string]string `json:"categoryNames" db:"category_names"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (group *ProjectGroup) Update(update ProjectGroupUpdate) {
|
||||||
|
for key, value := range update.SetCategoryNames {
|
||||||
|
if value == "" {
|
||||||
|
delete(group.CategoryNames, key)
|
||||||
|
} else {
|
||||||
|
group.CategoryNames[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.Name != nil && *update.Name != "" {
|
||||||
|
group.Name = *update.Name
|
||||||
|
}
|
||||||
|
if update.Abbreviation != nil && *update.Abbreviation != "" {
|
||||||
|
group.Abbreviation = *update.Abbreviation
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
group.Description = *update.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ProjectGroupUpdate struct {
|
type ProjectGroupUpdate struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Abbreviation *string `json:"abbreviation"`
|
Abbreviation *string `json:"abbreviation"`
|
||||||
SetCategoryNames map[string]string `json:"setCategoryNames"`
|
Description *string `json:"description"`
|
||||||
ResetCategoryName *string `json:"resetCategoryName"`
|
SetCategoryNames map[string]string `json:"setCategoryNames"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectGroupFilter struct {
|
type ProjectGroupFilter struct {
|
||||||
UserID string `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectGroupResult struct {
|
||||||
|
ProjectGroup
|
||||||
|
|
||||||
|
Projects []*ProjectResult `json:"projects"`
|
||||||
|
ProjectCounts map[string]int `json:"projectCounts"`
|
||||||
|
TaskCounts map[string]int `json:"taskCounts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProjectGroupResult) RecountTasks() {
|
||||||
|
r.ProjectCounts = make(map[string]int)
|
||||||
|
r.TaskCounts = make(map[string]int)
|
||||||
|
|
||||||
|
r.ProjectCounts["total"] = 0
|
||||||
|
r.TaskCounts["total"] = 0
|
||||||
|
|
||||||
|
for _, project := range r.Projects {
|
||||||
|
r.ProjectCounts["total"] += 1
|
||||||
|
if project.StatusTag == nil {
|
||||||
|
r.ProjectCounts["active"] += 1
|
||||||
|
} else {
|
||||||
|
r.ProjectCounts[*project.StatusTag] += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, task := range project.Tasks {
|
||||||
|
r.TaskCounts["total"] += 1
|
||||||
|
if task.StatusTag == nil {
|
||||||
|
r.TaskCounts["active"] += 1
|
||||||
|
} else {
|
||||||
|
r.TaskCounts[*task.StatusTag] += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ProjectGroupRepository interface {
|
type ProjectGroupRepository interface {
|
||||||
Find(ctx context.Context, id string) (*ProjectGroup, error)
|
Find(ctx context.Context, id string) (*ProjectGroup, error)
|
||||||
List(ctx context.Context, filter ProjectGroupFilter) ([]*ProjectGroup, error)
|
List(ctx context.Context, filter ProjectGroupFilter) ([]*ProjectGroup, error)
|
||||||
|
|||||||
@@ -373,6 +373,89 @@ func (l *Loader) ListProjects(ctx context.Context, filter models.ProjectFilter)
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Loader) FindProjectGroup(ctx context.Context, id string) (*models.ProjectGroupResult, error) {
|
||||||
|
group, err := l.DB.ProjectGroups().Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projects, err := l.ListProjects(ctx, models.ProjectFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
ProjectGroupIDs: []string{group.ID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &models.ProjectGroupResult{
|
||||||
|
ProjectGroup: *group,
|
||||||
|
Projects: projects,
|
||||||
|
}
|
||||||
|
result.RecountTasks()
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loader) ListProjectGroups(ctx context.Context) ([]*models.ProjectGroupResult, error) {
|
||||||
|
groups, err := l.DB.ProjectGroups().List(ctx, models.ProjectGroupFilter{UserID: auth.UserID(ctx)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]string, 0, len(groups))
|
||||||
|
for _, group := range groups {
|
||||||
|
ids = append(ids, group.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
projects, err := l.ListProjects(ctx, models.ProjectFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
ProjectGroupIDs: ids,
|
||||||
|
})
|
||||||
|
|
||||||
|
results := make([]*models.ProjectGroupResult, 0, len(groups)+1)
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
matchingProjects := make([]*models.ProjectResult, 0, len(projects)/len(groups))
|
||||||
|
for _, project := range projects {
|
||||||
|
if *project.GroupID == group.ID {
|
||||||
|
matchingProjects = append(matchingProjects, project)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &models.ProjectGroupResult{
|
||||||
|
ProjectGroup: *group,
|
||||||
|
Projects: matchingProjects,
|
||||||
|
}
|
||||||
|
result.RecountTasks()
|
||||||
|
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
ungroupedProjects, err := l.ListProjects(ctx, models.ProjectFilter{
|
||||||
|
UserID: auth.UserID(ctx),
|
||||||
|
Ungrouped: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(ungroupedProjects) > 0 {
|
||||||
|
result := &models.ProjectGroupResult{
|
||||||
|
ProjectGroup: models.ProjectGroup{
|
||||||
|
ID: "META_UNGROUPED",
|
||||||
|
Name: "Ungrouped",
|
||||||
|
Abbreviation: "OTHER",
|
||||||
|
CategoryNames: map[string]string{},
|
||||||
|
},
|
||||||
|
Projects: ungroupedProjects,
|
||||||
|
}
|
||||||
|
result.RecountTasks()
|
||||||
|
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Loader) FindTask(ctx context.Context, id string) (*models.TaskResult, error) {
|
func (l *Loader) FindTask(ctx context.Context, id string) (*models.TaskResult, error) {
|
||||||
task, err := l.DB.Tasks().Find(ctx, id)
|
task, err := l.DB.Tasks().Find(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user