feat: Implement menu assignment and removal for clients with corresponding service and repository methods

This commit is contained in:
Habib Fatkhul Rohman 2025-10-28 13:55:33 +07:00
parent 4371103cc7
commit 1bfd4ff176
6 changed files with 285 additions and 140 deletions

View File

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

View File

@ -10,10 +10,15 @@ const (
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 (
@ -21,6 +26,7 @@ var (
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 (
@ -41,6 +47,12 @@ type (
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
}
)

View File

@ -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+"%")
}
// 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.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+"%")
}
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
}

View File

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

View File

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

View File

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