diff --git a/internal/database/migrations/20260106101434_add_unique_daily_checklists.down.sql b/internal/database/migrations/20260106101434_add_unique_daily_checklists.down.sql new file mode 100644 index 00000000..f33ea629 --- /dev/null +++ b/internal/database/migrations/20260106101434_add_unique_daily_checklists.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE daily_checklists + DROP CONSTRAINT IF EXISTS daily_checklists_date_kandang_category_key; diff --git a/internal/database/migrations/20260106101434_add_unique_daily_checklists.up.sql b/internal/database/migrations/20260106101434_add_unique_daily_checklists.up.sql new file mode 100644 index 00000000..6566083b --- /dev/null +++ b/internal/database/migrations/20260106101434_add_unique_daily_checklists.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE daily_checklists + ADD CONSTRAINT daily_checklists_date_kandang_category_key + UNIQUE (date, kandang_id, category); diff --git a/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.down.sql b/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.down.sql new file mode 100644 index 00000000..a1095689 --- /dev/null +++ b/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE daily_checklists + ALTER COLUMN checklist_id SET NOT NULL; diff --git a/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.up.sql b/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.up.sql new file mode 100644 index 00000000..2f804e4b --- /dev/null +++ b/internal/database/migrations/20260106102657_alter_daily_checklists_checklist_id_nullable.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE daily_checklists + ALTER COLUMN checklist_id DROP NOT NULL; diff --git a/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.down.sql b/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.down.sql new file mode 100644 index 00000000..e2b34f4e --- /dev/null +++ b/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE daily_checklist_phases + DROP CONSTRAINT IF EXISTS fk_dcp_daily_checklist, + ADD CONSTRAINT fk_dcp_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) ON DELETE CASCADE; diff --git a/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.up.sql b/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.up.sql new file mode 100644 index 00000000..5f4384b4 --- /dev/null +++ b/internal/database/migrations/20260106111217_update_daily_checklist_phases_fk.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE daily_checklist_phases + DROP CONSTRAINT IF EXISTS fk_dcp_checklist, + ADD CONSTRAINT fk_dcp_daily_checklist + FOREIGN KEY (checklist_id) REFERENCES daily_checklists(id) ON DELETE CASCADE; diff --git a/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.down.sql b/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.down.sql new file mode 100644 index 00000000..e37f1ad0 --- /dev/null +++ b/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE daily_checklist_activity_tasks + DROP CONSTRAINT IF EXISTS fk_dcat_daily_checklist, + ADD CONSTRAINT fk_dcat_checklist + FOREIGN KEY (checklist_id) REFERENCES checklists(id) ON DELETE CASCADE; diff --git a/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.up.sql b/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.up.sql new file mode 100644 index 00000000..337ea821 --- /dev/null +++ b/internal/database/migrations/20260106113936_update_daily_checklist_activity_task_fk.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE daily_checklist_activity_tasks + DROP CONSTRAINT IF EXISTS fk_dcat_checklist, + ADD CONSTRAINT fk_dcat_daily_checklist + FOREIGN KEY (checklist_id) REFERENCES daily_checklists(id) ON DELETE CASCADE; diff --git a/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.down.sql b/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.down.sql new file mode 100644 index 00000000..921645e0 --- /dev/null +++ b/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE daily_checklist_activity_task_assignments + DROP CONSTRAINT IF EXISTS daily_checklist_activity_task_assignments_task_employee_key; diff --git a/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.up.sql b/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.up.sql new file mode 100644 index 00000000..b4fd9e18 --- /dev/null +++ b/internal/database/migrations/20260106150814_add_unique_activity_task_assignments.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE daily_checklist_activity_task_assignments + ADD CONSTRAINT daily_checklist_activity_task_assignments_task_employee_key + UNIQUE (task_id, employee_id); diff --git a/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.down.sql b/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.down.sql new file mode 100644 index 00000000..fb17404d --- /dev/null +++ b/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE phase_activities + DROP COLUMN IF EXISTS deleted_at; + +ALTER TABLE phases + DROP COLUMN IF EXISTS deleted_at; + +ALTER TABLE employees + DROP COLUMN IF EXISTS deleted_at; diff --git a/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.up.sql b/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.up.sql new file mode 100644 index 00000000..0fdf6531 --- /dev/null +++ b/internal/database/migrations/20260106164640_add_deleted_at_to_master_tables.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE employees + ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +ALTER TABLE phases + ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +ALTER TABLE phase_activities + ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; diff --git a/internal/entities/daily-checklist.go b/internal/entities/daily-checklist.go new file mode 100644 index 00000000..8b62b1a3 --- /dev/null +++ b/internal/entities/daily-checklist.go @@ -0,0 +1,81 @@ +package entities + +import "time" + +type DailyChecklist struct { + Id uint `gorm:"primaryKey"` + KandangId uint `gorm:"not null"` + ChecklistId *uint + Date time.Time `gorm:"type:date;not null"` + Name *string `gorm:"type:varchar(255)"` + Status *string `gorm:"type:varchar(255)"` + Category string `gorm:"type:category_code;not null"` + TotalScore *int + DocumentPath *string + RejectReason *string + CreatedBy *uint + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Kandang Kandang `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"` +} + +type DailyChecklistPhase struct { + Id uint `gorm:"primaryKey"` + ChecklistId uint `gorm:"not null"` + PhaseId uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + + Checklist Checklist `gorm:"foreignKey:ChecklistId;references:Id"` + Phase Phases `gorm:"foreignKey:PhaseId;references:Id"` +} + +type DailyChecklistActivityTask struct { + Id uint `gorm:"primaryKey"` + ChecklistId uint `gorm:"not null"` + PhaseId uint `gorm:"not null"` + PhaseActivityId uint `gorm:"not null"` + TimeType *string `gorm:"type:text"` + Notes *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Checklist DailyChecklist `gorm:"foreignKey:ChecklistId;references:Id"` + Phase Phases `gorm:"foreignKey:PhaseId;references:Id"` + PhaseActivity PhaseActivity `gorm:"foreignKey:PhaseActivityId;references:Id"` + Assignments []DailyChecklistActivityTaskAssignment `gorm:"foreignKey:TaskId;references:Id"` +} + +type DailyChecklistActivityTaskAssignment struct { + Id uint `gorm:"primaryKey"` + TaskId uint `gorm:"not null"` + EmployeeId uint `gorm:"not null"` + Checked bool `gorm:"not null;default:false"` + Note *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + Task DailyChecklistActivityTask `gorm:"foreignKey:TaskId;references:Id"` + Employee Employee `gorm:"foreignKey:EmployeeId;references:Id"` +} + +type DailyChecklistTask struct { + Id uint `gorm:"primaryKey"` + DailyChecklistId uint `gorm:"not null"` + ChecklistId uint `gorm:"not null"` + ChecklistItemId *uint + IsCompleted bool `gorm:"not null;default:false"` + ScoreValue *int + Notes *string `gorm:"type:text"` + PhotoProof *string + Status *string + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + DailyChecklist *DailyChecklist `gorm:"foreignKey:DailyChecklistId;references:Id"` + Checklist Checklist `gorm:"foreignKey:ChecklistId;references:Id"` + ChecklistItem *PhaseActivity `gorm:"foreignKey:ChecklistItemId;references:Id"` +} diff --git a/internal/entities/employee.go b/internal/entities/employee.go index 5810c6ee..a93cbb46 100644 --- a/internal/entities/employee.go +++ b/internal/entities/employee.go @@ -1,13 +1,18 @@ package entities -import "time" +import ( + "time" + + "gorm.io/gorm" +) 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"` + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + IsActive bool `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` EmployeeKandangs []EmployeeKandang `gorm:"foreignKey:EmployeeId;references:Id"` } diff --git a/internal/entities/phase.go b/internal/entities/phase.go index d30369eb..178ed695 100644 --- a/internal/entities/phase.go +++ b/internal/entities/phase.go @@ -7,25 +7,27 @@ import ( ) type Phases 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"` + 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"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` 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"` + 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"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Phase Phase `gorm:"foreignKey:PhaseId;references:Id"` + Phase Phases `gorm:"foreignKey:PhaseId;references:Id"` } type Checklist struct { @@ -37,5 +39,5 @@ type Checklist struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Phase *Phase `gorm:"foreignKey:PhaseId;references:Id"` + Phase *Phases `gorm:"foreignKey:PhaseId;references:Id"` } diff --git a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go new file mode 100644 index 00000000..b5a9b7b5 --- /dev/null +++ b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go @@ -0,0 +1,243 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type DailyChecklistController struct { + DailyChecklistService service.DailyChecklistService +} + +func NewDailyChecklistController(dailyChecklistService service.DailyChecklistService) *DailyChecklistController { + return &DailyChecklistController{ + DailyChecklistService: dailyChecklistService, + } +} + +func (u *DailyChecklistController) 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.DailyChecklistService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.DailyChecklistListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all dailyChecklists successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToDailyChecklistListDTOs(result), + }) +} + +func (u *DailyChecklistController) 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.DailyChecklistService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get dailyChecklist successfully", + Data: dto.ToDailyChecklistListDTO(*result), + }) +} + +func (u *DailyChecklistController) 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.DailyChecklistService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create dailyChecklist successfully", + Data: dto.ToDailyChecklistListDTO(*result), + }) +} + +func (u *DailyChecklistController) 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.DailyChecklistService.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 dailyChecklist successfully", + Data: dto.ToDailyChecklistListDTO(*result), + }) +} + +func (u *DailyChecklistController) 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.DailyChecklistService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete dailyChecklist successfully", + }) +} + +func (u *DailyChecklistController) CreateDailyChecklistPhase(c *fiber.Ctx) error { + param := c.Params("idDailyChecklist") + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id") + } + + req := new(validation.AssignPhases) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if err := u.DailyChecklistService.AssignPhases(c, uint(id), req); err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Daily checklist phases saved successfully", + }) +} + +func (u *DailyChecklistController) CreateAssignment(c *fiber.Ctx) error { + param := c.Params("idDailyChecklist") + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id") + } + + req := new(validation.AssignTask) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if err := u.DailyChecklistService.AssignTasks(c, uint(id), req); err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Daily checklist assignments saved successfully", + }) +} + +func (u *DailyChecklistController) RemoveAssignment(c *fiber.Ctx) error { + dailyChecklistParam := c.Params("idDailyChecklist") + employeeParam := c.Params("idEmployee") + + dailyChecklistID, err := strconv.Atoi(dailyChecklistParam) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id") + } + + employeeID, err := strconv.Atoi(employeeParam) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id") + } + + if err := u.DailyChecklistService.RemoveAssignment(c, uint(dailyChecklistID), uint(employeeID)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Assignment removed successfully", + }) +} + +func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error { + checklistParam := c.Query("checklist_id", "") + if checklistParam == "" { + return fiber.NewError(fiber.StatusBadRequest, "checklist_id is required") + } + + checklistID, err := strconv.Atoi(checklistParam) + if err != nil || checklistID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid checklist_id") + } + + result, err := u.DailyChecklistService.GetTasks(c, uint(checklistID)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get daily checklist tasks successfully", + Data: result, + }) +} diff --git a/internal/modules/daily-checklists/dto/daily-checklist.dto.go b/internal/modules/daily-checklists/dto/daily-checklist.dto.go new file mode 100644 index 00000000..31953def --- /dev/null +++ b/internal/modules/daily-checklists/dto/daily-checklist.dto.go @@ -0,0 +1,76 @@ +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 DailyChecklistRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type DailyChecklistListDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type DailyChecklistDetailDTO struct { + DailyChecklistListDTO +} + +// === Mapper Functions === + +func ToDailyChecklistRelationDTO(e entity.DailyChecklist) DailyChecklistRelationDTO { + var name string + if e.Name != nil { + name = *e.Name + } + + return DailyChecklistRelationDTO{ + Id: e.Id, + Name: name, + } +} + +func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO { + var createdUser *userDTO.UserRelationDTO + // if e.CreatedUser.Id != 0 { + // mapped := userDTO.ToUserRelationDTO(e.CreatedUser) + // createdUser = &mapped + // } + + var name string + if e.Name != nil { + name = *e.Name + } + + return DailyChecklistListDTO{ + Id: e.Id, + Name: name, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToDailyChecklistListDTOs(e []entity.DailyChecklist) []DailyChecklistListDTO { + result := make([]DailyChecklistListDTO, len(e)) + for i, r := range e { + result[i] = ToDailyChecklistListDTO(r) + } + return result +} + +func ToDailyChecklistDetailDTO(e entity.DailyChecklist) DailyChecklistDetailDTO { + return DailyChecklistDetailDTO{ + DailyChecklistListDTO: ToDailyChecklistListDTO(e), + } +} diff --git a/internal/modules/daily-checklists/module.go b/internal/modules/daily-checklists/module.go new file mode 100644 index 00000000..bc82d5f6 --- /dev/null +++ b/internal/modules/daily-checklists/module.go @@ -0,0 +1,27 @@ +package dailyChecklists + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" + sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services" + rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type DailyChecklistModule struct{} + +func (DailyChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db) + phasesRepo := rPhases.NewPhasesRepository(db) + userRepo := rUser.NewUserRepository(db) + + dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + DailyChecklistRoutes(router, userService, dailyChecklistService) +} diff --git a/internal/modules/daily-checklists/repositories/daily-checklist.repository.go b/internal/modules/daily-checklists/repositories/daily-checklist.repository.go new file mode 100644 index 00000000..e653ba3b --- /dev/null +++ b/internal/modules/daily-checklists/repositories/daily-checklist.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 DailyChecklistRepository interface { + repository.BaseRepository[entity.DailyChecklist] +} + +type DailyChecklistRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.DailyChecklist] +} + +func NewDailyChecklistRepository(db *gorm.DB) DailyChecklistRepository { + return &DailyChecklistRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.DailyChecklist](db), + } +} diff --git a/internal/modules/daily-checklists/route.go b/internal/modules/daily-checklists/route.go new file mode 100644 index 00000000..c8542671 --- /dev/null +++ b/internal/modules/daily-checklists/route.go @@ -0,0 +1,35 @@ +package dailyChecklists + +import ( + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers" + dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.DailyChecklistService) { + ctrl := controller.NewDailyChecklistController(s) + + route := v1.Group("/daily-checklists") + route.Use(m.Auth(u)) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + + // create task + route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase) + + // create assigment + route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment) + // remove assignment + route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment) + + //get all tasks + route.Get("/tasks", ctrl.GetAllTasks) + + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go new file mode 100644 index 00000000..bf5320e6 --- /dev/null +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -0,0 +1,410 @@ +package service + +import ( + "errors" + "strconv" + "strings" + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" + phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" + "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" + "gorm.io/gorm/clause" +) + +type DailyChecklistService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.DailyChecklist, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DailyChecklist, error) + DeleteOne(ctx *fiber.Ctx, id uint) error + AssignPhases(ctx *fiber.Ctx, id uint, req *validation.AssignPhases) error + AssignTasks(ctx *fiber.Ctx, id uint, req *validation.AssignTask) error + RemoveAssignment(ctx *fiber.Ctx, id uint, employeeID uint) error + GetTasks(ctx *fiber.Ctx, checklistID uint) ([]entity.DailyChecklistActivityTask, error) +} + +type dailyChecklistService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.DailyChecklistRepository + PhaseRepo phaseRepo.PhasesRepository +} + +func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService { + return &dailyChecklistService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + PhaseRepo: phaseRepo, + } +} + +func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB { + return db +} + +func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + dailyChecklists, 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 dailyChecklists: %+v", err) + return nil, 0, err + } + return dailyChecklists, total, nil +} + +func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) { + dailyChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + if err != nil { + s.Log.Errorf("Failed get dailyChecklist by id: %+v", err) + return nil, err + } + return dailyChecklist, nil +} + +func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + date, err := time.Parse("2006-01-02", req.Date) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date format, use YYYY-MM-DD") + } + + status := req.Status + category := req.Category + + createBody := &entity.DailyChecklist{ + KandangId: req.KandangId, + Date: date, + Category: category, + Status: &status, + } + + err = s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "date"}, {Name: "kandang_id"}, {Name: "category"}}, + DoUpdates: clause.Assignments(map[string]any{"status": status, "updated_at": time.Now()}), + }).Create(createBody).Error + if err != nil { + s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.DailyChecklist, 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, "DailyChecklist not found") + } + s.Log.Errorf("Failed to update dailyChecklist: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s dailyChecklistService) 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, "DailyChecklist not found") + } + s.Log.Errorf("Failed to delete dailyChecklist: %+v", err) + return err + } + return nil +} + +func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validation.AssignPhases) error { + if err := s.Validate.Struct(req); err != nil { + return err + } + + if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + return err + } + + phaseIDs, err := parsePhaseIDs(req.PhaseIDs) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + if len(phaseIDs) > 0 { + phases, err := s.PhaseRepo.GetByIDs(c.Context(), phaseIDs, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusBadRequest, "Phase not found") + } + return err + } + if len(phases) != len(phaseIDs) { + return fiber.NewError(fiber.StatusBadRequest, "Phase not found") + } + } + + db := s.Repository.DB() + if err := db.WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + if err := tx.Where("checklist_id = ?", id).Delete(&entity.DailyChecklistPhase{}).Error; err != nil { + return err + } + + if len(phaseIDs) == 0 { + return nil + } + + records := make([]entity.DailyChecklistPhase, 0, len(phaseIDs)) + for _, pid := range phaseIDs { + records = append(records, entity.DailyChecklistPhase{ + ChecklistId: id, + PhaseId: pid, + }) + } + + if err := tx.Create(&records).Error; err != nil { + return err + } + + if err := tx.Where("checklist_id = ?", id).Delete(&entity.DailyChecklistActivityTask{}).Error; err != nil { + return err + } + + var activities []entity.PhaseActivity + if err := tx.Where("phase_id IN ?", phaseIDs).Find(&activities).Error; err != nil { + return err + } + + activityRecords := make([]entity.DailyChecklistActivityTask, 0, len(activities)) + for _, activity := range activities { + activityRecords = append(activityRecords, entity.DailyChecklistActivityTask{ + ChecklistId: id, + PhaseId: activity.PhaseId, + PhaseActivityId: activity.Id, + TimeType: activity.TimeType, + }) + } + + if len(activityRecords) == 0 { + return nil + } + + return tx.Create(&activityRecords).Error + }); err != nil { + s.Log.Errorf("Failed to assign phases to daily checklist: %+v", err) + return err + } + + return nil +} + +func (s dailyChecklistService) RemoveAssignment(c *fiber.Ctx, id uint, employeeID uint) error { + if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + return err + } + + if employeeID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id") + } + + db := s.Repository.DB() + if err := db.WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + var tasks []entity.DailyChecklistActivityTask + if err := tx.Where("checklist_id = ?", id).Find(&tasks).Error; err != nil { + return err + } + + if len(tasks) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "No activity tasks found for this checklist") + } + + taskIDs := collectTaskIDs(tasks) + return tx.Where("task_id IN ? AND employee_id = ?", taskIDs, employeeID). + Delete(&entity.DailyChecklistActivityTaskAssignment{}).Error + }); err != nil { + s.Log.Errorf("Failed to remove assignment: %+v", err) + return err + } + + return nil +} + +func (s dailyChecklistService) GetTasks(c *fiber.Ctx, checklistID uint) ([]entity.DailyChecklistActivityTask, error) { + if checklistID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required") + } + + if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + return nil, err + } + + var tasks []entity.DailyChecklistActivityTask + if err := s.Repository.DB().WithContext(c.Context()). + Where("checklist_id = ?", checklistID). + Order("created_at ASC"). + Find(&tasks).Error; err != nil { + s.Log.Errorf("Failed to get daily checklist tasks: %+v", err) + return nil, err + } + + return tasks, nil +} + +func parsePhaseIDs(raw string) ([]uint, error) { + parts := strings.Split(raw, ",") + result := make([]uint, 0, len(parts)) + seen := make(map[uint]struct{}) + + for _, part := range parts { + value := strings.TrimSpace(part) + if value == "" { + continue + } + + num, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return nil, errors.New("invalid phase id: " + value) + } + u := uint(num) + if _, ok := seen[u]; ok { + continue + } + seen[u] = struct{}{} + result = append(result, u) + } + + return result, nil +} + +func parseIDs(raw string) ([]uint, error) { + parts := strings.Split(raw, ",") + result := make([]uint, 0, len(parts)) + seen := make(map[uint]struct{}) + + for _, part := range parts { + value := strings.TrimSpace(part) + if value == "" { + continue + } + + num, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return nil, errors.New("invalid employee id: " + value) + } + u := uint(num) + if _, ok := seen[u]; ok { + continue + } + seen[u] = struct{}{} + result = append(result, u) + } + + return result, nil +} + +func collectTaskIDs(tasks []entity.DailyChecklistActivityTask) []uint { + result := make([]uint, len(tasks)) + for i, task := range tasks { + result[i] = task.Id + } + return result +} +func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validation.AssignTask) error { + if err := s.Validate.Struct(req); err != nil { + return err + } + + employeeIDs, err := parseIDs(req.EmployeeIDs) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + if len(employeeIDs) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "employee_ids cannot be empty") + } + + if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + return err + } + + db := s.Repository.DB() + if err := db.WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + var tasks []entity.DailyChecklistActivityTask + if err := tx.Where("checklist_id = ?", id).Find(&tasks).Error; err != nil { + return err + } + + if len(tasks) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "No activity tasks found for this checklist") + } + + assignments := make([]entity.DailyChecklistActivityTaskAssignment, 0, len(tasks)*len(employeeIDs)) + for _, task := range tasks { + for _, empID := range employeeIDs { + assignments = append(assignments, entity.DailyChecklistActivityTaskAssignment{ + TaskId: task.Id, + EmployeeId: empID, + }) + } + } + + return tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "task_id"}, {Name: "employee_id"}}, + DoUpdates: clause.Assignments(map[string]any{"updated_at": time.Now()}), + }).Create(&assignments).Error + }); err != nil { + s.Log.Errorf("Failed to assign tasks to daily checklist: %+v", err) + return err + } + + return nil +} diff --git a/internal/modules/daily-checklists/validations/daily-checklist.validation.go b/internal/modules/daily-checklists/validations/daily-checklist.validation.go new file mode 100644 index 00000000..ba81fd0d --- /dev/null +++ b/internal/modules/daily-checklists/validations/daily-checklist.validation.go @@ -0,0 +1,26 @@ +package validation + +type Create struct { + Date string `json:"date" validate:"required"` + KandangId uint `json:"kandang_id" validate:"required"` + Category string `json:"category" validate:"required"` + Status string `json:"status" validate:"required"` +} + +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"` +} + +type AssignPhases struct { + PhaseIDs string `json:"phase_ids" validate:"required"` +} + +type AssignTask struct { + EmployeeIDs string `json:"employee_ids" validate:"required"` +} diff --git a/internal/modules/master/employees/services/employees.service.go b/internal/modules/master/employees/services/employees.service.go index aa82255d..4998eaec 100644 --- a/internal/modules/master/employees/services/employees.service.go +++ b/internal/modules/master/employees/services/employees.service.go @@ -2,8 +2,6 @@ package service import ( "errors" - "fmt" - "strconv" "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -41,10 +39,8 @@ func NewEmployeesService(repo repository.EmployeesRepository, validate *validato 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") + Preload("EmployeeKandangs.Kandang"). + Where("employees.deleted_at IS NULL") } func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) { @@ -98,9 +94,9 @@ func (s *employeesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent 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()) + kandangIDs := normalizeKandangIDs(req.KandangIDs) + if len(kandangIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") } if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB { @@ -181,9 +177,9 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } if req.KandangIDs != nil { - ids, err := parseKandangIDs(*req.KandangIDs) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + ids := normalizeKandangIDs(*req.KandangIDs) + if len(ids) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") } kandangIDs = ids @@ -248,33 +244,22 @@ func (s employeesService) DeleteOne(c *fiber.Ctx, id uint) error { return nil } -func parseKandangIDs(raw string) ([]uint, error) { - parts := strings.Split(raw, ",") - ids := make([]uint, 0, len(parts)) +func normalizeKandangIDs(ids []uint) []uint { + result := make([]uint, 0, len(ids)) seen := make(map[uint]struct{}) - for _, part := range parts { - value := strings.TrimSpace(part) - if value == "" { + for _, id := range ids { + if id == 0 { 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) + result = append(result, id) } - if len(ids) == 0 { - return nil, errors.New("kandang_ids must contain at least one valid id") - } - - return ids, nil + return result } diff --git a/internal/modules/master/employees/validations/employees.validation.go b/internal/modules/master/employees/validations/employees.validation.go index 159b875f..2e2cc879 100644 --- a/internal/modules/master/employees/validations/employees.validation.go +++ b/internal/modules/master/employees/validations/employees.validation.go @@ -2,13 +2,13 @@ package validation type Create struct { Name string `json:"name" validate:"required_strict,min=3"` - KandangIDs string `json:"kandang_ids" validate:"required"` + KandangIDs []uint `json:"kandang_ids" validate:"required,min=1,dive,required"` IsActive bool `json:"is_active"` } type Update struct { Name *string `json:"name,omitempty" validate:"omitempty"` - KandangIDs *string `json:"kandang_ids,omitempty"` + KandangIDs *[]uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,required"` IsActive *bool `json:"is_active,omitempty"` } diff --git a/internal/route/route.go b/internal/route/route.go index 877ec875..519ea5aa 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -11,6 +11,7 @@ import ( approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals" closings "gitlab.com/mbugroup/lti-api.git/internal/modules/closings" constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants" + dailyChecklists "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists" expenses "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses" finance "gitlab.com/mbugroup/lti-api.git/internal/modules/finance" inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory" @@ -46,6 +47,7 @@ func Routes(app *fiber.App, db *gorm.DB) { closings.ClosingModule{}, repports.RepportModule{}, finance.FinanceModule{}, + dailyChecklists.DailyChecklistModule{}, // MODULE REGISTRY }