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/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 new file mode 100644 index 00000000..a93cbb46 --- /dev/null +++ b/internal/entities/employee.go @@ -0,0 +1,31 @@ +package entities + +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"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + 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..178ed695 --- /dev/null +++ b/internal/entities/phase.go @@ -0,0 +1,43 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +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"` + 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"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Phase Phases `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 *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/controllers/employees.controller.go b/internal/modules/master/employees/controllers/employees.controller.go new file mode 100644 index 00000000..3d0901c8 --- /dev/null +++ b/internal/modules/master/employees/controllers/employees.controller.go @@ -0,0 +1,161 @@ +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") + } + + if kandangParam := c.Query("kandang_id", ""); kandangParam != "" { + id, err := strconv.Atoi(kandangParam) + if err != nil || id <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "invalid kandang_id") + } + temp := uint(id) + query.KandangId = &temp + } + + if activeParam := c.Query("is_active", ""); activeParam != "" { + value, err := strconv.ParseBool(activeParam) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid is_active value") + } + query.IsActive = &value + } + + 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..4998eaec --- /dev/null +++ b/internal/modules/master/employees/services/employees.service.go @@ -0,0 +1,265 @@ +package service + +import ( + "errors" + "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"). + Where("employees.deleted_at IS NULL") +} + +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 != "" { + db = db.Where("employees.name LIKE ?", "%"+params.Search+"%") + } + if params.KandangId != nil { + db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id"). + Where("ek.kandang_id = ?", *params.KandangId) + } + if params.IsActive != nil { + db = db.Where("employees.is_active = ?", *params.IsActive) + } + return db.Order("employees.created_at DESC").Order("employees.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 := 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 { + 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) + var ( + kandangIDs []uint + needKandangUpdate bool + ) + + if req.Name != nil { + trimmed := strings.TrimSpace(*req.Name) + if trimmed == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty") + } + + if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("LOWER(name) = ? AND id <> ?", strings.ToLower(trimmed), id) + }); 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 + } + + updateBody["name"] = trimmed + } + + if req.IsActive != nil { + updateBody["is_active"] = *req.IsActive + } + + if req.KandangIDs != nil { + 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 + needKandangUpdate = true + } + + if len(updateBody) == 0 && !needKandangUpdate { + return s.GetOne(c, id) + } + + if err := s.Repository.DB().Transaction(func(tx *gorm.DB) error { + repoTx := s.Repository.WithTx(tx) + + if len(updateBody) > 0 { + if err := repoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { + return err + } + } + + if needKandangUpdate { + if err := tx.WithContext(c.Context()). + Where("employee_id = ?", id). + Delete(&entity.EmployeeKandang{}).Error; err != nil { + return err + } + + relations := make([]entity.EmployeeKandang, 0, len(kandangIDs)) + for _, kandangID := range kandangIDs { + relations = append(relations, entity.EmployeeKandang{ + EmployeeId: id, + KandangId: kandangID, + }) + } + + if len(relations) > 0 { + if err := tx.WithContext(c.Context()).Create(&relations).Error; err != nil { + return err + } + } + } + + return 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 normalizeKandangIDs(ids []uint) []uint { + result := make([]uint, 0, len(ids)) + seen := make(map[uint]struct{}) + + for _, id := range ids { + if id == 0 { + continue + } + + if _, ok := seen[id]; ok { + continue + } + + seen[id] = struct{}{} + result = append(result, id) + } + + return result +} 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..2e2cc879 --- /dev/null +++ b/internal/modules/master/employees/validations/employees.validation.go @@ -0,0 +1,21 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` + 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 *[]uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,required"` + 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"` + KandangId *uint `query:"kandang_id" validate:"omitempty"` + IsActive *bool `query:"is_active" validate:"omitempty"` +} diff --git a/internal/modules/master/phase-activities/controllers/phase-activity.controller.go b/internal/modules/master/phase-activities/controllers/phase-activity.controller.go new file mode 100644 index 00000000..455ff1e4 --- /dev/null +++ b/internal/modules/master/phase-activities/controllers/phase-activity.controller.go @@ -0,0 +1,153 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type PhaseActivityController struct { + PhaseActivityService service.PhaseActivityService +} + +func NewPhaseActivityController(phaseActivityService service.PhaseActivityService) *PhaseActivityController { + return &PhaseActivityController{ + PhaseActivityService: phaseActivityService, + } +} + +func (u *PhaseActivityController) 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") + } + + if phaseParam := c.Query("phase_id", ""); phaseParam != "" { + id, err := strconv.Atoi(phaseParam) + if err != nil || id <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "invalid phase_id") + } + temp := uint(id) + query.PhaseId = &temp + } + + result, totalResults, err := u.PhaseActivityService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.PhaseActivityListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all phaseActivitys successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToPhaseActivityListDTOs(result), + }) +} + +func (u *PhaseActivityController) 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.PhaseActivityService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get phaseActivity successfully", + Data: dto.ToPhaseActivityListDTO(*result), + }) +} + +func (u *PhaseActivityController) 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.PhaseActivityService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create phaseActivity successfully", + Data: dto.ToPhaseActivityListDTO(*result), + }) +} + +func (u *PhaseActivityController) 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.PhaseActivityService.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 phaseActivity successfully", + Data: dto.ToPhaseActivityListDTO(*result), + }) +} + +func (u *PhaseActivityController) 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.PhaseActivityService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete phaseActivity successfully", + }) +} diff --git a/internal/modules/master/phase-activities/dto/phase-activity.dto.go b/internal/modules/master/phase-activities/dto/phase-activity.dto.go new file mode 100644 index 00000000..ee5942d5 --- /dev/null +++ b/internal/modules/master/phase-activities/dto/phase-activity.dto.go @@ -0,0 +1,72 @@ +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 PhaseActivityRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type PhaseActivityListDTO struct { + Id uint `json:"id"` + PhaseId uint `json:"phase_id"` + Name string `json:"name"` + Description *string `json:"description"` + TimeType *string `json:"time_type"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type PhaseActivityDetailDTO struct { + PhaseActivityListDTO +} + +// === Mapper Functions === + +func ToPhaseActivityRelationDTO(e entity.PhaseActivity) PhaseActivityRelationDTO { + return PhaseActivityRelationDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToPhaseActivityListDTO(e entity.PhaseActivity) PhaseActivityListDTO { + var createdUser *userDTO.UserRelationDTO + // if e.CreatedUser.Id != 0 { + // mapped := userDTO.ToUserRelationDTO(e.CreatedUser) + // createdUser = &mapped + // } + + return PhaseActivityListDTO{ + Id: e.Id, + PhaseId: e.PhaseId, + Name: e.Name, + Description: e.Description, + TimeType: e.TimeType, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToPhaseActivityListDTOs(e []entity.PhaseActivity) []PhaseActivityListDTO { + result := make([]PhaseActivityListDTO, len(e)) + for i, r := range e { + result[i] = ToPhaseActivityListDTO(r) + } + return result +} + +func ToPhaseActivityDetailDTO(e entity.PhaseActivity) PhaseActivityDetailDTO { + return PhaseActivityDetailDTO{ + PhaseActivityListDTO: ToPhaseActivityListDTO(e), + } +} diff --git a/internal/modules/master/phase-activities/module.go b/internal/modules/master/phase-activities/module.go new file mode 100644 index 00000000..22d25189 --- /dev/null +++ b/internal/modules/master/phase-activities/module.go @@ -0,0 +1,27 @@ +package phaseActivity + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rPhaseActivity "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/repositories" + sPhaseActivity "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/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 PhaseActivityModule struct{} + +func (PhaseActivityModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + phaseActivityRepo := rPhaseActivity.NewPhaseActivityRepository(db) + phasesRepo := rPhases.NewPhasesRepository(db) + userRepo := rUser.NewUserRepository(db) + + phaseActivityService := sPhaseActivity.NewPhaseActivityService(phaseActivityRepo, phasesRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + PhaseActivityRoutes(router, userService, phaseActivityService) +} diff --git a/internal/modules/master/phase-activities/repositories/phase-activity.repository.go b/internal/modules/master/phase-activities/repositories/phase-activity.repository.go new file mode 100644 index 00000000..cc5eaae5 --- /dev/null +++ b/internal/modules/master/phase-activities/repositories/phase-activity.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 PhaseActivityRepository interface { + repository.BaseRepository[entity.PhaseActivity] +} + +type PhaseActivityRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.PhaseActivity] +} + +func NewPhaseActivityRepository(db *gorm.DB) PhaseActivityRepository { + return &PhaseActivityRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.PhaseActivity](db), + } +} diff --git a/internal/modules/master/phase-activities/route.go b/internal/modules/master/phase-activities/route.go new file mode 100644 index 00000000..6fcef558 --- /dev/null +++ b/internal/modules/master/phase-activities/route.go @@ -0,0 +1,23 @@ +package phaseActivity + +import ( + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/controllers" + phaseActivity "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func PhaseActivityRoutes(v1 fiber.Router, u user.UserService, s phaseActivity.PhaseActivityService) { + ctrl := controller.NewPhaseActivityController(s) + + route := v1.Group("/phase-activities") + 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/phase-activities/services/phase-activity.service.go b/internal/modules/master/phase-activities/services/phase-activity.service.go new file mode 100644 index 00000000..3426eab4 --- /dev/null +++ b/internal/modules/master/phase-activities/services/phase-activity.service.go @@ -0,0 +1,167 @@ +package service + +import ( + "errors" + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/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" +) + +type PhaseActivityService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.PhaseActivity, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.PhaseActivity, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.PhaseActivity, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.PhaseActivity, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type phaseActivityService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.PhaseActivityRepository + PhaseRepo phaseRepo.PhasesRepository +} + +func NewPhaseActivityService(repo repository.PhaseActivityRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) PhaseActivityService { + return &phaseActivityService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + PhaseRepo: phaseRepo, + } +} + +func (s phaseActivityService) withRelations(db *gorm.DB) *gorm.DB { + return db +} + +func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.PhaseActivity, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + phaseActivitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.Search != "" { + db = db.Where("name LIKE ?", "%"+params.Search+"%") + } + if params.PhaseId != nil { + db = db.Where("phase_id = ?", *params.PhaseId) + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get phaseActivitys: %+v", err) + return nil, 0, err + } + return phaseActivitys, total, nil +} + +func (s phaseActivityService) GetOne(c *fiber.Ctx, id uint) (*entity.PhaseActivity, error) { + phaseActivity, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "PhaseActivity not found") + } + if err != nil { + s.Log.Errorf("Failed get phaseActivity by id: %+v", err) + return nil, err + } + return phaseActivity, nil +} + +func (s *phaseActivityService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.PhaseActivity, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + phase, err := s.PhaseRepo.GetByID(c.Context(), req.PhaseId, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusBadRequest, "phase not found") + } + if err != nil { + s.Log.Errorf("Failed to get phase: %+v", err) + return nil, err + } + + name := strings.TrimSpace(req.Name) + if name == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty") + } + + timeType := strings.TrimSpace(req.TimeType) + if timeType == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty") + } + + createBody := &entity.PhaseActivity{ + PhaseId: phase.Id, + Name: name, + Description: req.Description, + TimeType: &timeType, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create phaseActivity: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s phaseActivityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.PhaseActivity, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + trimmedName := strings.TrimSpace(req.Name) + if trimmedName == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty") + } + + trimmedTimeType := strings.TrimSpace(req.TimeType) + if trimmedTimeType == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty") + } + + updateBody := map[string]any{ + "name": trimmedName, + "time_type": trimmedTimeType, + } + + if req.Description != nil { + updateBody["description"] = *req.Description + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "PhaseActivity not found") + } + s.Log.Errorf("Failed to update phaseActivity: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s phaseActivityService) 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, "PhaseActivity not found") + } + s.Log.Errorf("Failed to delete phaseActivity: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/master/phase-activities/validations/phase-activity.validation.go b/internal/modules/master/phase-activities/validations/phase-activity.validation.go new file mode 100644 index 00000000..a2ab8e1b --- /dev/null +++ b/internal/modules/master/phase-activities/validations/phase-activity.validation.go @@ -0,0 +1,21 @@ +package validation + +type Create struct { + PhaseId uint `json:"phase_id" validate:"required"` + Name string `json:"name" validate:"required_strict,min=3"` + Description *string `json:"description,omitempty"` + TimeType string `json:"time_type" validate:"required"` +} + +type Update struct { + Name string `json:"name" validate:"required_strict,min=3"` + Description *string `json:"description,omitempty"` + TimeType string `json:"time_type" validate:"required"` +} + +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"` + PhaseId *uint `query:"phase_id" validate:"omitempty"` +} diff --git a/internal/modules/master/phasess/controllers/phases.controller.go b/internal/modules/master/phasess/controllers/phases.controller.go new file mode 100644 index 00000000..c9d9d349 --- /dev/null +++ b/internal/modules/master/phasess/controllers/phases.controller.go @@ -0,0 +1,148 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type PhasesController struct { + PhasesService service.PhasesService +} + +func NewPhasesController(phasesService service.PhasesService) *PhasesController { + return &PhasesController{ + PhasesService: phasesService, + } +} + +func (u *PhasesController) 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") + } + + if category := c.Query("category", ""); category != "" { + query.Category = &category + } + + result, totalResults, err := u.PhasesService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.PhasesListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all phasess successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToPhasesListDTOs(result), + }) +} + +func (u *PhasesController) 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.PhasesService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get phases successfully", + Data: dto.ToPhasesListDTO(*result), + }) +} + +func (u *PhasesController) 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.PhasesService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create phases successfully", + Data: dto.ToPhasesListDTO(*result), + }) +} + +func (u *PhasesController) 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.PhasesService.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 phases successfully", + Data: dto.ToPhasesListDTO(*result), + }) +} + +func (u *PhasesController) 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.PhasesService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete phases successfully", + }) +} diff --git a/internal/modules/master/phasess/dto/phases.dto.go b/internal/modules/master/phasess/dto/phases.dto.go new file mode 100644 index 00000000..51724556 --- /dev/null +++ b/internal/modules/master/phasess/dto/phases.dto.go @@ -0,0 +1,68 @@ +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 PhasesRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type PhasesListDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + IsActive bool `json:"is_active"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` +} + +type PhasesDetailDTO struct { + PhasesListDTO +} + +// === Mapper Functions === + +func ToPhasesRelationDTO(e entity.Phases) PhasesRelationDTO { + return PhasesRelationDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToPhasesListDTO(e entity.Phases) PhasesListDTO { + var createdUser *userDTO.UserRelationDTO + // if e.CreatedUser.Id != 0 { + // mapped := userDTO.ToUserRelationDTO(e.CreatedUser) + // createdUser = &mapped + // } + + return PhasesListDTO{ + Id: e.Id, + Name: e.Name, + Category: e.Category, + IsActive: e.IsActive, + CreatedAt: e.CreatedAt, + CreatedUser: createdUser, + } +} + +func ToPhasesListDTOs(e []entity.Phases) []PhasesListDTO { + result := make([]PhasesListDTO, len(e)) + for i, r := range e { + result[i] = ToPhasesListDTO(r) + } + return result +} + +func ToPhasesDetailDTO(e entity.Phases) PhasesDetailDTO { + return PhasesDetailDTO{ + PhasesListDTO: ToPhasesListDTO(e), + } +} diff --git a/internal/modules/master/phasess/module.go b/internal/modules/master/phasess/module.go new file mode 100644 index 00000000..3f44c220 --- /dev/null +++ b/internal/modules/master/phasess/module.go @@ -0,0 +1,25 @@ +package phases + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" + sPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type PhasesModule struct{} + +func (PhasesModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + phasesRepo := rPhases.NewPhasesRepository(db) + userRepo := rUser.NewUserRepository(db) + + phasesService := sPhases.NewPhasesService(phasesRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + PhasesRoutes(router, userService, phasesService) +} diff --git a/internal/modules/master/phasess/repositories/phases.repository.go b/internal/modules/master/phasess/repositories/phases.repository.go new file mode 100644 index 00000000..d243ca2e --- /dev/null +++ b/internal/modules/master/phasess/repositories/phases.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 PhasesRepository interface { + repository.BaseRepository[entity.Phases] +} + +type PhasesRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.Phases] +} + +func NewPhasesRepository(db *gorm.DB) PhasesRepository { + return &PhasesRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.Phases](db), + } +} diff --git a/internal/modules/master/phasess/route.go b/internal/modules/master/phasess/route.go new file mode 100644 index 00000000..b4ca202d --- /dev/null +++ b/internal/modules/master/phasess/route.go @@ -0,0 +1,23 @@ +package phases + +import ( + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/controllers" + phases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func PhasesRoutes(v1 fiber.Router, u user.UserService, s phases.PhasesService) { + ctrl := controller.NewPhasesController(s) + + route := v1.Group("/phases") + 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/phasess/services/phases.service.go b/internal/modules/master/phasess/services/phases.service.go new file mode 100644 index 00000000..98e73bef --- /dev/null +++ b/internal/modules/master/phasess/services/phases.service.go @@ -0,0 +1,158 @@ +package service + +import ( + "errors" + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/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 PhasesService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Phases, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.Phases, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Phases, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Phases, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type phasesService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.PhasesRepository +} + +func NewPhasesService(repo repository.PhasesRepository, validate *validator.Validate) PhasesService { + return &phasesService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s phasesService) withRelations(db *gorm.DB) *gorm.DB { + return db +} + +func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Phases, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + phasess, 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+"%") + } + if params.Category != nil { + db = db.Where("category = ?", *params.Category) + } + return db.Order("created_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get phasess: %+v", err) + return nil, 0, err + } + return phasess, total, nil +} + +func (s phasesService) GetOne(c *fiber.Ctx, id uint) (*entity.Phases, error) { + phases, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Phases not found") + } + if err != nil { + s.Log.Errorf("Failed get phases by id: %+v", err) + return nil, err + } + return phases, nil +} + +func (s *phasesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Phases, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("LOWER(name) = ? AND category = ?", strings.ToLower(req.Name), req.Category) + }); err == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "phase already exists") + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed checking phase uniqueness: %+v", err) + return nil, err + } + + createBody := &entity.Phases{ + Name: req.Name, + Category: req.Category, + IsActive: true, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create phases: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s phasesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Phases, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + existing, err := s.Repository.GetByID(c.Context(), id, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Phases not found") + } + if err != nil { + s.Log.Errorf("Failed get phases by id: %+v", err) + return nil, err + } + + updateBody := make(map[string]any) + + if req.Name != nil { + if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("LOWER(name) = ? AND category = ? AND id <> ?", strings.ToLower(*req.Name), existing.Category, id) + }); err == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "phase already exists") + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed checking phase uniqueness: %+v", err) + return nil, err + } + + updateBody["name"] = strings.TrimSpace(*req.Name) + } + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + s.Log.Errorf("Failed to update phases: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s phasesService) 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, "Phases not found") + } + s.Log.Errorf("Failed to delete phases: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/master/phasess/validations/phases.validation.go b/internal/modules/master/phasess/validations/phases.validation.go new file mode 100644 index 00000000..c22d4208 --- /dev/null +++ b/internal/modules/master/phasess/validations/phases.validation.go @@ -0,0 +1,17 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` + Category string `json:"category" 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"` + Category *string `query:"category" validate:"omitempty"` +} diff --git a/internal/modules/master/route.go b/internal/modules/master/route.go index 26ae28ee..f9bc7b13 100644 --- a/internal/modules/master/route.go +++ b/internal/modules/master/route.go @@ -10,17 +10,20 @@ 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" + phaseActivitys "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities" + phasess "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess" 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 +45,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida banks.BankModule{}, flocks.FlockModule{}, productionStandards.ProductionStandardModule{}, + employeess.EmployeesModule{}, + phasess.PhasesModule{}, + phaseActivitys.PhaseActivityModule{}, // MODULE REGISTRY } 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 }