feat: Implement company management API endpoints and handler

This commit is contained in:
Jean Jacques Avril 2025-03-10 22:03:13 +00:00
parent 58173b436c
commit 8785b86bfc
6 changed files with 1464 additions and 1 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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