Merge branch 'feat/kandang-groups' into 'development'

[FEAT][BE]: create master data group kandangs

See merge request mbugroup/lti-api!365
This commit is contained in:
Adnan Zahir
2026-03-09 14:06:55 +07:00
21 changed files with 904 additions and 67 deletions
@@ -0,0 +1,28 @@
BEGIN;
ALTER TABLE daily_checklists
DROP CONSTRAINT IF EXISTS fk_daily_checklists_kandang;
UPDATE daily_checklists dc
SET kandang_id = k.id
FROM kandangs k
WHERE
dc.kandang_id = k.kandang_group_id;
ALTER TABLE daily_checklists
ADD CONSTRAINT fk_daily_checklists_kandang
FOREIGN KEY (kandang_id) REFERENCES kandangs (id) ON DELETE CASCADE;
DROP INDEX IF EXISTS idx_kandangs_kandang_group_id;
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS fk_kandangs_kandang_group;
ALTER TABLE kandangs
DROP COLUMN IF EXISTS kandang_group_id;
DROP INDEX IF EXISTS kandang_groups_name_unique;
DROP TABLE IF EXISTS kandang_groups;
COMMIT;
@@ -0,0 +1,91 @@
BEGIN;
CREATE TABLE kandang_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
status VARCHAR(20) NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX kandang_groups_name_unique ON kandang_groups (name)
WHERE
deleted_at IS NULL;
ALTER TABLE kandangs
ADD COLUMN kandang_group_id BIGINT;
CREATE TEMP TABLE tmp_kandang_group_map (
kandang_id BIGINT PRIMARY KEY,
kandang_group_id BIGINT NOT NULL
) ON COMMIT DROP;
INSERT INTO tmp_kandang_group_map (kandang_id, kandang_group_id)
SELECT
k.id,
nextval(pg_get_serial_sequence('kandang_groups', 'id'))
FROM kandangs k
ORDER BY
k.id;
INSERT INTO kandang_groups (
id,
name,
status,
location_id,
pic_id,
created_at,
updated_at,
deleted_at,
created_by
)
SELECT
m.kandang_group_id,
k.name,
k.status,
k.location_id,
CASE WHEN pic.id IS NOT NULL THEN k.pic_id ELSE NULL END,
k.created_at,
k.updated_at,
k.deleted_at,
CASE WHEN creator.id IS NOT NULL THEN k.created_by ELSE NULL END
FROM kandangs k
JOIN tmp_kandang_group_map m ON m.kandang_id = k.id
LEFT JOIN users pic ON pic.id = k.pic_id
LEFT JOIN users creator ON creator.id = k.created_by
ORDER BY
k.id;
UPDATE kandangs k
SET kandang_group_id = m.kandang_group_id
FROM tmp_kandang_group_map m
WHERE
m.kandang_id = k.id;
ALTER TABLE daily_checklists
DROP CONSTRAINT IF EXISTS fk_daily_checklists_kandang;
UPDATE daily_checklists dc
SET kandang_id = m.kandang_group_id
FROM tmp_kandang_group_map m
WHERE
dc.kandang_id = m.kandang_id;
ALTER TABLE daily_checklists
ADD CONSTRAINT fk_daily_checklists_kandang
FOREIGN KEY (kandang_id) REFERENCES kandang_groups (id) ON DELETE CASCADE;
ALTER TABLE kandangs
ALTER COLUMN kandang_group_id SET NOT NULL;
ALTER TABLE kandangs
ADD CONSTRAINT fk_kandangs_kandang_group
FOREIGN KEY (kandang_group_id) REFERENCES kandang_groups (id) ON DELETE RESTRICT ON UPDATE CASCADE;
CREATE INDEX idx_kandangs_kandang_group_id ON kandangs (kandang_group_id);
COMMIT;
+1 -1
View File
@@ -17,7 +17,7 @@ type DailyChecklist struct {
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Kandang KandangGroup `gorm:"foreignKey:KandangId;references:Id"`
Checklist *Checklist `gorm:"foreignKey:ChecklistId;references:Id"`
Creator *User `gorm:"foreignKey:CreatedBy;references:Id"`
Tasks []DailyChecklistTask `gorm:"foreignKey:DailyChecklistId;references:Id"`
+2
View File
@@ -11,6 +11,7 @@ type Kandang struct {
Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
KandangGroupId uint `gorm:"not null"`
Capacity float64 `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
@@ -19,6 +20,7 @@ type Kandang struct {
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
KandangGroup KandangGroup `gorm:"foreignKey:KandangGroupId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
Warehouses []Warehouse `gorm:"foreignKey:KandangId;references:Id"`
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
+24
View File
@@ -0,0 +1,24 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type KandangGroup struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandang_groups_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:KandangGroupId;references:Id"`
}
+2
View File
@@ -130,6 +130,8 @@ const (
P_KandangsUpdateOne = "lti.master.kandangs.update"
P_KandangsDeleteOne = "lti.master.kandangs.delete"
P_KandangGroups = "lti.daily_checklist.master_data.kandang"
P_LocationsGetAll = "lti.master.locations.list"
P_LocationsGetOne = "lti.master.locations.detail"
P_LocationsCreateOne = "lti.master.locations.create"
@@ -64,9 +64,11 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
status = *item.Status
}
var kandang *kandangDTO.KandangRelationDTO
// var kandang *kandangDTO.KandangRelationDTO
var kandang *kandangDTO.KandangGroupRelationDTO
if item.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangRelationDTO(item.Kandang)
// mapped := kandangDTO.ToKandangRelationDTO(item.Kandang)x
mapped := kandangDTO.ToKandangGroupRelationDTO(item.Kandang)
kandang = &mapped
}
@@ -19,19 +19,19 @@ type DailyChecklistRelationDTO struct {
}
type DailyChecklistListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Category string `json:"category"`
Date time.Time `json:"date"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TotalPhase int `json:"total_phase"`
TotalActivity int `json:"total_activity"`
Progress int `json:"progress"`
RejectReason *string `json:"reject_reason"`
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Category string `json:"category"`
Date time.Time `json:"date"`
Kandang *kandangDTO.KandangGroupRelationDTO `json:"kandang,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TotalPhase int `json:"total_phase"`
TotalActivity int `json:"total_activity"`
Progress int `json:"progress"`
RejectReason *string `json:"reject_reason"`
}
type DailyChecklistDetailDTO struct {
@@ -156,9 +156,9 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO {
status = *e.Status
}
var kandang *kandangDTO.KandangRelationDTO
var kandang *kandangDTO.KandangGroupRelationDTO
if e.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangRelationDTO(e.Kandang)
mapped := kandangDTO.ToKandangGroupRelationDTO(e.Kandang)
kandang = &mapped
}
@@ -75,7 +75,7 @@ type DailyChecklistListItem struct {
RejectReason *string
CreatedAt time.Time
UpdatedAt time.Time
Kandang entity.Kandang
Kandang entity.KandangGroup
TotalPhase int
TotalActivity int
Progress int
@@ -142,7 +142,7 @@ func (s dailyChecklistService) ensureChecklistAccess(c *fiber.Ctx, checklistID u
db := s.Repository.DB().WithContext(c.Context()).
Table("daily_checklists dc").
Joins("JOIN kandangs k ON k.id = dc.kandang_id").
Joins("JOIN kandang_groups k ON k.id = dc.kandang_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id").
Where("dc.id = ?", checklistID)
@@ -168,7 +168,7 @@ func (s dailyChecklistService) ensureKandangAccess(c *fiber.Ctx, kandangID uint)
}
db := s.Repository.DB().WithContext(c.Context()).
Table("kandangs k").
Table("kandang_groups k").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id").
Where("k.id = ?", kandangID)
@@ -196,7 +196,7 @@ func (s dailyChecklistService) ensureTaskAccess(c *fiber.Ctx, taskID uint) error
db := s.Repository.DB().WithContext(c.Context()).
Table("daily_checklist_activity_tasks t").
Joins("JOIN daily_checklists dc ON dc.id = t.checklist_id").
Joins("JOIN kandangs k ON k.id = dc.kandang_id").
Joins("JOIN kandang_groups k ON k.id = dc.kandang_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id").
Where("t.id = ?", taskID)
@@ -225,7 +225,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
db := s.Repository.DB().WithContext(c.Context()).
Table("daily_checklists dc").
Joins("JOIN kandangs k ON k.id = dc.kandang_id").
Joins("JOIN kandang_groups k ON k.id = dc.kandang_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id")
@@ -341,9 +341,9 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
}
}
kandangMap := make(map[uint]entity.Kandang)
kandangMap := make(map[uint]entity.KandangGroup)
if len(kandangIDs) > 0 {
var kandangs []entity.Kandang
var kandangs []entity.KandangGroup
if err := s.Repository.DB().WithContext(c.Context()).
Where("id IN ?", kandangIDs).
Preload("Location").
@@ -1019,7 +1019,7 @@ func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.Summa
MAX(a.updated_at) AS last_activity`).
Joins("JOIN daily_checklist_activity_tasks t ON t.id = a.task_id").
Joins("JOIN daily_checklists d ON d.id = t.checklist_id").
Joins("JOIN kandangs k ON k.id = d.kandang_id").
Joins("JOIN kandang_groups k ON k.id = d.kandang_id").
Joins("JOIN employees e ON e.id = a.employee_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas ar ON ar.id = loc.area_id").
@@ -1092,7 +1092,7 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report
Joins("JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id").
Joins("JOIN daily_checklists dc ON dc.id = dcat.checklist_id").
Joins("JOIN employees e ON e.id = dca.employee_id").
Joins("JOIN kandangs k ON k.id = dc.kandang_id").
Joins("JOIN kandang_groups k ON k.id = dc.kandang_id").
Joins("JOIN locations loc ON loc.id = k.location_id").
Joins("JOIN areas a ON a.id = loc.area_id").
Joins("JOIN phases p ON p.id = dcat.phase_id").
@@ -0,0 +1,146 @@
package controller
import (
"math"
"strconv"
"github.com/gofiber/fiber/v2"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
)
type KandangGroupController struct {
KandangGroupService service.KandangGroupService
}
func NewKandangGroupController(kandangGroupService service.KandangGroupService) *KandangGroupController {
return &KandangGroupController{
KandangGroupService: kandangGroupService,
}
}
func (u *KandangGroupController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
LocationId: c.QueryInt("location_id", 0),
PicId: c.QueryInt("pic_id", 0),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.KandangGroupService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.KandangGroupListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all kandang groups successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToKandangGroupListDTOs(result),
})
}
func (u *KandangGroupController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.KandangGroupService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get kandang group successfully",
Data: dto.ToKandangGroupListDTO(*result),
})
}
func (u *KandangGroupController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.KandangGroupService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create kandang group successfully",
Data: dto.ToKandangGroupListDTO(*result),
})
}
func (u *KandangGroupController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.KandangGroupService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update kandang group successfully",
Data: dto.ToKandangGroupListDTO(*result),
})
}
func (u *KandangGroupController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.KandangGroupService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete kandang group successfully",
})
}
@@ -0,0 +1,114 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
type KandangGroupRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
}
type RecordingKandangDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type KandangGroupListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Location locationDTO.LocationRelationDTO `json:"location"`
Pic userDTO.UserRelationDTO `json:"pic"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
RecordingKandangs []RecordingKandangDTO `json:"recording_kandangs"`
}
type KandangGroupDetailDTO struct {
KandangGroupListDTO
}
func ToKandangGroupRelationDTO(e entity.KandangGroup) KandangGroupRelationDTO {
var location *locationDTO.LocationRelationDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(e.Location)
location = &mapped
}
var pic *userDTO.UserRelationDTO
if e.Pic.Id != 0 {
mapped := userDTO.ToUserRelationDTO(e.Pic)
pic = &mapped
}
return KandangGroupRelationDTO{
Id: e.Id,
Name: e.Name,
Status: e.Status,
Location: location,
Pic: pic,
}
}
func ToKandangGroupListDTO(e entity.KandangGroup) KandangGroupListDTO {
var location locationDTO.LocationRelationDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(e.Location)
location = mapped
}
var pic userDTO.UserRelationDTO
if e.Pic.Id != 0 {
mapped := userDTO.ToUserRelationDTO(e.Pic)
pic = mapped
}
var createdUser userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
createdUser = mapped
}
recordingKandangs := make([]RecordingKandangDTO, 0, len(e.Kandangs))
for _, kandang := range e.Kandangs {
recordingKandangs = append(recordingKandangs, RecordingKandangDTO{
Id: kandang.Id,
Name: kandang.Name,
})
}
return KandangGroupListDTO{
Id: e.Id,
Name: e.Name,
Status: e.Status,
Location: location,
Pic: pic,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
RecordingKandangs: recordingKandangs,
}
}
func ToKandangGroupListDTOs(e []entity.KandangGroup) []KandangGroupListDTO {
result := make([]KandangGroupListDTO, len(e))
for i, r := range e {
result[i] = ToKandangGroupListDTO(r)
}
return result
}
func ToKandangGroupDetailDTO(e entity.KandangGroup) KandangGroupDetailDTO {
return KandangGroupDetailDTO{
KandangGroupListDTO: ToKandangGroupListDTO(e),
}
}
@@ -0,0 +1,25 @@
package kandanggroups
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rKandangGroup "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups/repositories"
sKandangGroup "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type KandangGroupModule struct{}
func (KandangGroupModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
kandangGroupRepo := rKandangGroup.NewKandangGroupRepository(db)
userRepo := rUser.NewUserRepository(db)
kandangGroupService := sKandangGroup.NewKandangGroupService(kandangGroupRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
KandangGroupRoutes(router, userService, kandangGroupService)
}
@@ -0,0 +1,41 @@
package repository
import (
"context"
"gorm.io/gorm"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
type KandangGroupRepository interface {
repository.BaseRepository[entity.KandangGroup]
LocationExists(ctx context.Context, locationId uint) (bool, error)
PicExists(ctx context.Context, picId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type KandangGroupRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.KandangGroup]
db *gorm.DB
}
func NewKandangGroupRepository(db *gorm.DB) KandangGroupRepository {
return &KandangGroupRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.KandangGroup](db),
db: db,
}
}
func (r *KandangGroupRepositoryImpl) LocationExists(ctx context.Context, locationId uint) (bool, error) {
return repository.Exists[entity.Location](ctx, r.db, locationId)
}
func (r *KandangGroupRepositoryImpl) PicExists(ctx context.Context, picId uint) (bool, error) {
return repository.Exists[entity.User](ctx, r.db, picId)
}
func (r *KandangGroupRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.KandangGroup](ctx, r.db, name, excludeID)
}
@@ -0,0 +1,23 @@
package kandanggroups
import (
"github.com/gofiber/fiber/v2"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups/controllers"
kandanggroup "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
func KandangGroupRoutes(v1 fiber.Router, u user.UserService, s kandanggroup.KandangGroupService) {
ctrl := controller.NewKandangGroupController(s)
route := v1.Group("/kandang-groups")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_KandangGroups), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_KandangGroups), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_KandangGroups), ctrl.UpdateOne)
route.Delete("/:id", m.RequirePermissions(m.P_KandangGroups), ctrl.DeleteOne)
}
@@ -0,0 +1,238 @@
package service
import (
"errors"
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type KandangGroupService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.KandangGroup, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.KandangGroup, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.KandangGroup, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.KandangGroup, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type kandangGroupService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.KandangGroupRepository
}
func NewKandangGroupService(repo repository.KandangGroupRepository, validate *validator.Validate) KandangGroupService {
return &kandangGroupService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s kandangGroupService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Location").
Preload("Pic").
Preload("Kandangs", func(tx *gorm.DB) *gorm.DB {
return tx.Select("id", "name", "kandang_group_id").Order("name ASC")
})
}
func (s kandangGroupService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.KandangGroup, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
var scopeErr error
offset := (params.Page - 1) * params.Limit
kandangGroups, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db, scopeErr = m.ApplyLocationScope(c, db, "kandang_groups.location_id")
if params.Search != "" {
db = db.Where("kandang_groups.name ILIKE ?", "%"+params.Search+"%")
}
if params.LocationId != 0 {
db = db.Where("kandang_groups.location_id = ?", params.LocationId)
}
if params.PicId != 0 {
db = db.Where("kandang_groups.pic_id = ?", params.PicId)
}
return db.Order("kandang_groups.created_at DESC").Order("kandang_groups.updated_at DESC")
})
if scopeErr != nil {
return nil, 0, scopeErr
}
if err != nil {
s.Log.Errorf("Failed to get kandang groups: %+v", err)
return nil, 0, err
}
return kandangGroups, total, nil
}
func (s kandangGroupService) GetOne(c *fiber.Ctx, id uint) (*entity.KandangGroup, error) {
var scopeErr error
kandangGroup, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db, scopeErr = m.ApplyLocationScope(c, db, "kandang_groups.location_id")
return db
})
if scopeErr != nil {
return nil, scopeErr
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang group not found")
}
if err != nil {
s.Log.Errorf("Failed to get kandang group by id: %+v", err)
return nil, err
}
return kandangGroup, nil
}
func (s *kandangGroupService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.KandangGroup, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if err := m.EnsureLocationAccess(c, s.Repository.DB(), req.LocationId); err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check kandang group name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check kandang group name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang group with name %s already exists", req.Name))
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
common.RelationCheck{Name: "Pic", ID: &req.PicId, Exists: s.Repository.PicExists},
); err != nil {
return nil, err
}
status := strings.ToUpper(strings.TrimSpace(req.Status))
if status == "" {
status = string(utils.KandangStatusNonActive)
}
if !utils.IsValidKandangStatus(status) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang group status")
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
createBody := &entity.KandangGroup{
Name: req.Name,
Status: status,
LocationId: req.LocationId,
PicId: req.PicId,
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create kandang group: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s kandangGroupService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.KandangGroup, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
existing, err := s.GetOne(c, id)
if err != nil {
return nil, err
}
if req.LocationId != nil {
if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil {
return nil, err
}
}
updateBody := make(map[string]any)
if req.Name != nil {
if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil {
s.Log.Errorf("Failed to check kandang group name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check kandang group name")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang group with name %s already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists},
common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists},
); err != nil {
return nil, err
}
if req.LocationId != nil {
updateBody["location_id"] = *req.LocationId
}
if req.PicId != nil {
updateBody["pic_id"] = *req.PicId
}
if req.Status != nil {
status := strings.ToUpper(strings.TrimSpace(*req.Status))
if !utils.IsValidKandangStatus(status) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang group status")
}
updateBody["status"] = status
}
if len(updateBody) == 0 {
return existing, nil
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang group not found")
}
s.Log.Errorf("Failed to update kandang group: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s kandangGroupService) DeleteOne(c *fiber.Ctx, id uint) error {
if _, err := s.GetOne(c, id); err != nil {
return err
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Kandang group not found")
}
s.Log.Errorf("Failed to delete kandang group: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,23 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Status string `json:"status,omitempty" validate:"omitempty,min=3,max=50"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
Status *string `json:"status,omitempty" validate:"omitempty,min=3,max=50"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500"`
Search string `query:"search" validate:"omitempty,max=50"`
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
}
@@ -11,24 +11,32 @@ import (
// === DTO Structs ===
type KandangRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Capacity float64 `json:"capacity"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Capacity float64 `json:"capacity"`
KandangGroup *KandangGroupRelationDTO `json:"kandang_group,omitempty"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
}
type KandangGroupRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
type KandangListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Capacity float64 `json:"capacity"`
Location locationDTO.LocationRelationDTO `json:"location"`
Pic userDTO.UserRelationDTO `json:"pic"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Capacity float64 `json:"capacity"`
KandangGroup KandangGroupRelationDTO `json:"kandang_group"`
Location locationDTO.LocationRelationDTO `json:"location"`
Pic userDTO.UserRelationDTO `json:"pic"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type KandangDetailDTO struct {
@@ -38,6 +46,12 @@ type KandangDetailDTO struct {
// === Mapper Functions ===
func ToKandangRelationDTO(e entity.Kandang) KandangRelationDTO {
var kandangGroup *KandangGroupRelationDTO
if e.KandangGroup.Id != 0 {
mapped := ToKandangGroupRelationDTO(e.KandangGroup)
kandangGroup = &mapped
}
var location *locationDTO.LocationRelationDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(e.Location)
@@ -51,16 +65,31 @@ func ToKandangRelationDTO(e entity.Kandang) KandangRelationDTO {
}
return KandangRelationDTO{
Id: e.Id,
Name: e.Name,
Status: e.Status,
Capacity: e.Capacity,
Location: location,
Pic: pic,
Id: e.Id,
Name: e.Name,
Status: e.Status,
Capacity: e.Capacity,
KandangGroup: kandangGroup,
Location: location,
Pic: pic,
}
}
func ToKandangGroupRelationDTO(e entity.KandangGroup) KandangGroupRelationDTO {
return KandangGroupRelationDTO{
Id: e.Id,
Name: e.Name,
Status: e.Status,
}
}
func ToKandangListDTO(e entity.Kandang) KandangListDTO {
var kandangGroup KandangGroupRelationDTO
if e.KandangGroup.Id != 0 {
mapped := ToKandangGroupRelationDTO(e.KandangGroup)
kandangGroup = mapped
}
var location locationDTO.LocationRelationDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(e.Location)
@@ -80,15 +109,16 @@ func ToKandangListDTO(e entity.Kandang) KandangListDTO {
}
return KandangListDTO{
Id: e.Id,
Name: e.Name,
Status: e.Status,
Location: location,
Capacity: e.Capacity,
Pic: pic,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Id: e.Id,
Name: e.Name,
Status: e.Status,
Location: location,
KandangGroup: kandangGroup,
Capacity: e.Capacity,
Pic: pic,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
@@ -4,10 +4,10 @@ import (
"context"
"errors"
"github.com/gofiber/fiber/v2"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
@@ -15,6 +15,7 @@ type KandangRepository interface {
repository.BaseRepository[entity.Kandang]
LocationExists(ctx context.Context, areaId uint) (bool, error)
PicExists(ctx context.Context, areaId uint) (bool, error)
KandangGroupExists(ctx context.Context, kandangGroupId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
ProjectFlockExists(ctx context.Context, projectFlockID uint) (bool, error)
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error)
@@ -45,6 +46,10 @@ func (r *KandangRepositoryImpl) PicExists(ctx context.Context, picId uint) (bool
return repository.Exists[entity.User](ctx, r.db, picId)
}
func (r *KandangRepositoryImpl) KandangGroupExists(ctx context.Context, kandangGroupId uint) (bool, error) {
return repository.Exists[entity.KandangGroup](ctx, r.db, kandangGroupId)
}
func (r *KandangRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Kandang](ctx, r.db, name, excludeID)
}
@@ -3,13 +3,14 @@ package service
import (
"errors"
"fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -40,7 +41,7 @@ func NewKandangService(repo repository.KandangRepository, validate *validator.Va
}
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").Preload("Location").Preload("Pic").Preload("ProjectFlockKandangs.ProjectFlock")
return db.Preload("CreatedUser").Preload("Location").Preload("KandangGroup").Preload("Pic").Preload("ProjectFlockKandangs.ProjectFlock")
}
@@ -99,6 +100,28 @@ func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) {
return kandang, nil
}
func (s kandangService) ensureKandangGroupAccess(c *fiber.Ctx, groupID uint, expectedLocationID *uint) error {
var kandangGroup entity.KandangGroup
if err := s.Repository.DB().WithContext(c.Context()).
Select("id", "location_id").
First(&kandangGroup, groupID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Kandang group not found")
}
return err
}
if err := m.EnsureLocationAccess(c, s.Repository.DB(), kandangGroup.LocationId); err != nil {
return fiber.NewError(fiber.StatusNotFound, "Kandang group not found")
}
if expectedLocationID != nil && kandangGroup.LocationId != *expectedLocationID {
return fiber.NewError(fiber.StatusBadRequest, "Kandang group location must match kandang location")
}
return nil
}
func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Kandang, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
@@ -116,10 +139,14 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
common.RelationCheck{Name: "KandangGroup", ID: &req.GroupId, Exists: s.Repository.KandangGroupExists},
common.RelationCheck{Name: "Pic", ID: &req.PicId, Exists: s.Repository.PicExists},
); err != nil {
return nil, err
}
if err := s.ensureKandangGroupAccess(c, req.GroupId, &req.LocationId); err != nil {
return nil, err
}
status := strings.ToUpper(strings.TrimSpace(req.Status))
if status == "" {
@@ -154,12 +181,13 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
}
createBody := &entity.Kandang{
Name: req.Name,
LocationId: req.LocationId,
Capacity: req.Capacity,
Status: status,
PicId: req.PicId,
CreatedBy: actorID,
Name: req.Name,
LocationId: req.LocationId,
KandangGroupId: req.GroupId,
Capacity: req.Capacity,
Status: status,
PicId: req.PicId,
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -212,6 +240,7 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists},
common.RelationCheck{Name: "KandangGroup", ID: req.GroupId, Exists: s.Repository.KandangGroupExists},
common.RelationCheck{Name: "Pic", ID: req.PicId, Exists: s.Repository.PicExists},
); err != nil {
return nil, err
@@ -220,6 +249,16 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if req.LocationId != nil {
updateBody["location_id"] = *req.LocationId
}
if req.GroupId != nil {
targetLocationID := existing.LocationId
if req.LocationId != nil {
targetLocationID = *req.LocationId
}
if err := s.ensureKandangGroupAccess(c, *req.GroupId, &targetLocationID); err != nil {
return nil, err
}
updateBody["kandang_group_id"] = *req.GroupId
}
if req.PicId != nil {
updateBody["pic_id"] = *req.PicId
@@ -5,6 +5,7 @@ type Create struct {
Status string `json:"status,omitempty" validate:"omitempty,min=3,max=50"`
Capacity float64 `json:"capacity" validate:"required_strict,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
GroupId uint `json:"group_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"`
}
@@ -14,6 +15,7 @@ type Update struct {
Status *string `json:"status,omitempty" validate:"omitempty,min=3,max=50"`
Capacity *float64 `json:"capacity" validate:"omitempty,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
GroupId *uint `json:"group_id" validate:"required_strict,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"`
}
+3 -1
View File
@@ -9,10 +9,12 @@ import (
areas "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas"
banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks"
configChecklists "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists"
customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers"
employeess "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees"
fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs"
flocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks"
kandanggroups "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandang-groups"
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks"
@@ -24,7 +26,6 @@ import (
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
configChecklists "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists"
// MODULE IMPORTS
)
@@ -35,6 +36,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
uoms.UomModule{},
areas.AreaModule{},
locations.LocationModule{},
kandanggroups.KandangGroupModule{},
kandangs.KandangModule{},
warehouses.WarehouseModule{},
customers.CustomerModule{},