diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql new file mode 100644 index 00000000..6ad77820 --- /dev/null +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS laying_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql new file mode 100644 index 00000000..8677ca71 --- /dev/null +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -0,0 +1,42 @@ +CREATE TABLE IF NOT EXISTS laying_transfers ( + id BIGSERIAL PRIMARY KEY, + from_project_flock_id BIGINT NOT NULL, + to_project_flock_id BIGINT NOT NULL, + total_quantity INTEGER, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ, + created_by BIGINT +); + +-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_from_project_flock + FOREIGN KEY (from_project_flock_id) + REFERENCES project_flocks(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_to_project_flock + FOREIGN KEY (to_project_flock_id) + REFERENCES project_flocks(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- INDEXES +CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers(from_project_flock_id); +CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers(to_project_flock_id); +CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers(created_by); +CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers(deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql new file mode 100644 index 00000000..caf4f52d --- /dev/null +++ b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS laying_kandang_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql new file mode 100644 index 00000000..786f8de3 --- /dev/null +++ b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql @@ -0,0 +1,40 @@ +CREATE TABLE IF NOT EXISTS laying_kandang_transfers ( + id BIGSERIAL PRIMARY KEY, + kandang_id BIGINT, + product_warehouse_id BIGINT, + qty NUMERIC(15,3), + laying_transfer_id BIGINT NOT NULL +); + +-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN + ALTER TABLE laying_kandang_transfers + ADD CONSTRAINT fk_laying_kandang_transfers_kandang + FOREIGN KEY (kandang_id) + REFERENCES kandangs(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE laying_kandang_transfers + ADD CONSTRAINT fk_laying_kandang_transfers_product_warehouse + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN + ALTER TABLE laying_kandang_transfers + ADD CONSTRAINT fk_laying_kandang_transfers_laying_transfer + FOREIGN KEY (laying_transfer_id) + REFERENCES laying_transfers(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- INDEXES +CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_kandang_id ON laying_kandang_transfers(kandang_id); +CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_product_warehouse_id ON laying_kandang_transfers(product_warehouse_id); +CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_laying_transfer_id ON laying_kandang_transfers(laying_transfer_id); \ No newline at end of file diff --git a/internal/entities/transfer_laying.go b/internal/entities/transfer_laying.go new file mode 100644 index 00000000..fd60a435 --- /dev/null +++ b/internal/entities/transfer_laying.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type TransferLaying struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS 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"` +} diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index b41ef1e7..10effb58 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -10,6 +10,7 @@ import ( chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins" projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" + transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings" // MODULE IMPORTS ) @@ -20,8 +21,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida projectflocks.ProjectflockModule{}, recordings.RecordingModule{}, chickins.ChickinModule{}, + transferLayings.TransferLayingModule{}, // MODULE REGISTRY - } +} for _, m := range allModules { m.RegisterRoutes(group, db, validate) diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go new file mode 100644 index 00000000..01f49867 --- /dev/null +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type TransferLayingController struct { + TransferLayingService service.TransferLayingService +} + +func NewTransferLayingController(transferLayingService service.TransferLayingService) *TransferLayingController { + return &TransferLayingController{ + TransferLayingService: transferLayingService, + } +} + +func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.TransferLayingService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all transferLayings successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToTransferLayingListDTOs(result), + }) +} + +func (u *TransferLayingController) 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.TransferLayingService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get transferLaying successfully", + Data: dto.ToTransferLayingListDTO(*result), + }) +} + +func (u *TransferLayingController) 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.TransferLayingService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create transferLaying successfully", + Data: dto.ToTransferLayingListDTO(*result), + }) +} + +func (u *TransferLayingController) 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.TransferLayingService.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 transferLaying successfully", + Data: dto.ToTransferLayingListDTO(*result), + }) +} + +func (u *TransferLayingController) 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.TransferLayingService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete transferLaying successfully", + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go new file mode 100644 index 00000000..103c4c35 --- /dev/null +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -0,0 +1,64 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type TransferLayingBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type TransferLayingListDTO struct { + TransferLayingBaseDTO + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TransferLayingDetailDTO struct { + TransferLayingListDTO +} + +// === Mapper Functions === + +func ToTransferLayingBaseDTO(e entity.TransferLaying) TransferLayingBaseDTO { + return TransferLayingBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToTransferLayingListDTO(e entity.TransferLaying) TransferLayingListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + return TransferLayingListDTO{ + TransferLayingBaseDTO: ToTransferLayingBaseDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToTransferLayingListDTOs(e []entity.TransferLaying) []TransferLayingListDTO { + result := make([]TransferLayingListDTO, len(e)) + for i, r := range e { + result[i] = ToTransferLayingListDTO(r) + } + return result +} + +func ToTransferLayingDetailDTO(e entity.TransferLaying) TransferLayingDetailDTO { + return TransferLayingDetailDTO{ + TransferLayingListDTO: ToTransferLayingListDTO(e), + } +} diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go new file mode 100644 index 00000000..d3c6279c --- /dev/null +++ b/internal/modules/production/transfer_layings/module.go @@ -0,0 +1,26 @@ +package transfer_layings + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" + sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type TransferLayingModule struct{} + +func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db) + userRepo := rUser.NewUserRepository(db) + + transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + TransferLayingRoutes(router, userService, transferLayingService) +} + diff --git a/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go b/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go new file mode 100644 index 00000000..32ea27ee --- /dev/null +++ b/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type TransferLayingRepository interface { + repository.BaseRepository[entity.TransferLaying] +} + +type TransferLayingRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.TransferLaying] +} + +func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { + return &TransferLayingRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.TransferLaying](db), + } +} diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go new file mode 100644 index 00000000..ee806506 --- /dev/null +++ b/internal/modules/production/transfer_layings/route.go @@ -0,0 +1,28 @@ +package transfer_layings + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/controllers" + transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.TransferLayingService) { + ctrl := controller.NewTransferLayingController(s) + + route := v1.Group("/transfer_layings") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go new file mode 100644 index 00000000..36541600 --- /dev/null +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -0,0 +1,129 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type TransferLayingService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.TransferLaying, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type transferLayingService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.TransferLayingRepository +} + +func NewTransferLayingService(repo repository.TransferLayingRepository, validate *validator.Validate) TransferLayingService { + return &transferLayingService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + +func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.Search != "" { + return db.Where("name LIKE ?", "%"+params.Search+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get transferLayings: %+v", err) + return nil, 0, err + } + return transferLayings, total, nil +} + +func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.TransferLaying, error) { + transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + if err != nil { + s.Log.Errorf("Failed get transferLaying by id: %+v", err) + return nil, err + } + return transferLaying, nil +} + +func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + createBody := &entity.TransferLaying{ + Name: req.Name, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create transferLaying: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.Name != nil { + updateBody["name"] = *req.Name + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + s.Log.Errorf("Failed to update transferLaying: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + s.Log.Errorf("Failed to delete transferLaying: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go new file mode 100644 index 00000000..7d16d3ee --- /dev/null +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` +}