package handlers import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/oklog/ulid/v2" "github.com/timetracker/backend/internal/api/utils" dto "github.com/timetracker/backend/internal/dtos" "github.com/timetracker/backend/internal/models" ) // ProjectHandler handles project-related API endpoints type ProjectHandler struct{} // NewProjectHandler creates a new ProjectHandler func NewProjectHandler() *ProjectHandler { return &ProjectHandler{} } // GetProjects handles GET /projects // // @Summary Get all projects // @Description Get a list of all projects // @Tags projects // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} utils.Response{data=[]dto.ProjectDto} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /projects [get] func (h *ProjectHandler) GetProjects(c *gin.Context) { // Get projects from the database projects, err := models.GetAllProjects(c.Request.Context()) if err != nil { utils.InternalErrorResponse(c, "Error retrieving projects: "+err.Error()) return } // Convert to DTOs projectDTOs := make([]dto.ProjectDto, len(projects)) for i, project := range projects { projectDTOs[i] = convertProjectToDTO(&project) } utils.SuccessResponse(c, http.StatusOK, projectDTOs) } // GetProjectsWithCustomers handles GET /projects/with-customers // // @Summary Get all projects with customer information // @Description Get a list of all projects with their associated customer information // @Tags projects // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} utils.Response{data=[]dto.ProjectDto} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /projects/with-customers [get] func (h *ProjectHandler) GetProjectsWithCustomers(c *gin.Context) { // Get projects with customers from the database projects, err := models.GetAllProjectsWithCustomers(c.Request.Context()) if err != nil { utils.InternalErrorResponse(c, "Error retrieving projects: "+err.Error()) return } // Convert to DTOs projectDTOs := make([]dto.ProjectDto, len(projects)) for i, project := range projects { projectDTOs[i] = convertProjectToDTO(&project) } utils.SuccessResponse(c, http.StatusOK, projectDTOs) } // GetProjectByID handles GET /projects/:id // // @Summary Get project by ID // @Description Get a project by its ID // @Tags projects // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Project ID" // @Success 200 {object} utils.Response{data=dto.ProjectDto} // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 404 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /projects/{id} [get] func (h *ProjectHandler) GetProjectByID(c *gin.Context) { // Parse ID from URL idStr := c.Param("id") id, err := ulid.Parse(idStr) if err != nil { utils.BadRequestResponse(c, "Invalid project ID format") return } // Get project from the database project, err := models.GetProjectByID(c.Request.Context(), models.FromULID(id)) if err != nil { utils.InternalErrorResponse(c, "Error retrieving project: "+err.Error()) return } if project == nil { utils.NotFoundResponse(c, "Project not found") return } // Convert to DTO projectDTO := convertProjectToDTO(project) utils.SuccessResponse(c, http.StatusOK, projectDTO) } // GetProjectsByCustomerID handles GET /projects/customer/:customerId // // @Summary Get projects by customer ID // @Description Get a list of projects for a specific customer // @Tags projects // @Accept json // @Produce json // @Security BearerAuth // @Param customerId path string true "Customer ID" // @Success 200 {object} utils.Response{data=[]dto.ProjectDto} // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /projects/customer/{customerId} [get] func (h *ProjectHandler) GetProjectsByCustomerID(c *gin.Context) { // Parse customer ID from URL customerIDStr := c.Param("customerId") customerID, err := ulid.Parse(customerIDStr) if err != nil { utils.BadRequestResponse(c, "Invalid customer ID format") return } // Get projects from the database projects, err := models.GetProjectsByCustomerID(c.Request.Context(), customerID) if err != nil { utils.InternalErrorResponse(c, "Error retrieving projects: "+err.Error()) return } // Convert to DTOs projectDTOs := make([]dto.ProjectDto, len(projects)) for i, project := range projects { projectDTOs[i] = convertProjectToDTO(&project) } utils.SuccessResponse(c, http.StatusOK, projectDTOs) } // CreateProject handles POST /projects // // @Summary Create a new project // @Description Create a new project // @Tags projects // @Accept json // @Produce json // @Security BearerAuth // @Param project body dto.ProjectCreateDto true "Project data" // @Success 201 {object} utils.Response{data=dto.ProjectDto} // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /projects [post] func (h *ProjectHandler) CreateProject(c *gin.Context) { // Parse request body var projectCreateDTO dto.ProjectCreateDto if err := c.ShouldBindJSON(&projectCreateDTO); err != nil { utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) return } // Convert DTO to model projectCreate, err := convertCreateProjectDTOToModel(projectCreateDTO) if err != nil { utils.BadRequestResponse(c, err.Error()) return } // Create project in the database project, err := models.CreateProject(c.Request.Context(), projectCreate) if err != nil { utils.InternalErrorResponse(c, "Error creating project: "+err.Error()) return } // Convert to DTO projectDTO := convertProjectToDTO(project) utils.SuccessResponse(c, http.StatusCreated, projectDTO) } // UpdateProject handles PUT /projects/:id // // @Summary Update a project // @Description Update an existing project // @Tags projects // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Project ID" // @Param project body dto.ProjectUpdateDto true "Project data" // @Success 200 {object} utils.Response{data=dto.ProjectDto} // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 404 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /projects/{id} [put] func (h *ProjectHandler) UpdateProject(c *gin.Context) { // Parse ID from URL idStr := c.Param("id") id, err := ulid.Parse(idStr) if err != nil { utils.BadRequestResponse(c, "Invalid project ID format") return } // Parse request body var projectUpdateDTO dto.ProjectUpdateDto if err := c.ShouldBindJSON(&projectUpdateDTO); err != nil { utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) return } // Set ID from URL projectUpdateDTO.ID = id.String() // Convert DTO to model projectUpdate, err := convertUpdateProjectDTOToModel(projectUpdateDTO) if err != nil { utils.BadRequestResponse(c, err.Error()) return } // Update project in the database project, err := models.UpdateProject(c.Request.Context(), projectUpdate) if err != nil { utils.InternalErrorResponse(c, "Error updating project: "+err.Error()) return } if project == nil { utils.NotFoundResponse(c, "Project not found") return } // Convert to DTO projectDTO := convertProjectToDTO(project) utils.SuccessResponse(c, http.StatusOK, projectDTO) } // DeleteProject handles DELETE /projects/:id // // @Summary Delete a project // @Description Delete a project by its ID // @Tags projects // @Accept json // @Produce json // @Security BearerAuth // @Param id path string true "Project ID" // @Success 204 {object} utils.Response // @Failure 400 {object} utils.Response{error=utils.ErrorInfo} // @Failure 401 {object} utils.Response{error=utils.ErrorInfo} // @Failure 500 {object} utils.Response{error=utils.ErrorInfo} // @Router /projects/{id} [delete] func (h *ProjectHandler) DeleteProject(c *gin.Context) { // Parse ID from URL idStr := c.Param("id") id, err := ulid.Parse(idStr) if err != nil { utils.BadRequestResponse(c, "Invalid project ID format") return } // Delete project from the database err = models.DeleteProject(c.Request.Context(), id) if err != nil { utils.InternalErrorResponse(c, "Error deleting project: "+err.Error()) return } utils.SuccessResponse(c, http.StatusNoContent, nil) } // Helper functions for DTO conversion func convertProjectToDTO(project *models.Project) dto.ProjectDto { customerID := 0 if project.CustomerID.Compare(models.ULIDWrapper{}) != 0 { // This is a simplification, adjust as needed customerID = int(project.CustomerID.Time()) } return dto.ProjectDto{ ID: project.ID.String(), CreatedAt: project.CreatedAt, UpdatedAt: project.UpdatedAt, Name: project.Name, CustomerID: customerID, } } func convertCreateProjectDTOToModel(dto dto.ProjectCreateDto) (models.ProjectCreate, error) { // Convert CustomerID from int to ULID (this is a simplification, adjust as needed) customerID, err := customerIDToULID(dto.CustomerID) if err != nil { return models.ProjectCreate{}, fmt.Errorf("invalid customer ID: %w", err) } return models.ProjectCreate{ Name: dto.Name, CustomerID: models.FromULID(customerID), }, nil } func convertUpdateProjectDTOToModel(dto dto.ProjectUpdateDto) (models.ProjectUpdate, error) { id, _ := ulid.Parse(dto.ID) update := models.ProjectUpdate{ ID: models.FromULID(id), } if dto.Name != nil { update.Name = dto.Name } if dto.CustomerID != nil { // Convert CustomerID from int to ULID (this is a simplification, adjust as needed) customerID, err := customerIDToULID(*dto.CustomerID) if err != nil { return models.ProjectUpdate{}, fmt.Errorf("invalid customer ID: %w", err) } wrappedID := models.FromULID(customerID) update.CustomerID = &wrappedID } return update, nil } // Helper function to convert customer ID from int to ULID func customerIDToULID(id int) (ulid.ULID, error) { // This is a simplification, in a real application you would need to // fetch the actual ULID from the database or use a proper conversion method // For now, we'll create a deterministic ULID based on the int value entropy := ulid.Monotonic(nil, 0) timestamp := uint64(id) // Create a new ULID with the timestamp and entropy return ulid.MustNew(timestamp, entropy), nil }