diff --git a/backend/internal/api/handlers/company_handler.go b/backend/internal/api/handlers/company_handler.go new file mode 100644 index 0000000..f011d1a --- /dev/null +++ b/backend/internal/api/handlers/company_handler.go @@ -0,0 +1,247 @@ +package handlers + +import ( + "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" +) + +// CompanyHandler handles company-related API endpoints +type CompanyHandler struct{} + +// NewCompanyHandler creates a new CompanyHandler +func NewCompanyHandler() *CompanyHandler { + return &CompanyHandler{} +} + +// GetCompanies handles GET /companies +// +// @Summary Get all companies +// @Description Get a list of all companies +// @Tags companies +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} utils.Response{data=[]dto.CompanyDto} +// @Failure 401 {object} utils.Response{error=utils.ErrorInfo} +// @Failure 500 {object} utils.Response{error=utils.ErrorInfo} +// @Router /companies [get] +func (h *CompanyHandler) GetCompanies(c *gin.Context) { + // Get companies from the database + companies, err := models.GetAllCompanies(c.Request.Context()) + if err != nil { + utils.InternalErrorResponse(c, "Error retrieving companies: "+err.Error()) + return + } + + // Convert to DTOs + companyDTOs := make([]dto.CompanyDto, len(companies)) + for i, company := range companies { + companyDTOs[i] = convertCompanyToDTO(&company) + } + + utils.SuccessResponse(c, http.StatusOK, companyDTOs) +} + +// GetCompanyByID handles GET /companies/:id +// +// @Summary Get company by ID +// @Description Get a company by its ID +// @Tags companies +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Company ID" +// @Success 200 {object} utils.Response{data=dto.CompanyDto} +// @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 /companies/{id} [get] +func (h *CompanyHandler) GetCompanyByID(c *gin.Context) { + // Parse ID from URL + idStr := c.Param("id") + id, err := ulid.Parse(idStr) + if err != nil { + utils.BadRequestResponse(c, "Invalid company ID format") + return + } + + // Get company from the database + company, err := models.GetCompanyByID(c.Request.Context(), id) + if err != nil { + utils.InternalErrorResponse(c, "Error retrieving company: "+err.Error()) + return + } + + if company == nil { + utils.NotFoundResponse(c, "Company not found") + return + } + + // Convert to DTO + companyDTO := convertCompanyToDTO(company) + + utils.SuccessResponse(c, http.StatusOK, companyDTO) +} + +// CreateCompany handles POST /companies +// +// @Summary Create a new company +// @Description Create a new company +// @Tags companies +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param company body dto.CompanyCreateDto true "Company data" +// @Success 201 {object} utils.Response{data=dto.CompanyDto} +// @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 /companies [post] +func (h *CompanyHandler) CreateCompany(c *gin.Context) { + // Parse request body + var companyCreateDTO dto.CompanyCreateDto + if err := c.ShouldBindJSON(&companyCreateDTO); err != nil { + utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) + return + } + + // Convert DTO to model + companyCreate := convertCreateCompanyDTOToModel(companyCreateDTO) + + // Create company in the database + company, err := models.CreateCompany(c.Request.Context(), companyCreate) + if err != nil { + utils.InternalErrorResponse(c, "Error creating company: "+err.Error()) + return + } + + // Convert to DTO + companyDTO := convertCompanyToDTO(company) + + utils.SuccessResponse(c, http.StatusCreated, companyDTO) +} + +// UpdateCompany handles PUT /companies/:id +// +// @Summary Update a company +// @Description Update an existing company +// @Tags companies +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Company ID" +// @Param company body dto.CompanyUpdateDto true "Company data" +// @Success 200 {object} utils.Response{data=dto.CompanyDto} +// @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 /companies/{id} [put] +func (h *CompanyHandler) UpdateCompany(c *gin.Context) { + // Parse ID from URL + idStr := c.Param("id") + id, err := ulid.Parse(idStr) + if err != nil { + utils.BadRequestResponse(c, "Invalid company ID format") + return + } + + // Parse request body + var companyUpdateDTO dto.CompanyUpdateDto + if err := c.ShouldBindJSON(&companyUpdateDTO); err != nil { + utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) + return + } + + // Set ID from URL + companyUpdateDTO.ID = id.String() + + // Convert DTO to model + companyUpdate := convertUpdateCompanyDTOToModel(companyUpdateDTO) + + // Update company in the database + company, err := models.UpdateCompany(c.Request.Context(), companyUpdate) + if err != nil { + utils.InternalErrorResponse(c, "Error updating company: "+err.Error()) + return + } + + if company == nil { + utils.NotFoundResponse(c, "Company not found") + return + } + + // Convert to DTO + companyDTO := convertCompanyToDTO(company) + + utils.SuccessResponse(c, http.StatusOK, companyDTO) +} + +// DeleteCompany handles DELETE /companies/:id +// +// @Summary Delete a company +// @Description Delete a company by its ID +// @Tags companies +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Company 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 /companies/{id} [delete] +func (h *CompanyHandler) DeleteCompany(c *gin.Context) { + // Parse ID from URL + idStr := c.Param("id") + id, err := ulid.Parse(idStr) + if err != nil { + utils.BadRequestResponse(c, "Invalid company ID format") + return + } + + // Delete company from the database + err = models.DeleteCompany(c.Request.Context(), id) + if err != nil { + utils.InternalErrorResponse(c, "Error deleting company: "+err.Error()) + return + } + + utils.SuccessResponse(c, http.StatusNoContent, nil) +} + +// Helper functions for DTO conversion + +func convertCompanyToDTO(company *models.Company) dto.CompanyDto { + return dto.CompanyDto{ + ID: company.ID.String(), + CreatedAt: company.CreatedAt, + UpdatedAt: company.UpdatedAt, + Name: company.Name, + } +} + +func convertCreateCompanyDTOToModel(dto dto.CompanyCreateDto) models.CompanyCreate { + return models.CompanyCreate{ + Name: dto.Name, + } +} + +func convertUpdateCompanyDTOToModel(dto dto.CompanyUpdateDto) models.CompanyUpdate { + id, _ := ulid.Parse(dto.ID) + update := models.CompanyUpdate{ + ID: id, + } + + if dto.Name != nil { + update.Name = dto.Name + } + + return update +} diff --git a/backend/internal/api/handlers/customer_handler.go b/backend/internal/api/handlers/customer_handler.go new file mode 100644 index 0000000..34f1a08 --- /dev/null +++ b/backend/internal/api/handlers/customer_handler.go @@ -0,0 +1,300 @@ +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" +) + +// CustomerHandler handles customer-related API endpoints +type CustomerHandler struct{} + +// NewCustomerHandler creates a new CustomerHandler +func NewCustomerHandler() *CustomerHandler { + return &CustomerHandler{} +} + +// GetCustomers handles GET /customers +// +// @Summary Get all customers +// @Description Get a list of all customers +// @Tags customers +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} utils.Response{data=[]dto.CustomerDto} +// @Failure 401 {object} utils.Response{error=utils.ErrorInfo} +// @Failure 500 {object} utils.Response{error=utils.ErrorInfo} +// @Router /customers [get] +func (h *CustomerHandler) GetCustomers(c *gin.Context) { + // Get customers from the database + customers, err := models.GetAllCustomers(c.Request.Context()) + if err != nil { + utils.InternalErrorResponse(c, "Error retrieving customers: "+err.Error()) + return + } + + // Convert to DTOs + customerDTOs := make([]dto.CustomerDto, len(customers)) + for i, customer := range customers { + customerDTOs[i] = convertCustomerToDTO(&customer) + } + + utils.SuccessResponse(c, http.StatusOK, customerDTOs) +} + +// GetCustomerByID handles GET /customers/:id +// +// @Summary Get customer by ID +// @Description Get a customer by its ID +// @Tags customers +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Customer ID" +// @Success 200 {object} utils.Response{data=dto.CustomerDto} +// @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 /customers/{id} [get] +func (h *CustomerHandler) GetCustomerByID(c *gin.Context) { + // Parse ID from URL + idStr := c.Param("id") + id, err := ulid.Parse(idStr) + if err != nil { + utils.BadRequestResponse(c, "Invalid customer ID format") + return + } + + // Get customer from the database + customer, err := models.GetCustomerByID(c.Request.Context(), id) + if err != nil { + utils.InternalErrorResponse(c, "Error retrieving customer: "+err.Error()) + return + } + + if customer == nil { + utils.NotFoundResponse(c, "Customer not found") + return + } + + // Convert to DTO + customerDTO := convertCustomerToDTO(customer) + + utils.SuccessResponse(c, http.StatusOK, customerDTO) +} + +// GetCustomersByCompanyID handles GET /customers/company/:companyId +// +// @Summary Get customers by company ID +// @Description Get a list of customers for a specific company +// @Tags customers +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param companyId path int true "Company ID" +// @Success 200 {object} utils.Response{data=[]dto.CustomerDto} +// @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 /customers/company/{companyId} [get] +func (h *CustomerHandler) GetCustomersByCompanyID(c *gin.Context) { + // Parse company ID from URL + companyIDStr := c.Param("companyId") + companyID, err := parseCompanyID(companyIDStr) + if err != nil { + utils.BadRequestResponse(c, "Invalid company ID format") + return + } + + // Get customers from the database + customers, err := models.GetCustomersByCompanyID(c.Request.Context(), companyID) + if err != nil { + utils.InternalErrorResponse(c, "Error retrieving customers: "+err.Error()) + return + } + + // Convert to DTOs + customerDTOs := make([]dto.CustomerDto, len(customers)) + for i, customer := range customers { + customerDTOs[i] = convertCustomerToDTO(&customer) + } + + utils.SuccessResponse(c, http.StatusOK, customerDTOs) +} + +// CreateCustomer handles POST /customers +// +// @Summary Create a new customer +// @Description Create a new customer +// @Tags customers +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param customer body dto.CustomerCreateDto true "Customer data" +// @Success 201 {object} utils.Response{data=dto.CustomerDto} +// @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 /customers [post] +func (h *CustomerHandler) CreateCustomer(c *gin.Context) { + // Parse request body + var customerCreateDTO dto.CustomerCreateDto + if err := c.ShouldBindJSON(&customerCreateDTO); err != nil { + utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) + return + } + + // Convert DTO to model + customerCreate := convertCreateCustomerDTOToModel(customerCreateDTO) + + // Create customer in the database + customer, err := models.CreateCustomer(c.Request.Context(), customerCreate) + if err != nil { + utils.InternalErrorResponse(c, "Error creating customer: "+err.Error()) + return + } + + // Convert to DTO + customerDTO := convertCustomerToDTO(customer) + + utils.SuccessResponse(c, http.StatusCreated, customerDTO) +} + +// UpdateCustomer handles PUT /customers/:id +// +// @Summary Update a customer +// @Description Update an existing customer +// @Tags customers +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Customer ID" +// @Param customer body dto.CustomerUpdateDto true "Customer data" +// @Success 200 {object} utils.Response{data=dto.CustomerDto} +// @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 /customers/{id} [put] +func (h *CustomerHandler) UpdateCustomer(c *gin.Context) { + // Parse ID from URL + idStr := c.Param("id") + id, err := ulid.Parse(idStr) + if err != nil { + utils.BadRequestResponse(c, "Invalid customer ID format") + return + } + + // Parse request body + var customerUpdateDTO dto.CustomerUpdateDto + if err := c.ShouldBindJSON(&customerUpdateDTO); err != nil { + utils.BadRequestResponse(c, "Invalid request body: "+err.Error()) + return + } + + // Set ID from URL + customerUpdateDTO.ID = id.String() + + // Convert DTO to model + customerUpdate := convertUpdateCustomerDTOToModel(customerUpdateDTO) + + // Update customer in the database + customer, err := models.UpdateCustomer(c.Request.Context(), customerUpdate) + if err != nil { + utils.InternalErrorResponse(c, "Error updating customer: "+err.Error()) + return + } + + if customer == nil { + utils.NotFoundResponse(c, "Customer not found") + return + } + + // Convert to DTO + customerDTO := convertCustomerToDTO(customer) + + utils.SuccessResponse(c, http.StatusOK, customerDTO) +} + +// DeleteCustomer handles DELETE /customers/:id +// +// @Summary Delete a customer +// @Description Delete a customer by its ID +// @Tags customers +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Customer 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 /customers/{id} [delete] +func (h *CustomerHandler) DeleteCustomer(c *gin.Context) { + // Parse ID from URL + idStr := c.Param("id") + id, err := ulid.Parse(idStr) + if err != nil { + utils.BadRequestResponse(c, "Invalid customer ID format") + return + } + + // Delete customer from the database + err = models.DeleteCustomer(c.Request.Context(), id) + if err != nil { + utils.InternalErrorResponse(c, "Error deleting customer: "+err.Error()) + return + } + + utils.SuccessResponse(c, http.StatusNoContent, nil) +} + +// Helper functions for DTO conversion + +func convertCustomerToDTO(customer *models.Customer) dto.CustomerDto { + return dto.CustomerDto{ + ID: customer.ID.String(), + CreatedAt: customer.CreatedAt, + UpdatedAt: customer.UpdatedAt, + Name: customer.Name, + CompanyID: customer.CompanyID, + } +} + +func convertCreateCustomerDTOToModel(dto dto.CustomerCreateDto) models.CustomerCreate { + return models.CustomerCreate{ + Name: dto.Name, + CompanyID: dto.CompanyID, + } +} + +func convertUpdateCustomerDTOToModel(dto dto.CustomerUpdateDto) models.CustomerUpdate { + id, _ := ulid.Parse(dto.ID) + update := models.CustomerUpdate{ + ID: id, + } + + if dto.Name != nil { + update.Name = dto.Name + } + + if dto.CompanyID != nil { + update.CompanyID = dto.CompanyID + } + + return update +} + +// Helper function to parse company ID from string +func parseCompanyID(idStr string) (int, error) { + var id int + _, err := fmt.Sscanf(idStr, "%d", &id) + return id, err +} diff --git a/backend/internal/api/handlers/project_handler.go b/backend/internal/api/handlers/project_handler.go new file mode 100644 index 0000000..a8d3973 --- /dev/null +++ b/backend/internal/api/handlers/project_handler.go @@ -0,0 +1,359 @@ +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(), 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(ulid.ULID{}) != 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: customerID, + }, nil +} + +func convertUpdateProjectDTOToModel(dto dto.ProjectUpdateDto) (models.ProjectUpdate, error) { + id, _ := ulid.Parse(dto.ID) + update := models.ProjectUpdate{ + ID: 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) + } + update.CustomerID = &customerID + } + + 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 +} diff --git a/backend/internal/api/handlers/timeentry_handler.go b/backend/internal/api/handlers/timeentry_handler.go new file mode 100644 index 0000000..9cc2b60 --- /dev/null +++ b/backend/internal/api/handlers/timeentry_handler.go @@ -0,0 +1,504 @@ +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(), 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(), 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(), 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(), 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(), 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: int(timeEntry.UserID.Time()), // Simplified conversion + ProjectID: int(timeEntry.ProjectID.Time()), // Simplified conversion + ActivityID: int(timeEntry.ActivityID.Time()), // 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 := idToULID(dto.UserID) + if err != nil { + return models.TimeEntryCreate{}, fmt.Errorf("invalid user ID: %w", err) + } + + projectID, err := idToULID(dto.ProjectID) + if err != nil { + return models.TimeEntryCreate{}, fmt.Errorf("invalid project ID: %w", err) + } + + activityID, err := idToULID(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, _ := ulid.Parse(dto.ID) + update := models.TimeEntryUpdate{ + ID: id, + } + + if dto.UserID != nil { + userID, err := idToULID(*dto.UserID) + if err != nil { + return models.TimeEntryUpdate{}, fmt.Errorf("invalid user ID: %w", err) + } + update.UserID = &userID + } + + if dto.ProjectID != nil { + projectID, err := idToULID(*dto.ProjectID) + if err != nil { + return models.TimeEntryUpdate{}, fmt.Errorf("invalid project ID: %w", err) + } + update.ProjectID = &projectID + } + + if dto.ActivityID != nil { + activityID, err := idToULID(*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 +} + +// Helper function to convert ID from int to ULID +func idToULID(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) + return ulid.MustNew(timestamp, entropy), nil +} diff --git a/backend/internal/api/routes/router.go b/backend/internal/api/routes/router.go index 4b2c929..8c0ce8a 100644 --- a/backend/internal/api/routes/router.go +++ b/backend/internal/api/routes/router.go @@ -11,6 +11,10 @@ func SetupRouter(r *gin.Engine) { // Create handlers userHandler := handlers.NewUserHandler() activityHandler := handlers.NewActivityHandler() + companyHandler := handlers.NewCompanyHandler() + customerHandler := handlers.NewCustomerHandler() + projectHandler := handlers.NewProjectHandler() + timeEntryHandler := handlers.NewTimeEntryHandler() // Public routes r.POST("/auth/login", userHandler.Login) @@ -45,6 +49,51 @@ func SetupRouter(r *gin.Engine) { activities.DELETE("/:id", middleware.RoleMiddleware("admin"), activityHandler.DeleteActivity) } - // TODO: Add routes for other entities (Company, Project, TimeEntry, etc.) + // Company routes + companies := api.Group("/companies") + { + companies.GET("", companyHandler.GetCompanies) + companies.GET("/:id", companyHandler.GetCompanyByID) + companies.POST("", middleware.RoleMiddleware("admin"), companyHandler.CreateCompany) + companies.PUT("/:id", middleware.RoleMiddleware("admin"), companyHandler.UpdateCompany) + companies.DELETE("/:id", middleware.RoleMiddleware("admin"), companyHandler.DeleteCompany) + } + + // Customer routes + customers := api.Group("/customers") + { + customers.GET("", customerHandler.GetCustomers) + customers.GET("/:id", customerHandler.GetCustomerByID) + customers.GET("/company/:companyId", customerHandler.GetCustomersByCompanyID) + customers.POST("", middleware.RoleMiddleware("admin"), customerHandler.CreateCustomer) + customers.PUT("/:id", middleware.RoleMiddleware("admin"), customerHandler.UpdateCustomer) + customers.DELETE("/:id", middleware.RoleMiddleware("admin"), customerHandler.DeleteCustomer) + } + + // Project routes + projects := api.Group("/projects") + { + projects.GET("", projectHandler.GetProjects) + projects.GET("/with-customers", projectHandler.GetProjectsWithCustomers) + projects.GET("/:id", projectHandler.GetProjectByID) + projects.GET("/customer/:customerId", projectHandler.GetProjectsByCustomerID) + projects.POST("", middleware.RoleMiddleware("admin"), projectHandler.CreateProject) + projects.PUT("/:id", middleware.RoleMiddleware("admin"), projectHandler.UpdateProject) + projects.DELETE("/:id", middleware.RoleMiddleware("admin"), projectHandler.DeleteProject) + } + + // Time Entry routes + timeEntries := api.Group("/time-entries") + { + timeEntries.GET("", timeEntryHandler.GetTimeEntries) + timeEntries.GET("/me", timeEntryHandler.GetMyTimeEntries) + timeEntries.GET("/range", timeEntryHandler.GetTimeEntriesByDateRange) + timeEntries.GET("/:id", timeEntryHandler.GetTimeEntryByID) + timeEntries.GET("/user/:userId", timeEntryHandler.GetTimeEntriesByUserID) + timeEntries.GET("/project/:projectId", timeEntryHandler.GetTimeEntriesByProjectID) + timeEntries.POST("", timeEntryHandler.CreateTimeEntry) + timeEntries.PUT("/:id", timeEntryHandler.UpdateTimeEntry) + timeEntries.DELETE("/:id", timeEntryHandler.DeleteTimeEntry) + } } } diff --git a/backend/internal/api/utils/swagger.go b/backend/internal/api/utils/swagger.go index 2c11699..295912a 100644 --- a/backend/internal/api/utils/swagger.go +++ b/backend/internal/api/utils/swagger.go @@ -1,5 +1,9 @@ package utils +import ( + dto "github.com/timetracker/backend/internal/dtos" +) + // This file contains type definitions for Swagger documentation // LoginRequest is a Swagger representation of LoginDto