From df504e3ff03cc351788ce2d571babbc31047f3c7 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Mon, 5 Jan 2026 17:17:25 +0700 Subject: [PATCH] add migration;add api create employee --- ...44_create_daily_checklists_tables.down.sql | 12 + ...1644_create_daily_checklists_tables.up.sql | 194 ++++++++++++++++ internal/entities/employee.go | 26 +++ internal/entities/phase.go | 41 ++++ .../controllers/employees.controller.go | 144 ++++++++++++ .../master/employees/dto/employees.dto.go | 70 ++++++ internal/modules/master/employees/module.go | 25 +++ .../repositories/employees.repository.go | 21 ++ internal/modules/master/employees/route.go | 23 ++ .../employees/services/employees.service.go | 209 ++++++++++++++++++ .../validations/employees.validation.go | 19 ++ internal/modules/master/route.go | 4 +- 12 files changed, 787 insertions(+), 1 deletion(-) create mode 100644 internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql create mode 100644 internal/database/migrations/20260105131644_create_daily_checklists_tables.up.sql create mode 100644 internal/entities/employee.go create mode 100644 internal/entities/phase.go create mode 100644 internal/modules/master/employees/controllers/employees.controller.go create mode 100644 internal/modules/master/employees/dto/employees.dto.go create mode 100644 internal/modules/master/employees/module.go create mode 100644 internal/modules/master/employees/repositories/employees.repository.go create mode 100644 internal/modules/master/employees/route.go create mode 100644 internal/modules/master/employees/services/employees.service.go create mode 100644 internal/modules/master/employees/validations/employees.validation.go diff --git a/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql b/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql new file mode 100644 index 00000000..7be30be1 --- /dev/null +++ b/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS daily_checklist_tasks; +DROP TABLE IF EXISTS daily_checklist_activity_task_assignees; +DROP TABLE IF EXISTS daily_checklist_activity_tasks; +DROP TABLE IF EXISTS daily_checklist_phases; +DROP TABLE IF EXISTS daily_checklists; +DROP TABLE IF EXISTS checklists; +DROP TABLE IF EXISTS phase_activities; +DROP TABLE IF EXISTS phases; +DROP TABLE IF EXISTS employee_kandangs; +DROP TABLE IF EXISTS employees; + +DROP TYPE IF EXISTS category_code; diff --git a/internal/database/migrations/20260105131644_create_daily_checklists_tables.up.sql b/internal/database/migrations/20260105131644_create_daily_checklists_tables.up.sql new file mode 100644 index 00000000..6074fa8c --- /dev/null +++ b/internal/database/migrations/20260105131644_create_daily_checklists_tables.up.sql @@ -0,0 +1,194 @@ +CREATE TYPE category_code AS ENUM ( + 'pullet_open', + 'pullet_close', + 'produksi_open', + 'produksi_close' +); + +-- MASTER TABLES + +CREATE TABLE employees ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar NOT NULL, + is_active boolean NOT NULL DEFAULT true, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE employee_kandangs ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + employee_id bigint NOT NULL, + kandang_id bigint NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_employee_kandangs_employee + FOREIGN KEY (employee_id) REFERENCES employees(id) + ON DELETE CASCADE, + + CONSTRAINT fk_employee_kandangs_kandang + FOREIGN KEY (kandang_id) REFERENCES kandangs(id) + ON DELETE CASCADE, + + CONSTRAINT uq_employee_kandangs UNIQUE (employee_id, kandang_id) +); + +-- PHASE & CHECKLIST + +CREATE TABLE phases ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar NOT NULL, + is_active boolean NOT NULL DEFAULT true, + category category_code NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE phase_activities ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + phase_id bigint NOT NULL, + name varchar NOT NULL, + description text, + time_type text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_phase_activities_phase + FOREIGN KEY (phase_id) REFERENCES phases(id) + ON DELETE CASCADE +); + +CREATE TABLE checklists ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar NOT NULL, + description text, + phase_id bigint, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz, + + CONSTRAINT fk_checklists_phase + FOREIGN KEY (phase_id) REFERENCES phases(id) + ON DELETE SET NULL +); + + +-- DAILY CHECKLISTS +CREATE TABLE daily_checklists ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + kandang_id bigint NOT NULL, + checklist_id bigint NOT NULL, + date date NOT NULL, + name varchar, + status varchar, + category category_code NOT NULL, + total_score integer, + document_path varchar, + reject_reason text, + created_by bigint, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_daily_checklists_kandang + FOREIGN KEY (kandang_id) REFERENCES kandangs(id) + ON DELETE CASCADE, + + CONSTRAINT fk_daily_checklists_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) + ON DELETE RESTRICT, + + CONSTRAINT fk_daily_checklists_created_by + FOREIGN KEY (created_by) REFERENCES users(id) + ON DELETE SET NULL +); + + +--RELASI CHECKLIST ⇄ PHASE + +CREATE TABLE daily_checklist_phases ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + checklist_id bigint NOT NULL, + phase_id bigint NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_dcp_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dcp_phase + FOREIGN KEY (phase_id) REFERENCES phases(id) + ON DELETE CASCADE, + + CONSTRAINT uq_daily_checklist_phases UNIQUE (checklist_id, phase_id) +); + + +--ACTIVITY TASKS & ASSIGNMENT + + +CREATE TABLE daily_checklist_activity_tasks ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + checklist_id bigint NOT NULL, + phase_id bigint NOT NULL, + phase_activity_id bigint NOT NULL, + time_type text, + notes text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_dcat_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dcat_phase + FOREIGN KEY (phase_id) REFERENCES phases(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dcat_phase_activity + FOREIGN KEY (phase_activity_id) REFERENCES phase_activities(id) + ON DELETE CASCADE +); + +CREATE TABLE daily_checklist_activity_task_assignments ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + task_id bigint NOT NULL, + employee_id bigint NOT NULL, + checked boolean NOT NULL DEFAULT false, + note text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_assignment_task + FOREIGN KEY (task_id) REFERENCES daily_checklist_activity_tasks(id) + ON DELETE CASCADE, + + CONSTRAINT fk_assignment_employee + FOREIGN KEY (employee_id) REFERENCES employees(id) + ON DELETE CASCADE +); + +--DAILY CHECKLIST TASK RESULT +CREATE TABLE daily_checklist_tasks ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + daily_checklist_id bigint NOT NULL, + checklist_id bigint NOT NULL, + checklist_item_id bigint, + is_completed boolean NOT NULL DEFAULT false, + score_value integer, + notes text, + photo_proof varchar, + status varchar, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT fk_dct_daily + FOREIGN KEY (daily_checklist_id) REFERENCES daily_checklists(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dct_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) + ON DELETE CASCADE, + + CONSTRAINT fk_dct_checklist_item + FOREIGN KEY (checklist_item_id) REFERENCES phase_activities(id) + ON DELETE SET NULL +); diff --git a/internal/entities/employee.go b/internal/entities/employee.go new file mode 100644 index 00000000..5810c6ee --- /dev/null +++ b/internal/entities/employee.go @@ -0,0 +1,26 @@ +package entities + +import "time" + +type Employee struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + IsActive bool `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + EmployeeKandangs []EmployeeKandang `gorm:"foreignKey:EmployeeId;references:Id"` +} + +type Employees = Employee + +type EmployeeKandang struct { + Id uint `gorm:"primaryKey"` + EmployeeId uint `gorm:"not null"` + KandangId uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Employee Employee `gorm:"foreignKey:EmployeeId;references:Id"` + Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` +} diff --git a/internal/entities/phase.go b/internal/entities/phase.go new file mode 100644 index 00000000..4ee80804 --- /dev/null +++ b/internal/entities/phase.go @@ -0,0 +1,41 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type Phase struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + IsActive bool `gorm:"not null;default:true"` + Category string `gorm:"type:category_code;not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + + Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"` +} + +type PhaseActivity struct { + Id uint `gorm:"primaryKey"` + PhaseId uint `gorm:"not null"` + Name string `gorm:"not null"` + Description *string `gorm:"type:text"` + TimeType *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Phase Phase `gorm:"foreignKey:PhaseId;references:Id"` +} + +type Checklist struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + Description *string `gorm:"type:text"` + PhaseId *uint + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Phase *Phase `gorm:"foreignKey:PhaseId;references:Id"` +} diff --git a/internal/modules/master/employees/controllers/employees.controller.go b/internal/modules/master/employees/controllers/employees.controller.go new file mode 100644 index 00000000..6be28200 --- /dev/null +++ b/internal/modules/master/employees/controllers/employees.controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type EmployeesController struct { + EmployeesService service.EmployeesService +} + +func NewEmployeesController(employeesService service.EmployeesService) *EmployeesController { + return &EmployeesController{ + EmployeesService: employeesService, + } +} + +func (u *EmployeesController) 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.EmployeesService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.EmployeesListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all employeess successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToEmployeesListDTOs(result), + }) +} + +func (u *EmployeesController) 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.EmployeesService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get employees successfully", + Data: dto.ToEmployeesListDTO(*result), + }) +} + +func (u *EmployeesController) 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.EmployeesService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create employees successfully", + Data: dto.ToEmployeesListDTO(*result), + }) +} + +func (u *EmployeesController) 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.EmployeesService.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 employees successfully", + Data: dto.ToEmployeesListDTO(*result), + }) +} + +func (u *EmployeesController) 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.EmployeesService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete employees successfully", + }) +} diff --git a/internal/modules/master/employees/dto/employees.dto.go b/internal/modules/master/employees/dto/employees.dto.go new file mode 100644 index 00000000..65b1b5ca --- /dev/null +++ b/internal/modules/master/employees/dto/employees.dto.go @@ -0,0 +1,70 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" +) + +// === DTO Structs === + +type EmployeesRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type EmployeesListDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + IsActive bool `json:"is_active"` + Kandangs []kandangDTO.KandangRelationDTO `json:"kandangs"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type EmployeesDetailDTO struct { + EmployeesListDTO +} + +// === Mapper Functions === + +func ToEmployeesRelationDTO(e entity.Employees) EmployeesRelationDTO { + return EmployeesRelationDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToEmployeesListDTO(e entity.Employees) EmployeesListDTO { + kandangs := make([]kandangDTO.KandangRelationDTO, 0, len(e.EmployeeKandangs)) + for _, rel := range e.EmployeeKandangs { + if rel.Kandang.Id == 0 { + continue + } + kandangs = append(kandangs, kandangDTO.ToKandangRelationDTO(rel.Kandang)) + } + + return EmployeesListDTO{ + Id: e.Id, + Name: e.Name, + IsActive: e.IsActive, + Kandangs: kandangs, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } +} + +func ToEmployeesListDTOs(e []entity.Employees) []EmployeesListDTO { + result := make([]EmployeesListDTO, len(e)) + for i, r := range e { + result[i] = ToEmployeesListDTO(r) + } + return result +} + +func ToEmployeesDetailDTO(e entity.Employees) EmployeesDetailDTO { + return EmployeesDetailDTO{ + EmployeesListDTO: ToEmployeesListDTO(e), + } +} diff --git a/internal/modules/master/employees/module.go b/internal/modules/master/employees/module.go new file mode 100644 index 00000000..a916ced6 --- /dev/null +++ b/internal/modules/master/employees/module.go @@ -0,0 +1,25 @@ +package employeess + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rEmployees "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/repositories" + sEmployees "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type EmployeesModule struct{} + +func (EmployeesModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + employeesRepo := rEmployees.NewEmployeesRepository(db) + userRepo := rUser.NewUserRepository(db) + + employeesService := sEmployees.NewEmployeesService(employeesRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + EmployeesRoutes(router, userService, employeesService) +} diff --git a/internal/modules/master/employees/repositories/employees.repository.go b/internal/modules/master/employees/repositories/employees.repository.go new file mode 100644 index 00000000..f10a5884 --- /dev/null +++ b/internal/modules/master/employees/repositories/employees.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 EmployeesRepository interface { + repository.BaseRepository[entity.Employees] +} + +type EmployeesRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.Employees] +} + +func NewEmployeesRepository(db *gorm.DB) EmployeesRepository { + return &EmployeesRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.Employees](db), + } +} diff --git a/internal/modules/master/employees/route.go b/internal/modules/master/employees/route.go new file mode 100644 index 00000000..53974814 --- /dev/null +++ b/internal/modules/master/employees/route.go @@ -0,0 +1,23 @@ +package employeess + +import ( + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/controllers" + employees "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func EmployeesRoutes(v1 fiber.Router, u user.UserService, s employees.EmployeesService) { + ctrl := controller.NewEmployeesController(s) + + route := v1.Group("/employees") + route.Use(m.Auth(u)) + + 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/master/employees/services/employees.service.go b/internal/modules/master/employees/services/employees.service.go new file mode 100644 index 00000000..c17f941a --- /dev/null +++ b/internal/modules/master/employees/services/employees.service.go @@ -0,0 +1,209 @@ +package service + +import ( + "errors" + "fmt" + "strconv" + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/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 EmployeesService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.Employees, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Employees, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Employees, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type employeesService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.EmployeesRepository +} + +func NewEmployeesService(repo repository.EmployeesRepository, validate *validator.Validate) EmployeesService { + return &employeesService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s employeesService) withRelations(db *gorm.DB) *gorm.DB { + return db. + Preload("EmployeeKandangs.Kandang"). + Preload("EmployeeKandangs.Kandang.Location"). + Preload("EmployeeKandangs.Kandang.Pic"). + Preload("EmployeeKandangs.Kandang.CreatedUser") +} + +func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + employeess, 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 employeess: %+v", err) + return nil, 0, err + } + return employeess, total, nil +} + +func (s employeesService) GetOne(c *fiber.Ctx, id uint) (*entity.Employees, error) { + employees, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Employees not found") + } + if err != nil { + s.Log.Errorf("Failed get employees by id: %+v", err) + return nil, err + } + return employees, nil +} + +func (s *employeesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Employees, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + name := strings.TrimSpace(req.Name) + if name == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty") + } + + kandangIDs, err := parseKandangIDs(req.KandangIDs) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("LOWER(name) = ?", strings.ToLower(name)) + }); err == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "employee already exists") + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed checking employee uniqueness: %+v", err) + return nil, err + } + + createBody := &entity.Employees{ + Name: name, + IsActive: req.IsActive, + } + + if err := s.Repository.DB().Transaction(func(tx *gorm.DB) error { + repoTx := s.Repository.WithTx(tx) + + if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil { + return err + } + + relations := make([]entity.EmployeeKandang, 0, len(kandangIDs)) + for _, kandangID := range kandangIDs { + relations = append(relations, entity.EmployeeKandang{ + EmployeeId: createBody.Id, + KandangId: kandangID, + }) + } + + if len(relations) > 0 { + if err := tx.WithContext(c.Context()).Create(&relations).Error; err != nil { + return err + } + } + + return nil + }); err != nil { + s.Log.Errorf("Failed to create employees: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Employees, 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, "Employees not found") + } + s.Log.Errorf("Failed to update employees: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s employeesService) 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, "Employees not found") + } + s.Log.Errorf("Failed to delete employees: %+v", err) + return err + } + return nil +} + +func parseKandangIDs(raw string) ([]uint, error) { + parts := strings.Split(raw, ",") + ids := make([]uint, 0, len(parts)) + seen := make(map[uint]struct{}) + + for _, part := range parts { + value := strings.TrimSpace(part) + if value == "" { + continue + } + + parsed, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid kandang id: %s", value) + } + + id := uint(parsed) + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + ids = append(ids, id) + } + + if len(ids) == 0 { + return nil, errors.New("kandang_ids must contain at least one valid id") + } + + return ids, nil +} diff --git a/internal/modules/master/employees/validations/employees.validation.go b/internal/modules/master/employees/validations/employees.validation.go new file mode 100644 index 00000000..4449bfcc --- /dev/null +++ b/internal/modules/master/employees/validations/employees.validation.go @@ -0,0 +1,19 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` + KandangIDs string `json:"kandang_ids" validate:"required"` + IsActive bool `json:"is_active"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` + KandangIDs *string `json:"kandang_ids,omitempty"` + IsActive *bool `json:"is_active,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"` +} diff --git a/internal/modules/master/route.go b/internal/modules/master/route.go index 26ae28ee..2965baae 100644 --- a/internal/modules/master/route.go +++ b/internal/modules/master/route.go @@ -10,17 +10,18 @@ import ( areas "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas" banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks" 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" 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" productcategories "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories" + productionStandards "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards" products "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products" 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" - productionStandards "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards" // MODULE IMPORTS ) @@ -42,6 +43,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida banks.BankModule{}, flocks.FlockModule{}, productionStandards.ProductionStandardModule{}, + employeess.EmployeesModule{}, // MODULE REGISTRY }