498 lines
15 KiB
Go
498 lines
15 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/oklog/ulid/v2"
|
|
"github.com/timetracker/backend/internal/api/middleware"
|
|
"github.com/timetracker/backend/internal/api/utils"
|
|
dto "github.com/timetracker/backend/internal/dtos"
|
|
"github.com/timetracker/backend/internal/models"
|
|
)
|
|
|
|
// TimeEntryHandler handles time entry-related API endpoints
|
|
type TimeEntryHandler struct{}
|
|
|
|
// NewTimeEntryHandler creates a new TimeEntryHandler
|
|
func NewTimeEntryHandler() *TimeEntryHandler {
|
|
return &TimeEntryHandler{}
|
|
}
|
|
|
|
// GetTimeEntries handles GET /time-entries
|
|
//
|
|
// @Summary Get all time entries
|
|
// @Description Get a list of all time entries
|
|
// @Tags time-entries
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
|
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
|
// @Router /time-entries [get]
|
|
func (h *TimeEntryHandler) GetTimeEntries(c *gin.Context) {
|
|
// Get time entries from the database
|
|
timeEntries, err := models.GetAllTimeEntries(c.Request.Context())
|
|
if err != nil {
|
|
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert to DTOs
|
|
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
for i, timeEntry := range timeEntries {
|
|
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
}
|
|
|
|
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
}
|
|
|
|
// GetTimeEntryByID handles GET /time-entries/:id
|
|
//
|
|
// @Summary Get time entry by ID
|
|
// @Description Get a time entry by its ID
|
|
// @Tags time-entries
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Time Entry ID"
|
|
// @Success 200 {object} utils.Response{data=dto.TimeEntryDto}
|
|
// @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 /time-entries/{id} [get]
|
|
func (h *TimeEntryHandler) GetTimeEntryByID(c *gin.Context) {
|
|
// Parse ID from URL
|
|
idStr := c.Param("id")
|
|
id, err := ulid.Parse(idStr)
|
|
if err != nil {
|
|
utils.BadRequestResponse(c, "Invalid time entry ID format")
|
|
return
|
|
}
|
|
|
|
// Get time entry from the database
|
|
timeEntry, err := models.GetTimeEntryByID(c.Request.Context(), models.FromULID(id))
|
|
if err != nil {
|
|
utils.InternalErrorResponse(c, "Error retrieving time entry: "+err.Error())
|
|
return
|
|
}
|
|
|
|
if timeEntry == nil {
|
|
utils.NotFoundResponse(c, "Time entry not found")
|
|
return
|
|
}
|
|
|
|
// Convert to DTO
|
|
timeEntryDTO := convertTimeEntryToDTO(timeEntry)
|
|
|
|
utils.SuccessResponse(c, http.StatusOK, timeEntryDTO)
|
|
}
|
|
|
|
// GetTimeEntriesByUserID handles GET /time-entries/user/:userId
|
|
//
|
|
// @Summary Get time entries by user ID
|
|
// @Description Get a list of time entries for a specific user
|
|
// @Tags time-entries
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param userId path string true "User ID"
|
|
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
|
// @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 /time-entries/user/{userId} [get]
|
|
func (h *TimeEntryHandler) GetTimeEntriesByUserID(c *gin.Context) {
|
|
// Parse user ID from URL
|
|
userIDStr := c.Param("userId")
|
|
userID, err := ulid.Parse(userIDStr)
|
|
if err != nil {
|
|
utils.BadRequestResponse(c, "Invalid user ID format")
|
|
return
|
|
}
|
|
|
|
// Get time entries from the database
|
|
timeEntries, err := models.GetTimeEntriesByUserID(c.Request.Context(), models.FromULID(userID))
|
|
if err != nil {
|
|
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert to DTOs
|
|
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
for i, timeEntry := range timeEntries {
|
|
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
}
|
|
|
|
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
}
|
|
|
|
// GetMyTimeEntries handles GET /time-entries/me
|
|
//
|
|
// @Summary Get current user's time entries
|
|
// @Description Get a list of time entries for the currently authenticated user
|
|
// @Tags time-entries
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
|
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
|
// @Router /time-entries/me [get]
|
|
func (h *TimeEntryHandler) GetMyTimeEntries(c *gin.Context) {
|
|
// Get user ID from context (set by AuthMiddleware)
|
|
userID, err := middleware.GetUserIDFromContext(c)
|
|
if err != nil {
|
|
utils.UnauthorizedResponse(c, "User not authenticated")
|
|
return
|
|
}
|
|
|
|
// Get time entries from the database
|
|
timeEntries, err := models.GetTimeEntriesByUserID(c.Request.Context(), models.FromULID(userID))
|
|
if err != nil {
|
|
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert to DTOs
|
|
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
for i, timeEntry := range timeEntries {
|
|
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
}
|
|
|
|
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
}
|
|
|
|
// GetTimeEntriesByProjectID handles GET /time-entries/project/:projectId
|
|
//
|
|
// @Summary Get time entries by project ID
|
|
// @Description Get a list of time entries for a specific project
|
|
// @Tags time-entries
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param projectId path string true "Project ID"
|
|
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
|
// @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 /time-entries/project/{projectId} [get]
|
|
func (h *TimeEntryHandler) GetTimeEntriesByProjectID(c *gin.Context) {
|
|
// Parse project ID from URL
|
|
projectIDStr := c.Param("projectId")
|
|
projectID, err := ulid.Parse(projectIDStr)
|
|
if err != nil {
|
|
utils.BadRequestResponse(c, "Invalid project ID format")
|
|
return
|
|
}
|
|
|
|
// Get time entries from the database
|
|
timeEntries, err := models.GetTimeEntriesByProjectID(c.Request.Context(), models.FromULID(projectID))
|
|
if err != nil {
|
|
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert to DTOs
|
|
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
for i, timeEntry := range timeEntries {
|
|
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
}
|
|
|
|
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
}
|
|
|
|
// GetTimeEntriesByDateRange handles GET /time-entries/range
|
|
//
|
|
// @Summary Get time entries by date range
|
|
// @Description Get a list of time entries within a specific date range
|
|
// @Tags time-entries
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param start query string true "Start date (ISO 8601 format)"
|
|
// @Param end query string true "End date (ISO 8601 format)"
|
|
// @Success 200 {object} utils.Response{data=[]dto.TimeEntryDto}
|
|
// @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 /time-entries/range [get]
|
|
func (h *TimeEntryHandler) GetTimeEntriesByDateRange(c *gin.Context) {
|
|
// Parse date range from query parameters
|
|
startStr := c.Query("start")
|
|
endStr := c.Query("end")
|
|
|
|
if startStr == "" || endStr == "" {
|
|
utils.BadRequestResponse(c, "Start and end dates are required")
|
|
return
|
|
}
|
|
|
|
start, err := time.Parse(time.RFC3339, startStr)
|
|
if err != nil {
|
|
utils.BadRequestResponse(c, "Invalid start date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
|
return
|
|
}
|
|
|
|
end, err := time.Parse(time.RFC3339, endStr)
|
|
if err != nil {
|
|
utils.BadRequestResponse(c, "Invalid end date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)")
|
|
return
|
|
}
|
|
|
|
if end.Before(start) {
|
|
utils.BadRequestResponse(c, "End date cannot be before start date")
|
|
return
|
|
}
|
|
|
|
// Get time entries from the database
|
|
timeEntries, err := models.GetTimeEntriesByDateRange(c.Request.Context(), start, end)
|
|
if err != nil {
|
|
utils.InternalErrorResponse(c, "Error retrieving time entries: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert to DTOs
|
|
timeEntryDTOs := make([]dto.TimeEntryDto, len(timeEntries))
|
|
for i, timeEntry := range timeEntries {
|
|
timeEntryDTOs[i] = convertTimeEntryToDTO(&timeEntry)
|
|
}
|
|
|
|
utils.SuccessResponse(c, http.StatusOK, timeEntryDTOs)
|
|
}
|
|
|
|
// CreateTimeEntry handles POST /time-entries
|
|
//
|
|
// @Summary Create a new time entry
|
|
// @Description Create a new time entry
|
|
// @Tags time-entries
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param timeEntry body dto.TimeEntryCreateDto true "Time Entry data"
|
|
// @Success 201 {object} utils.Response{data=dto.TimeEntryDto}
|
|
// @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 /time-entries [post]
|
|
func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
|
|
// Parse request body
|
|
var timeEntryCreateDTO dto.TimeEntryCreateDto
|
|
if err := c.ShouldBindJSON(&timeEntryCreateDTO); err != nil {
|
|
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert DTO to model
|
|
timeEntryCreate, err := convertCreateTimeEntryDTOToModel(timeEntryCreateDTO)
|
|
if err != nil {
|
|
utils.BadRequestResponse(c, err.Error())
|
|
return
|
|
}
|
|
|
|
// Create time entry in the database
|
|
timeEntry, err := models.CreateTimeEntry(c.Request.Context(), timeEntryCreate)
|
|
if err != nil {
|
|
utils.InternalErrorResponse(c, "Error creating time entry: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Convert to DTO
|
|
timeEntryDTO := convertTimeEntryToDTO(timeEntry)
|
|
|
|
utils.SuccessResponse(c, http.StatusCreated, timeEntryDTO)
|
|
}
|
|
|
|
// UpdateTimeEntry handles PUT /time-entries/:id
|
|
//
|
|
// @Summary Update a time entry
|
|
// @Description Update an existing time entry
|
|
// @Tags time-entries
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Time Entry ID"
|
|
// @Param timeEntry body dto.TimeEntryUpdateDto true "Time Entry data"
|
|
// @Success 200 {object} utils.Response{data=dto.TimeEntryDto}
|
|
// @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 /time-entries/{id} [put]
|
|
func (h *TimeEntryHandler) UpdateTimeEntry(c *gin.Context) {
|
|
// Parse ID from URL
|
|
idStr := c.Param("id")
|
|
id, err := ulid.Parse(idStr)
|
|
if err != nil {
|
|
utils.BadRequestResponse(c, "Invalid time entry ID format")
|
|
return
|
|
}
|
|
|
|
// Parse request body
|
|
var timeEntryUpdateDTO dto.TimeEntryUpdateDto
|
|
if err := c.ShouldBindJSON(&timeEntryUpdateDTO); err != nil {
|
|
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Set ID from URL
|
|
timeEntryUpdateDTO.ID = id.String()
|
|
|
|
// Convert DTO to model
|
|
timeEntryUpdate, err := convertUpdateTimeEntryDTOToModel(timeEntryUpdateDTO)
|
|
if err != nil {
|
|
utils.BadRequestResponse(c, err.Error())
|
|
return
|
|
}
|
|
|
|
// Update time entry in the database
|
|
timeEntry, err := models.UpdateTimeEntry(c.Request.Context(), timeEntryUpdate)
|
|
if err != nil {
|
|
utils.InternalErrorResponse(c, "Error updating time entry: "+err.Error())
|
|
return
|
|
}
|
|
|
|
if timeEntry == nil {
|
|
utils.NotFoundResponse(c, "Time entry not found")
|
|
return
|
|
}
|
|
|
|
// Convert to DTO
|
|
timeEntryDTO := convertTimeEntryToDTO(timeEntry)
|
|
|
|
utils.SuccessResponse(c, http.StatusOK, timeEntryDTO)
|
|
}
|
|
|
|
// DeleteTimeEntry handles DELETE /time-entries/:id
|
|
//
|
|
// @Summary Delete a time entry
|
|
// @Description Delete a time entry by its ID
|
|
// @Tags time-entries
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Time Entry 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 /time-entries/{id} [delete]
|
|
func (h *TimeEntryHandler) DeleteTimeEntry(c *gin.Context) {
|
|
// Parse ID from URL
|
|
idStr := c.Param("id")
|
|
id, err := ulid.Parse(idStr)
|
|
if err != nil {
|
|
utils.BadRequestResponse(c, "Invalid time entry ID format")
|
|
return
|
|
}
|
|
|
|
// Delete time entry from the database
|
|
err = models.DeleteTimeEntry(c.Request.Context(), models.FromULID(id))
|
|
if err != nil {
|
|
utils.InternalErrorResponse(c, "Error deleting time entry: "+err.Error())
|
|
return
|
|
}
|
|
|
|
utils.SuccessResponse(c, http.StatusNoContent, nil)
|
|
}
|
|
|
|
// Helper functions for DTO conversion
|
|
|
|
func convertTimeEntryToDTO(timeEntry *models.TimeEntry) dto.TimeEntryDto {
|
|
return dto.TimeEntryDto{
|
|
ID: timeEntry.ID.String(),
|
|
CreatedAt: timeEntry.CreatedAt,
|
|
UpdatedAt: timeEntry.UpdatedAt,
|
|
UserID: timeEntry.UserID.String(), // Simplified conversion
|
|
ProjectID: timeEntry.ProjectID.String(), // Simplified conversion
|
|
ActivityID: timeEntry.ActivityID.String(), // Simplified conversion
|
|
Start: timeEntry.Start,
|
|
End: timeEntry.End,
|
|
Description: timeEntry.Description,
|
|
Billable: timeEntry.Billable,
|
|
}
|
|
}
|
|
|
|
func convertCreateTimeEntryDTOToModel(dto dto.TimeEntryCreateDto) (models.TimeEntryCreate, error) {
|
|
// Convert IDs from int to ULID (this is a simplification, adjust as needed)
|
|
userID, err := models.ULIDWrapperFromString(dto.UserID)
|
|
if err != nil {
|
|
return models.TimeEntryCreate{}, fmt.Errorf("invalid user ID: %w", err)
|
|
}
|
|
|
|
projectID, err := models.ULIDWrapperFromString(dto.ProjectID)
|
|
if err != nil {
|
|
return models.TimeEntryCreate{}, fmt.Errorf("invalid project ID: %w", err)
|
|
}
|
|
|
|
activityID, err := models.ULIDWrapperFromString(dto.ActivityID)
|
|
if err != nil {
|
|
return models.TimeEntryCreate{}, fmt.Errorf("invalid activity ID: %w", err)
|
|
}
|
|
|
|
return models.TimeEntryCreate{
|
|
UserID: userID,
|
|
ProjectID: projectID,
|
|
ActivityID: activityID,
|
|
Start: dto.Start,
|
|
End: dto.End,
|
|
Description: dto.Description,
|
|
Billable: dto.Billable,
|
|
}, nil
|
|
}
|
|
|
|
func convertUpdateTimeEntryDTOToModel(dto dto.TimeEntryUpdateDto) (models.TimeEntryUpdate, error) {
|
|
id, err := ulid.Parse(dto.ID)
|
|
if err != nil {
|
|
return models.TimeEntryUpdate{}, fmt.Errorf("invalid time entry ID: %w", err)
|
|
}
|
|
update := models.TimeEntryUpdate{
|
|
ID: models.FromULID(id),
|
|
}
|
|
|
|
if dto.UserID != nil {
|
|
userID, err := models.ULIDWrapperFromString(*dto.UserID)
|
|
if err != nil {
|
|
return models.TimeEntryUpdate{}, fmt.Errorf("invalid user ID: %w", err)
|
|
}
|
|
update.UserID = &userID
|
|
}
|
|
|
|
if dto.ProjectID != nil {
|
|
projectID, err := models.ULIDWrapperFromString(*dto.ProjectID)
|
|
if err != nil {
|
|
return models.TimeEntryUpdate{}, fmt.Errorf("invalid project ID: %w", err)
|
|
}
|
|
update.ProjectID = &projectID
|
|
}
|
|
|
|
if dto.ActivityID != nil {
|
|
activityID, err := models.ULIDWrapperFromString(*dto.ActivityID)
|
|
if err != nil {
|
|
return models.TimeEntryUpdate{}, fmt.Errorf("invalid activity ID: %w", err)
|
|
}
|
|
update.ActivityID = &activityID
|
|
}
|
|
|
|
if dto.Start != nil {
|
|
update.Start = dto.Start
|
|
}
|
|
|
|
if dto.End != nil {
|
|
update.End = dto.End
|
|
}
|
|
|
|
if dto.Description != nil {
|
|
update.Description = dto.Description
|
|
}
|
|
|
|
if dto.Billable != nil {
|
|
update.Billable = dto.Billable
|
|
}
|
|
|
|
return update, nil
|
|
}
|