package projects import ( "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "senlinai-agent/backend/internal/logic/auth" "senlinai-agent/backend/internal/logic/files" ) type Handler struct { service *Service fileService *files.Service } func NewHandler(service *Service, fileServices ...*files.Service) *Handler { var fileService *files.Service if len(fileServices) > 0 { fileService = fileServices[0] } return &Handler{service: service, fileService: fileService} } func (h *Handler) Register(router gin.IRouter) { router.POST("/projects", h.createProject) router.GET("/projects", h.listProjects) router.GET("/projects/:id/dashboard", h.dashboard) router.GET("/projects/:id/workspace", h.workspace) router.POST("/projects/:id/tasks", h.createTask) router.POST("/projects/:id/sources", h.uploadSource) router.POST("/projects/:id/cron-plans", h.createCronPlan) } func (h *Handler) createProject(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } 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(userID, 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) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projects, err := h.service.ListProjects(userID) 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) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } 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(userID, uint(projectID)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load dashboard"}) return } c.JSON(http.StatusOK, dashboard) } func (h *Handler) workspace(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) return } workspace, err := h.service.Workspace(userID, uint(projectID)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load workspace"}) return } c.JSON(http.StatusOK, workspace) } func (h *Handler) createTask(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectID, ok := parseProjectID(c) if !ok { return } var input struct { Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` DueAt string `json:"dueAt"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } dueAt, err := parseOptionalTime(input.DueAt) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid dueAt"}) return } task, err := h.service.CreateTask(userID, projectID, CreateTaskInput{ Title: input.Title, Description: input.Description, Status: input.Status, DueAt: dueAt, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, task) } func (h *Handler) uploadSource(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } if h.fileService == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "file service is not configured"}) return } projectID, ok := parseProjectID(c) if !ok { return } if err := ensureProjectOwner(userID, projectID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } fileHeader, err := c.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) return } file, err := fileHeader.Open() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read file"}) return } defer file.Close() stored, err := h.fileService.Save(projectID, fileHeader.Filename, file) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store file"}) return } title := c.PostForm("title") if strings.TrimSpace(title) == "" { title = stored.OriginalName } source, err := h.service.CreateFileSource(userID, projectID, CreateFileSourceInput{ Title: title, FilePath: stored.RelativePath, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, source) } func (h *Handler) createCronPlan(c *gin.Context) { userID, ok := auth.CurrentUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } projectID, ok := parseProjectID(c) if !ok { return } var input struct { Title string `json:"title"` Schedule string `json:"schedule"` Enabled bool `json:"enabled"` NextRunAt string `json:"nextRunAt"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } nextRunAt, err := parseOptionalTime(input.NextRunAt) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid nextRunAt"}) return } plan, err := h.service.CreateCronPlan(userID, projectID, CreateCronPlanInput{ Title: input.Title, Schedule: input.Schedule, Enabled: input.Enabled, NextRunAt: nextRunAt, }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, plan) } func parseProjectID(c *gin.Context) (uint, bool) { projectID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"}) return 0, false } return uint(projectID), true } func parseOptionalTime(value string) (*time.Time, error) { trimmed := strings.TrimSpace(value) if trimmed == "" { return nil, nil } parsed, err := time.Parse(time.RFC3339, trimmed) if err != nil { return nil, err } return &parsed, nil }