feat: add project workspace service
This commit is contained in:
62
backend/internal/projects/handlers.go
Normal file
62
backend/internal/projects/handlers.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package projects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service *Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service *Service) *Handler {
|
||||||
|
return &Handler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Register(router gin.IRouter) {
|
||||||
|
router.POST("/projects", h.createProject)
|
||||||
|
router.GET("/projects", h.listProjects)
|
||||||
|
router.GET("/projects/:id/dashboard", h.dashboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) createProject(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
project, err := h.service.CreateProject(1, input.Name, input.Description)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, project)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) listProjects(c *gin.Context) {
|
||||||
|
projects, err := h.service.ListProjects(1)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list projects"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, projects)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) dashboard(c *gin.Context) {
|
||||||
|
projectID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dashboard, err := h.service.Dashboard(uint(projectID))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load dashboard"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, dashboard)
|
||||||
|
}
|
||||||
72
backend/internal/projects/service.go
Normal file
72
backend/internal/projects/service.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package projects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"senlinai-agent/backend/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dashboard struct {
|
||||||
|
ProjectID uint `json:"project_id"`
|
||||||
|
PendingInboxCount int64 `json:"pending_inbox_count"`
|
||||||
|
OpenTaskCount int64 `json:"open_task_count"`
|
||||||
|
RecentNoteCount int64 `json:"recent_note_count"`
|
||||||
|
RecentSessionCount int64 `json:"recent_session_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(database *gorm.DB) *Service {
|
||||||
|
return &Service{db: database}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateProject(ownerID uint, name string, description string) (*domain.Project, error) {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New("project name is required")
|
||||||
|
}
|
||||||
|
project := &domain.Project{OwnerID: ownerID, Name: name, Description: description}
|
||||||
|
return project, s.db.Create(project).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListProjects(ownerID uint) ([]domain.Project, error) {
|
||||||
|
var projects []domain.Project
|
||||||
|
err := s.db.Where("owner_id = ?", ownerID).Order("updated_at desc").Find(&projects).Error
|
||||||
|
return projects, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateTag(projectID uint, name string) (*domain.Tag, error) {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New("tag name is required")
|
||||||
|
}
|
||||||
|
tag := &domain.Tag{ProjectID: projectID, Name: name}
|
||||||
|
return tag, s.db.Create(tag).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListTags(projectID uint) ([]domain.Tag, error) {
|
||||||
|
var tags []domain.Tag
|
||||||
|
err := s.db.Where("project_id = ?", projectID).Order("name asc").Find(&tags).Error
|
||||||
|
return tags, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Dashboard(projectID uint) (Dashboard, error) {
|
||||||
|
dashboard := Dashboard{ProjectID: projectID}
|
||||||
|
if err := s.db.Model(&domain.InboxItem{}).Where("project_id = ? AND status = ?", projectID, "open").Count(&dashboard.PendingInboxCount).Error; err != nil {
|
||||||
|
return dashboard, err
|
||||||
|
}
|
||||||
|
if err := s.db.Model(&domain.Task{}).Where("project_id = ? AND status <> ?", projectID, "done").Count(&dashboard.OpenTaskCount).Error; err != nil {
|
||||||
|
return dashboard, err
|
||||||
|
}
|
||||||
|
if err := s.db.Model(&domain.Note{}).Where("project_id = ?", projectID).Count(&dashboard.RecentNoteCount).Error; err != nil {
|
||||||
|
return dashboard, err
|
||||||
|
}
|
||||||
|
if err := s.db.Model(&domain.AISession{}).Where("project_id = ?", projectID).Count(&dashboard.RecentSessionCount).Error; err != nil {
|
||||||
|
return dashboard, err
|
||||||
|
}
|
||||||
|
return dashboard, nil
|
||||||
|
}
|
||||||
57
backend/internal/projects/service_test.go
Normal file
57
backend/internal/projects/service_test.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package projects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"senlinai-agent/backend/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProjectTagsAreScopedToProject(t *testing.T) {
|
||||||
|
database := newTestDB(t)
|
||||||
|
service := NewService(database)
|
||||||
|
first, err := service.CreateProject(1, "Alpha", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
second, err := service.CreateProject(1, "Beta", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = service.CreateTag(first.ID, "重要")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = service.CreateTag(second.ID, "重要")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
firstTags, err := service.ListTags(first.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, firstTags, 1)
|
||||||
|
require.Equal(t, first.ID, firstTags[0].ProjectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashboardCountsOnlyRequestedProject(t *testing.T) {
|
||||||
|
database := newTestDB(t)
|
||||||
|
service := NewService(database)
|
||||||
|
first, err := service.CreateProject(1, "Alpha", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
second, err := service.CreateProject(1, "Beta", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, database.Create(&domain.InboxItem{ProjectID: first.ID, CreatedBy: 1, SourceType: "text", Status: "open"}).Error)
|
||||||
|
require.NoError(t, database.Create(&domain.InboxItem{ProjectID: second.ID, CreatedBy: 1, SourceType: "text", Status: "open"}).Error)
|
||||||
|
require.NoError(t, database.Create(&domain.Task{ProjectID: first.ID, CreatedBy: 1, Title: "A", Status: "open"}).Error)
|
||||||
|
require.NoError(t, database.Create(&domain.Task{ProjectID: second.ID, CreatedBy: 1, Title: "B", Status: "open"}).Error)
|
||||||
|
|
||||||
|
dashboard, err := service.Dashboard(first.ID)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), dashboard.PendingInboxCount)
|
||||||
|
require.Equal(t, int64(1), dashboard.OpenTaskCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, domain.AutoMigrate(database))
|
||||||
|
return database
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user