diff --git a/modules/client/controller/client_controller.go b/modules/client/controller/client_controller.go index 5dd28c3..3906858 100644 --- a/modules/client/controller/client_controller.go +++ b/modules/client/controller/client_controller.go @@ -10,9 +10,10 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/modules/client/service" "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" "github.com/Caknoooo/go-gin-clean-starter/pkg/utils" - "github.com/Caknoooo/go-pagination" "github.com/gin-gonic/gin" "github.com/samber/do" + + // "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -23,6 +24,8 @@ type ( Delete(ctx *gin.Context) GetById(ctx *gin.Context) GetAll(ctx *gin.Context) + AssignMenusToClient(ctx *gin.Context) + RemoveMenusFromClient(ctx *gin.Context) } clientController struct { @@ -116,34 +119,69 @@ func (c *clientController) GetById(ctx *gin.Context) { res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_CLIENT, client) ctx.JSON(http.StatusOK, res) } - func (c *clientController) GetAll(ctx *gin.Context) { - // clientId := ctx.MustGet("client_id").(string) - var filter = &query.ClientFilter{ - Name: ctx.Query("name"), - PIC: ctx.Query("pic"), - Phone: ctx.Query("phone"), - Email: ctx.Query("email"), - Address: ctx.Query("address"), - // ClientID: clientId, - Includes: ctx.QueryArray("includes"), + // Ambil filter dari query param + var filter query.ClientFilter + if err := ctx.ShouldBindQuery(&filter); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_CLIENT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return } - filter.BindPagination(ctx) - ctx.ShouldBindQuery(filter) - groups, total, err := pagination.PaginatedQueryWithIncludableAndOptions[query.M_Client]( - c.db, - filter, - pagination.PaginatedQueryOptions{ - EnableSoftDelete: true, - }, - ) + + // Ambil limit & offset dari query param (default: limit=10, offset=0) + perPage := utils.ParseInt(ctx.DefaultQuery("per_page", "10")) + page := utils.ParseInt(ctx.DefaultQuery("page", "1")) + filter.PerPage = perPage + filter.Page = (page - 1) * perPage + + clients, total, err := c.clientService.GetAll(ctx, filter) if err != nil { res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_CLIENT, err.Error(), nil) ctx.JSON(http.StatusBadRequest, res) return } - paginationResponse := pagination.CalculatePagination(filter.Pagination, total) - response := pagination.NewPaginatedResponse(http.StatusOK, dto.MESSAGE_SUCCESS_GET_CLIENT, groups, paginationResponse) + paginationResponse := utils.BuildPaginationResponse(perPage, page, total) + response := utils.BuildResponseSuccessWithPagination(http.StatusOK, dto.MESSAGE_SUCCESS_GET_CLIENT, clients, paginationResponse) ctx.JSON(http.StatusOK, response) } + +// AssignMenusToClient implements MenuController. +func (c *clientController) AssignMenusToClient(ctx *gin.Context) { + clientId := ctx.Param("id") + var req dto.AssignMenusToClientRequest + if err := ctx.ShouldBind(&req); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.AbortWithStatusJSON(http.StatusBadRequest, res) + return + } + + if err := c.clientService.AssignMenusToClient(ctx.Request.Context(), clientId, req.MenuIds); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_ASSIGN_MENUS_TO_CLIENT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + resp := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_ASSIGN_MENUS_TO_CLIENT, nil) + ctx.JSON(http.StatusOK, resp) +} + +// RemoveMenusFromClient implements MenuController. +func (c *clientController) RemoveMenusFromClient(ctx *gin.Context) { + clientId := ctx.Param("id") + var req dto.RemoveMenusFromClientRequest + if err := ctx.ShouldBind(&req); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.AbortWithStatusJSON(http.StatusBadRequest, res) + return + } + + if err := c.clientService.RemoveMenusFromClient(ctx.Request.Context(), clientId, req.MenuIds); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_REMOVE_MENUS_FROM_CLIENT, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + resp := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_REMOVE_MENUS_FROM_CLIENT, nil) + ctx.JSON(http.StatusOK, resp) +} diff --git a/modules/client/dto/client_dto.go b/modules/client/dto/client_dto.go index a6bd4bc..c195d6d 100644 --- a/modules/client/dto/client_dto.go +++ b/modules/client/dto/client_dto.go @@ -5,22 +5,28 @@ import ( ) const ( - MESSAGE_FAILED_CREATE_CLIENT = "failed create client" - MESSAGE_SUCCESS_CREATE_CLIENT = "success create client" - MESSAGE_FAILED_GET_CLIENT = "failed get client" - MESSAGE_SUCCESS_GET_CLIENT = "success get client" - MESSAGE_FAILED_UPDATE_CLIENT = "failed update client" - MESSAGE_SUCCESS_UPDATE_CLIENT = "success update client" - MESSAGE_FAILED_DELETE_CLIENT = "failed delete client" - MESSAGE_SUCCESS_DELETE_CLIENT = "success delete client" - MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" + MESSAGE_FAILED_CREATE_CLIENT = "failed create client" + MESSAGE_SUCCESS_CREATE_CLIENT = "success create client" + MESSAGE_FAILED_GET_CLIENT = "failed get client" + MESSAGE_SUCCESS_GET_CLIENT = "success get client" + MESSAGE_FAILED_UPDATE_CLIENT = "failed update client" + MESSAGE_FAILED_PROSES_REQUEST = "failed proses request" + MESSAGE_SUCCESS_UPDATE_CLIENT = "success update client" + MESSAGE_FAILED_DELETE_CLIENT = "failed delete client" + MESSAGE_SUCCESS_DELETE_CLIENT = "success delete client" + MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" + MESSAGE_SUCCESS_ASSIGN_MENUS_TO_CLIENT = "success assign menus to client" + MESSAGE_SUCCESS_REMOVE_MENUS_FROM_CLIENT = "success remove menus from client" + MESSAGE_FAILED_ASSIGN_MENUS_TO_CLIENT = "failed assign menus to client" + MESSAGE_FAILED_REMOVE_MENUS_FROM_CLIENT = "failed remove menus from client" ) var ( - ErrCreateClient = errors.New("failed to create client") - ErrGetClientById = errors.New("failed to get client by id") - ErrUpdateClient = errors.New("failed to update client") - ErrDeleteClient = errors.New("failed to delete client") + ErrCreateClient = errors.New("failed to create client") + ErrGetClientById = errors.New("failed to get client by id") + ErrUpdateClient = errors.New("failed to update client") + ErrDeleteClient = errors.New("failed to delete client") + ErrMenuIdsAndClientIdEmpty = errors.New("menuIds and clientId must not be empty") ) type ( @@ -34,13 +40,19 @@ type ( } ClientResponse struct { - ID string `json:"id"` - Name string `json:"name"` - PIC string `json:"pic"` - Phone string `json:"phone"` - Email string `json:"email"` - Address string `json:"address"` - Logo string `json:"logo"` + ID string `json:"id"` + Name string `json:"name"` + PIC string `json:"pic"` + Phone string `json:"phone"` + Email string `json:"email"` + Address string `json:"address"` + Logo string `json:"logo"` + Menus []MenuResponse `json:"menus,omitempty"` + } + + MenuResponse struct { + ID string `json:"id"` + Name string `json:"name"` } ClientUpdateRequest struct { @@ -51,4 +63,22 @@ type ( Address *string `json:"address" form:"address" binding:"omitempty"` Logo *string `form:"-"` // jika update logo, isi manual dari file } + + AssignMenusToClientRequest struct { + MenuIds []string `json:"menu_ids" form:"menu_ids" binding:"required,dive,required,uuid4"` + } + + RemoveMenusFromClientRequest struct { + MenuIds []string `json:"menu_ids" form:"menu_ids" binding:"required,dive,required,uuid4"` + } + + ClientFilter struct { + Name string + PIC string + Phone string + Email string + Address string + PerPage int + Page int + } ) diff --git a/modules/client/query/client_query.go b/modules/client/query/client_query.go index d2bf1b2..2145131 100644 --- a/modules/client/query/client_query.go +++ b/modules/client/query/client_query.go @@ -1,96 +1,45 @@ package query import ( - "github.com/Caknoooo/go-pagination" "gorm.io/gorm" ) -type M_Client struct { - ID string `json:"id"` - Name string `json:"name"` - PIC string `json:"pic"` - Phone string `json:"phone"` - Email string `json:"email"` - Address string `json:"address"` - Logo string `json:"logo"` -} - type ClientFilter struct { - pagination.BaseFilter - Name string `form:"name"` // tambahkan ini - PIC string `form:"pic"` // tambahkan ini - Phone string `form:"phone"` // tambahkan ini - Email string `form:"email"` // tambahkan ini - Address string `form:"address"` // tambahkan ini - ClientID string `form:"client_id"` // tambahkan ini - Includes []string `form:"includes"` // tambahkan ini + Name string `form:"name"` + PIC string `form:"pic"` + Phone string `form:"phone"` + Email string `form:"email"` + Address string `form:"address"` + MenuID string `form:"menu_id"` // tambahkan field ini + MenuName string `form:"menu_name"` // tambahkan field ini + PerPage int `form:"per_page"` + Page int `form:"page"` } -func (f *ClientFilter) ApplyFilters(query *gorm.DB) *gorm.DB { - // Apply your filters here - if f.Name != "" { - query = query.Where("name ILIKE ?", "%"+f.Name+"%") +func ApplyClientFilters(db *gorm.DB, filter ClientFilter) *gorm.DB { + if filter.Name != "" { + db = db.Where("name ILIKE ?", "%"+filter.Name+"%") } - if f.ClientID != "" { - query = query.Where("client_id = ?", f.ClientID) + if filter.PIC != "" { + db = db.Where("pic ILIKE ?", "%"+filter.PIC+"%") } - if f.PIC != "" { - query = query.Where("pic ILIKE ?", "%"+f.PIC+"%") + if filter.Phone != "" { + db = db.Where("phone ILIKE ?", "%"+filter.Phone+"%") } - if f.Phone != "" { - query = query.Where("phone ILIKE ?", "%"+f.Phone+"%") + if filter.Email != "" { + db = db.Where("email ILIKE ?", "%"+filter.Email+"%") } - if f.Email != "" { - query = query.Where("email ILIKE ?", "%"+f.Email+"%") + if filter.Address != "" { + db = db.Where("address ILIKE ?", "%"+filter.Address+"%") } - if f.Address != "" { - query = query.Where("address ILIKE ?", "%"+f.Address+"%") + if filter.MenuName != "" { + db = db.Joins("JOIN m_menu_clients ON m_menu_clients.client_id = m_clients.id"). + Joins("JOIN m_menus ON m_menus.id = m_menu_clients.menu_id"). + Where("m_menus.name ILIKE ?", "%"+filter.MenuName+"%") } - - // Manual preload untuk roles dengan field terbatas - // for _, include := range f.Includes { - // if include == "Roles" { - // query = query.Preload("Roles", func(db *gorm.DB) *gorm.DB { - // return db.Select("id", "name") // Hanya ambil id dan name - // }) - // } - // } - return query -} - -func (f *ClientFilter) GetTableName() string { - return "m_clients" -} - -func (f *ClientFilter) GetSearchFields() []string { - return []string{"name"} -} - -func (f *ClientFilter) GetDefaultSort() string { - return "id asc" -} - -func (f *ClientFilter) GetIncludes() []string { - return f.Includes -} - -func (f *ClientFilter) GetPagination() pagination.PaginationRequest { - return f.Pagination -} - -func (f *ClientFilter) Validate() { - var validIncludes []string - allowedIncludes := f.GetAllowedIncludes() - for _, include := range f.Includes { - if allowedIncludes[include] { - validIncludes = append(validIncludes, include) - } - } - f.Includes = validIncludes -} - -func (f *ClientFilter) GetAllowedIncludes() map[string]bool { - return map[string]bool{ - // "Roles": true, + if filter.MenuID != "" { + db = db.Joins("JOIN m_menu_clients ON m_menu_clients.client_id = m_clients.id"). + Where("m_menu_clients.menu_id = ?", filter.MenuID) } + return db } diff --git a/modules/client/repository/client_repository.go b/modules/client/repository/client_repository.go index 5431881..ac490a4 100644 --- a/modules/client/repository/client_repository.go +++ b/modules/client/repository/client_repository.go @@ -4,8 +4,10 @@ import ( "context" "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/modules/client/query" "github.com/google/uuid" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type ( @@ -13,8 +15,11 @@ type ( Create(ctx context.Context, tx *gorm.DB, client entities.M_Client) (entities.M_Client, error) GetById(ctx context.Context, tx *gorm.DB, clientId string) (entities.M_Client, error) GetByName(ctx context.Context, tx *gorm.DB, name string) (entities.M_Client, error) + GetAll(ctx context.Context, filter query.ClientFilter) ([]entities.M_Client, int64, error) Update(ctx context.Context, tx *gorm.DB, client entities.M_Client) (entities.M_Client, error) Delete(ctx context.Context, tx *gorm.DB, clientId string) error + AssignMenusToClient(ctx context.Context, tx *gorm.DB, clientId string, menuIds []string) error + RemoveMenusFromClient(ctx context.Context, tx *gorm.DB, clientId string, menuIds []string) error } clientRepository struct { @@ -22,6 +27,28 @@ type ( } ) +func (r *clientRepository) GetAll(ctx context.Context, filter query.ClientFilter) ([]entities.M_Client, int64, error) { + var clients []entities.M_Client + var total int64 + + db := r.db.Model(&entities.M_Client{}) + db = query.ApplyClientFilters(db, filter) + + // Hitung total data + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + // Ambil data dengan pagination dan preload menus + err := db. + Preload("Menus"). + Limit(filter.PerPage). + Offset(filter.Page). + Find(&clients).Error + + return clients, total, err +} + func (r *clientRepository) Create(ctx context.Context, tx *gorm.DB, client entities.M_Client) (entities.M_Client, error) { if tx == nil { tx = r.db @@ -40,7 +67,9 @@ func (r *clientRepository) GetById(ctx context.Context, tx *gorm.DB, clientId st tx = r.db } var client entities.M_Client - if err := tx.WithContext(ctx).First(&client, "id = ?", clientId).Error; err != nil { + if err := tx.WithContext(ctx). + Preload("Menus"). + First(&client, "id = ?", clientId).Error; err != nil { return entities.M_Client{}, err } return client, nil @@ -77,6 +106,48 @@ func (r *clientRepository) Delete(ctx context.Context, tx *gorm.DB, clientId str return nil } +// AssignMenusToClient implements MenuRepository. +func (c *clientRepository) AssignMenusToClient(ctx context.Context, tx *gorm.DB, clientId string, menuIds []string) error { + if tx == nil { + tx = c.db + } + + var menuClients []entities.M_Menu_Client + for _, menuId := range menuIds { + menuClient := entities.M_Menu_Client{} + if err := menuClient.MenuID.UnmarshalText([]byte(menuId)); err != nil { + return err + } + if err := menuClient.ClientID.UnmarshalText([]byte(clientId)); err != nil { + return err + } + menuClients = append(menuClients, menuClient) + } + // Use Create with OnConflict to ignore duplicates + if err := tx.WithContext(ctx).Clauses( + // Only insert if not exists (Postgres syntax, adjust if needed) + clause.OnConflict{DoNothing: true}, + ).Create(&menuClients).Error; err != nil { + return err + } + return nil +} + +// RemoveMenusFromClient implements MenuRepository. +func (c *clientRepository) RemoveMenusFromClient(ctx context.Context, tx *gorm.DB, clientId string, menuIds []string) error { + if tx == nil { + tx = c.db + } + + if err := tx.WithContext(ctx). + Where("menu_id IN ? AND client_id = ?", menuIds, clientId). + Delete(&entities.M_Menu_Client{}).Error; err != nil { + return err + } + + return nil +} + func NewClientRepository(db *gorm.DB) ClientRepository { return &clientRepository{db: db} } diff --git a/modules/client/routes.go b/modules/client/routes.go index 82e9457..889586b 100644 --- a/modules/client/routes.go +++ b/modules/client/routes.go @@ -20,5 +20,7 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) { clientRoutes.PUT("/:id", middlewares.Authenticate(jwtService), clientController.Update) clientRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), clientController.Delete) clientRoutes.GET("", middlewares.Authenticate(jwtService), clientController.GetAll) + clientRoutes.POST("/:id/assign-menus", middlewares.Authenticate(jwtService), clientController.AssignMenusToClient) + clientRoutes.POST("/:id/remove-menus", middlewares.Authenticate(jwtService), clientController.RemoveMenusFromClient) } } diff --git a/modules/client/service/client_service.go b/modules/client/service/client_service.go index 4c55be7..8d1524b 100644 --- a/modules/client/service/client_service.go +++ b/modules/client/service/client_service.go @@ -5,6 +5,7 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/database/entities" "github.com/Caknoooo/go-gin-clean-starter/modules/client/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/client/query" "github.com/Caknoooo/go-gin-clean-starter/modules/client/repository" "gorm.io/gorm" ) @@ -13,8 +14,11 @@ type ClientService interface { Create(ctx context.Context, client dto.ClientCreateRequest) (dto.ClientResponse, error) GetById(ctx context.Context, clientId string) (dto.ClientResponse, error) GetByName(ctx context.Context, name string) (dto.ClientResponse, error) + GetAll(ctx context.Context, filter query.ClientFilter) ([]dto.ClientResponse, int64, error) Update(ctx context.Context, client dto.ClientUpdateRequest, clientId string) (dto.ClientResponse, error) Delete(ctx context.Context, clientId string) error + AssignMenusToClient(ctx context.Context, clientId string, menuIds []string) error + RemoveMenusFromClient(ctx context.Context, clientId string, menuIds []string) error } type clientService struct { @@ -22,6 +26,35 @@ type clientService struct { clientRepo repository.ClientRepository } +func (s *clientService) GetAll(ctx context.Context, filter query.ClientFilter) ([]dto.ClientResponse, int64, error) { + clients, total, err := s.clientRepo.GetAll(ctx, filter) + if err != nil { + return nil, 0, err + } + + var responses []dto.ClientResponse + for _, c := range clients { + var menus []dto.MenuResponse + for _, m := range c.Menus { + menus = append(menus, dto.MenuResponse{ + ID: m.ID.String(), + Name: m.Name, + }) + } + responses = append(responses, dto.ClientResponse{ + ID: c.ID.String(), + Name: c.Name, + PIC: c.PIC, + Phone: c.Phone, + Email: c.Email, + Address: c.Address, + Logo: c.Logo, + Menus: menus, + }) + } + return responses, total, nil +} + func (s *clientService) Create(ctx context.Context, req dto.ClientCreateRequest) (dto.ClientResponse, error) { tx := s.db.Begin() defer func() { @@ -56,15 +89,11 @@ func (s *clientService) Create(ctx context.Context, req dto.ClientCreateRequest) // logoBase64 = base64.StdEncoding.EncodeToString(createdClient.Logo) // } - return dto.ClientResponse{ - ID: createdClient.ID.String(), - Name: createdClient.Name, - PIC: createdClient.PIC, - Phone: createdClient.Phone, - Email: createdClient.Email, - Address: createdClient.Address, - Logo: req.Logo, - }, nil + result, err := s.GetById(ctx, createdClient.ID.String()) + if err != nil { + return dto.ClientResponse{}, err + } + return result, nil } func (s *clientService) GetById(ctx context.Context, clientId string) (dto.ClientResponse, error) { @@ -76,6 +105,16 @@ func (s *clientService) GetById(ctx context.Context, clientId string) (dto.Clien // if len(client.Logo) > 0 { // logoBase64 = base64.StdEncoding.EncodeToString(client.Logo) // } + + // Mapping menus ke DTO + var menus []dto.MenuResponse + for _, m := range client.Menus { + menus = append(menus, dto.MenuResponse{ + ID: m.ID.String(), + Name: m.Name, + }) + } + return dto.ClientResponse{ ID: client.ID.String(), Name: client.Name, @@ -84,6 +123,7 @@ func (s *clientService) GetById(ctx context.Context, clientId string) (dto.Clien Email: client.Email, Address: client.Address, Logo: client.Logo, + Menus: menus, }, nil } @@ -151,15 +191,12 @@ func (s *clientService) Update(ctx context.Context, req dto.ClientUpdateRequest, // if len(updatedClient.Logo) > 0 { // logoBase64 = base64.StdEncoding.EncodeToString(updatedClient.Logo) // } - return dto.ClientResponse{ - ID: updatedClient.ID.String(), - Name: updatedClient.Name, - PIC: updatedClient.PIC, - Phone: updatedClient.Phone, - Email: updatedClient.Email, - Address: updatedClient.Address, - Logo: updatedClient.Logo, - }, nil + + result, err := s.GetById(ctx, updatedClient.ID.String()) + if err != nil { + return dto.ClientResponse{}, err + } + return result, nil } func (s *clientService) Delete(ctx context.Context, clientId string) error { @@ -182,6 +219,24 @@ func (s *clientService) Delete(ctx context.Context, clientId string) error { return nil } +// AssignMenusToClient implements MenuService. +func (s *clientService) AssignMenusToClient(ctx context.Context, clientId string, menuIds []string) error { + if len(menuIds) == 0 || clientId == "" { + return dto.ErrMenuIdsAndClientIdEmpty + } + + return s.clientRepo.AssignMenusToClient(ctx, s.db, clientId, menuIds) +} + +// RemoveMenusFromClient implements MenuService. +func (s *clientService) RemoveMenusFromClient(ctx context.Context, clientId string, menuIds []string) error { + if len(menuIds) == 0 || clientId == "" { + return dto.ErrMenuIdsAndClientIdEmpty + } + + return s.clientRepo.RemoveMenusFromClient(ctx, s.db, clientId, menuIds) +} + func NewClientService(clientRepo repository.ClientRepository, db *gorm.DB) ClientService { return &clientService{ clientRepo: clientRepo,