From c2b60c1aff16fe8fb4b59d58e063735efad6fd14 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 18 Nov 2025 12:26:54 +0700 Subject: [PATCH] feat(BE-#270): Project flock period change to project_flock_kandangs --- ...14_adjustment_projectflock_period.down.sql | 16 ++ ...2514_adjustment_projectflock_period.up.sql | 29 +++ internal/entities/location.go | 5 +- internal/entities/projectflock.go | 2 - internal/entities/projectflock_kandang.go | 1 + .../repositories/kandang.repository.go | 33 ++++ .../production/chickins/dto/chickin.dto.go | 7 +- .../controllers/projectflock.controller.go | 61 +++++- .../project_flocks/dto/projectflock.dto.go | 36 +++- .../dto/projectflock_kandang.dto.go | 2 +- .../repositories/projectflock.repository.go | 84 ++------- .../projectflock_kandang.repository.go | 29 ++- .../production/project_flocks/route.go | 6 +- .../services/projectflock.service.go | 173 ++++++++++++------ .../dto/transfer_laying.dto.go | 2 - 15 files changed, 330 insertions(+), 156 deletions(-) create mode 100644 internal/database/migrations/20251117092514_adjustment_projectflock_period.down.sql create mode 100644 internal/database/migrations/20251117092514_adjustment_projectflock_period.up.sql diff --git a/internal/database/migrations/20251117092514_adjustment_projectflock_period.down.sql b/internal/database/migrations/20251117092514_adjustment_projectflock_period.down.sql new file mode 100644 index 00000000..64183dd7 --- /dev/null +++ b/internal/database/migrations/20251117092514_adjustment_projectflock_period.down.sql @@ -0,0 +1,16 @@ +BEGIN; + +ALTER TABLE project_flock_kandangs + DROP COLUMN IF EXISTS period; + +ALTER TABLE project_flocks + ADD COLUMN IF NOT EXISTS period INT NOT NULL DEFAULT 0; + +CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique + ON project_flocks ( + LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))), + period + ) + WHERE deleted_at IS NULL; + +COMMIT; diff --git a/internal/database/migrations/20251117092514_adjustment_projectflock_period.up.sql b/internal/database/migrations/20251117092514_adjustment_projectflock_period.up.sql new file mode 100644 index 00000000..a518c2d6 --- /dev/null +++ b/internal/database/migrations/20251117092514_adjustment_projectflock_period.up.sql @@ -0,0 +1,29 @@ +BEGIN; + +ALTER TABLE project_flock_kandangs + ADD COLUMN IF NOT EXISTS period INT; + +UPDATE project_flock_kandangs pfk +SET period = pf.period +FROM project_flocks pf +WHERE pfk.project_flock_id = pf.id + AND (pfk.period IS NULL OR pfk.period = 0) + AND pf.period IS NOT NULL; + +ALTER TABLE project_flock_kandangs + ALTER COLUMN period SET DEFAULT 0; + +UPDATE project_flock_kandangs +SET period = 0 +WHERE period IS NULL; + +ALTER TABLE project_flock_kandangs + ALTER COLUMN period SET NOT NULL; + +-- Drop period from project_flocks as the source of truth +DROP INDEX IF EXISTS project_flocks_base_period_unique; + +ALTER TABLE project_flocks + DROP COLUMN IF EXISTS period; + +COMMIT; diff --git a/internal/entities/location.go b/internal/entities/location.go index 9f87e97b..1dba8f82 100644 --- a/internal/entities/location.go +++ b/internal/entities/location.go @@ -16,6 +16,7 @@ type Location struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Area Area `gorm:"foreignKey:AreaId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Area Area `gorm:"foreignKey:AreaId;references:Id"` + Kandangs []Kandang `gorm:"foreignKey:LocationId;references:Id"` } diff --git a/internal/entities/projectflock.go b/internal/entities/projectflock.go index 64f47aaf..e8745455 100644 --- a/internal/entities/projectflock.go +++ b/internal/entities/projectflock.go @@ -13,7 +13,6 @@ type ProjectFlock struct { Category string `gorm:"type:varchar(20);not null"` FcrId uint `gorm:"not null"` LocationId uint `gorm:"not null"` - Period int `gorm:"not null"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` @@ -28,4 +27,3 @@ type ProjectFlock struct { LatestApproval *Approval `gorm:"-" json:"-"` } - diff --git a/internal/entities/projectflock_kandang.go b/internal/entities/projectflock_kandang.go index c02eafe1..d4bd7452 100644 --- a/internal/entities/projectflock_kandang.go +++ b/internal/entities/projectflock_kandang.go @@ -6,6 +6,7 @@ type ProjectFlockKandang struct { Id uint `gorm:"primaryKey"` ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"` KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"` + Period int `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` diff --git a/internal/modules/master/kandangs/repositories/kandang.repository.go b/internal/modules/master/kandangs/repositories/kandang.repository.go index 8f32a7b2..a08db163 100644 --- a/internal/modules/master/kandangs/repositories/kandang.repository.go +++ b/internal/modules/master/kandangs/repositories/kandang.repository.go @@ -7,6 +7,7 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" + "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) @@ -115,9 +116,41 @@ func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, p Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID). First(&link).Error if errors.Is(err, gorm.ErrRecordNotFound) { + var project entity.ProjectFlock + if err := r.db.WithContext(ctx). + Select("id, location_id"). + First(&project, projectFlockID).Error; err != nil { + return err + } + + var kandang entity.Kandang + if err := r.db.WithContext(ctx). + Select("id, location_id"). + First(&kandang, kandangID).Error; err != nil { + return err + } + if kandang.LocationId != project.LocationId { + return fiber.NewError(fiber.StatusBadRequest, "Kandang tidak berada pada lokasi yang sama dengan project flock") + } + + // Determine project's period from existing pivot rows so the new kandang + // shares the same period. + var period int + if err := r.db.WithContext(ctx). + Table("project_flock_kandangs"). + Where("project_flock_id = ?", projectFlockID). + Select("COALESCE(MAX(period), 0)"). + Scan(&period).Error; err != nil { + return err + } + if period <= 0 { + period = 1 + } + link = entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: kandangID, + Period: period, } return r.db.WithContext(ctx).Create(&link).Error } diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 7271c310..b581da3c 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -100,7 +100,8 @@ func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO { return userBaseDTO.ToUserBaseDTO(e) } -func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { +func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO { + e := pfk.ProjectFlock var flock *flockBaseDTO.FlockBaseDTO if base := pfutils.DeriveBaseName(e.FlockName); base != "" { summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base} @@ -123,7 +124,7 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { } return ProjectFlockDTO{ Id: e.Id, - Period: e.Period, + Period: pfk.Period, Category: e.Category, Flock: flock, Area: area, @@ -135,7 +136,7 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { var pf *ProjectFlockDTO if e.ProjectFlock.Id != 0 { - mapped := ToProjectFlockDTO(e.ProjectFlock) + mapped := ToProjectFlockDTO(e) pf = &mapped } var kandang *kandangBaseDTO.KandangBaseDTO diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index d3b0061c..9957e4d9 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -85,6 +85,17 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { return err } + var periodMap map[uint]int + if len(result) > 0 { + ids := make([]uint, len(result)) + for i, item := range result { + ids[i] = item.Id + } + if periods, err := u.ProjectflockService.GetProjectPeriods(c, ids); err == nil { + periodMap = periods + } + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.ProjectFlockListDTO]{ Code: fiber.StatusOK, @@ -96,7 +107,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToProjectFlockListDTOs(result), + Data: dto.ToProjectFlockListDTOsWithPeriods(result, periodMap), }) } @@ -113,12 +124,19 @@ func (u *ProjectflockController) GetOne(c *fiber.Ctx) error { return err } + var period int + if periods, err := u.ProjectflockService.GetProjectPeriods(c, []uint{uint(id)}); err == nil { + if p, ok := periods[uint(id)]; ok { + period = p + } + } + return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Get projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result), + Data: dto.ToProjectFlockListDTOWithPeriod(*result, period), }) } @@ -205,11 +223,29 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error { data interface{} message = "Submit projectflock approval successfully" ) + + var periodMap map[uint]int + if len(results) > 0 { + ids := make([]uint, len(results)) + for i, item := range results { + ids[i] = item.Id + } + if periods, err := u.ProjectflockService.GetProjectPeriods(c, ids); err == nil { + periodMap = periods + } + } + if len(results) == 1 { - data = dto.ToProjectFlockListDTO(results[0]) + period := 0 + if periodMap != nil { + if p, ok := periodMap[results[0].Id]; ok { + period = p + } + } + data = dto.ToProjectFlockListDTOWithPeriod(results[0], period) } else { message = "Submit projectflock approvals successfully" - data = dto.ToProjectFlockListDTOs(results) + data = dto.ToProjectFlockListDTOsWithPeriods(results, periodMap) } return c.Status(fiber.StatusOK). @@ -222,25 +258,32 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error { } func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { - param := c.Params("project_flock_kandang_id") + param := c.Params("location_id") id, err := strconv.Atoi(param) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") + return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id") } - summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id)) + summaries, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id)) if err != nil { return err } - responseBody := dto.ToFlockPeriodSummaryDTO(summary.Flock, summary.NextPeriod) + responseBody := make([]dto.KandangPeriodSummaryDTO, 0, len(summaries)) + for _, item := range summaries { + responseBody = append(responseBody, dto.KandangPeriodSummaryDTO{ + Id: item.Id, + Name: item.Name, + Period: item.Period, + }) + } return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", - Message: "Get flock period summary successfully", + Message: "Get kandang period summary successfully", Data: responseBody, }) } diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index 977aeb40..dac023df 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -53,7 +53,13 @@ type FlockPeriodDTO struct { NextPeriod int `json:"next_period"` } -func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { +type KandangPeriodSummaryDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Period int `json:"period"` +} + +func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectFlockListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(e.CreatedUser) @@ -99,7 +105,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { } return ProjectFlockListDTO{ - ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), + ProjectFlockBaseDTO: createProjectFlockBaseDTO(e, period), // Flock: flockSummary, Area: areaSummary, Kandangs: kandangSummaries, @@ -116,14 +122,32 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { func ToProjectFlockListDTOs(items []entity.ProjectFlock) []ProjectFlockListDTO { result := make([]ProjectFlockListDTO, len(items)) for i, item := range items { - result[i] = ToProjectFlockListDTO(item) + result[i] = ToProjectFlockListDTOWithPeriod(item, 0) + } + return result +} + +func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { + return ToProjectFlockListDTOWithPeriod(e, 0) +} + +func ToProjectFlockListDTOsWithPeriods(items []entity.ProjectFlock, periods map[uint]int) []ProjectFlockListDTO { + result := make([]ProjectFlockListDTO, len(items)) + for i, item := range items { + p := 0 + if periods != nil { + if v, ok := periods[item.Id]; ok { + p = v + } + } + result[i] = ToProjectFlockListDTOWithPeriod(item, p) } return result } func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO { return ProjectFlockDetailDTO{ - ProjectFlockListDTO: ToProjectFlockListDTO(e), + ProjectFlockListDTO: ToProjectFlockListDTOWithPeriod(e, 0), } } @@ -152,10 +176,10 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv return result } -func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { +func createProjectFlockBaseDTO(e entity.ProjectFlock, period int) ProjectFlockBaseDTO { return ProjectFlockBaseDTO{ Id: e.Id, - Period: e.Period, + Period: period, FlockName: e.FlockName, } } diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 24e53d28..2e25bf09 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -50,7 +50,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD pfLocal := ProjectFlockWithPivotDTO{ ProjectFlockBaseDTO: ProjectFlockBaseDTO{ Id: e.ProjectFlock.Id, - Period: e.ProjectFlock.Period, + Period: e.Period, FlockName: e.ProjectFlock.FlockName, }, Category: e.ProjectFlock.Category, diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index bb653fe9..afb05982 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -2,7 +2,6 @@ package repository import ( "context" - "errors" "fmt" "strings" @@ -10,17 +9,12 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" "gorm.io/gorm" - "gorm.io/gorm/clause" ) const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))" type ProjectflockRepository interface { repository.BaseRepository[entity.ProjectFlock] - GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) - GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) - GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) - GetNextSequenceForBase(ctx context.Context, baseName string) (int, error) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) @@ -39,65 +33,6 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository { } } -func (r *ProjectflockRepositoryImpl) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) { - var records []entity.ProjectFlock - if err := r.DB().WithContext(ctx). - Unscoped(). - Where(baseNameExpression+" = LOWER(?)", baseName). - Order("period ASC"). - Find(&records).Error; err != nil { - return nil, err - } - return records, nil -} - -func (r *ProjectflockRepositoryImpl) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) { - var record entity.ProjectFlock - err := r.DB().WithContext(ctx). - Where(baseNameExpression+" = LOWER(?)", baseName). - Order("period DESC"). - First(&record).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - if err != nil { - return nil, err - } - return &record, nil -} - -func (r *ProjectflockRepositoryImpl) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) { - var max int - if err := r.DB().WithContext(ctx). - Model(&entity.ProjectFlock{}). - Where(baseNameExpression+" = LOWER(?)", baseName). - Select("COALESCE(MAX(period), 0)"). - Scan(&max).Error; err != nil { - return 0, err - } - return max, nil -} - -func (r *ProjectflockRepositoryImpl) GetNextSequenceForBase(ctx context.Context, baseName string) (int, error) { - var payload struct { - Period int - } - if err := r.DB().WithContext(ctx). - Model(&entity.ProjectFlock{}). - Where(baseNameExpression+" = LOWER(?)", baseName). - Clauses(clause.Locking{Strength: "UPDATE"}). - Order("period DESC"). - Limit(1). - Select("period"). - Scan(&payload).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return 1, nil - } - return 0, err - } - return payload.Period + 1, nil -} - func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) { return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { db = r.withDefaultRelations(db) @@ -132,7 +67,13 @@ func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *vali db = db.Where("project_flocks.location_id = ?", params.LocationId) } if params.Period > 0 { - db = db.Where("project_flocks.period = ?", params.Period) + db = db.Where(` + EXISTS ( + SELECT 1 + FROM project_flock_kandangs pfk + WHERE pfk.project_flock_id = project_flocks.id + AND pfk.period = ? + )`, params.Period) } if len(params.KandangIds) > 0 { db = db.Where(` @@ -179,10 +120,15 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s OR LOWER(created_users.email) LIKE ? OR LOWER(project_flocks.flock_name) LIKE ? OR LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))) LIKE ? - OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ? + OR EXISTS ( + SELECT 1 FROM project_flock_kandangs + WHERE project_flock_kandangs.project_flock_id = project_flocks.id + AND LOWER(CAST(project_flock_kandangs.period AS TEXT)) LIKE ? + ) OR EXISTS ( SELECT 1 FROM kandangs - WHERE kandangs.project_flock_id = project_flocks.id + JOIN project_flock_kandangs pfk ON pfk.kandang_id = kandangs.id + WHERE pfk.project_flock_id = project_flocks.id AND LOWER(kandangs.name) LIKE ? ) `, @@ -236,7 +182,7 @@ func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder str } case "period": return []string{ - fmt.Sprintf("project_flocks.period %s", direction), + fmt.Sprintf("(SELECT COALESCE(MAX(period), 0) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction), fmt.Sprintf("project_flocks.id %s", direction), } default: diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 101d01ab..6923b1f3 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -19,6 +19,7 @@ type ProjectFlockKandangRepository interface { HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) + ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) WithTx(tx *gorm.DB) ProjectFlockKandangRepository IdExists(ctx context.Context, id uint) (bool, error) DB() *gorm.DB @@ -177,7 +178,33 @@ func (r *projectFlockKandangRepositoryImpl) MaxPeriodByBaseName(ctx context.Cont Table("project_flock_kandangs pfk"). Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). Where(flockBaseNameExpression+" = LOWER(?)", baseName). - Select("COALESCE(MAX(pf.period), 0)"). + Select("COALESCE(MAX(pfk.period), 0)"). Scan(&max).Error return max, err } + +func (r *projectFlockKandangRepositoryImpl) ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) { + result := make(map[uint]int) + if len(projectIDs) == 0 { + return result, nil + } + + type row struct { + ProjectFlockID uint + Period int + } + var rows []row + if err := r.db.WithContext(ctx). + Table("project_flock_kandangs"). + Where("project_flock_id IN ?", projectIDs). + Select("project_flock_id, COALESCE(MAX(period), 0) AS period"). + Group("project_flock_id"). + Scan(&rows).Error; err != nil { + return nil, err + } + + for _, item := range rows { + result[item.ProjectFlockID] = item.Period + } + return result, nil +} diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index 8128f943..0e6f80c4 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -1,7 +1,7 @@ package project_flocks import ( - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers" projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,7 +13,7 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj ctrl := controller.NewProjectflockController(s) route := v1.Group("/project-flocks") - route.Use(m.Auth(u)) + // route.Use(m.Auth(u)) route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) @@ -22,6 +22,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Delete("/:id", ctrl.DeleteOne) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) - route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary) + route.Get("/kandangs/:location_id/periods", ctrl.GetFlockPeriodSummary) } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index a91976d8..f812f412 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -35,7 +35,8 @@ type ProjectflockService interface { GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) - GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) + GetFlockPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) + GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) } @@ -52,9 +53,10 @@ type projectflockService struct { approvalWorkflow approvalutils.ApprovalWorkflowKey } -type FlockPeriodSummary struct { - Flock entity.Flock - NextPeriod int +type KandangPeriodSummary struct { + Id uint + Name string + Period int } func NewProjectflockService( @@ -218,6 +220,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* if len(kandangs) != len(kandangIDs) { return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") } + for _, kandang := range kandangs { + if kandang.LocationId != req.LocationId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id)) + } + } // larang kalau ada yg sudah terikat ke project lain if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage") @@ -236,22 +243,24 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { projectRepo := repository.NewProjectflockRepository(dbTransaction) - nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), canonicalBase) - if err != nil { - return err - } - generatedName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, nextSeq, nil) + // Generate unique flock name (sequential per base name, starting from 1) + generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil) if err != nil { return err } createBody.FlockName = generatedName - createBody.Period = seq if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { return err } - if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs); err != nil { + // Compute period based on location history (max period in that location + 1), + // and store it on project_flock_kandangs only. + nextPeriod, err := s.nextLocationPeriod(c.Context(), dbTransaction, req.LocationId) + if err != nil { + return err + } + if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, nextPeriod); err != nil { return err } @@ -387,6 +396,15 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id if len(kandangs) != len(newKandangIDs) { return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") } + targetLocationID := existing.LocationId + if req.LocationId != nil && *req.LocationId > 0 { + targetLocationID = *req.LocationId + } + for _, kandang := range kandangs { + if kandang.LocationId != targetLocationID { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id)) + } + } if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage") } else if linked { @@ -412,18 +430,11 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if needFlockNameRegenerate { - nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), baseForGeneration) - if err != nil { - return err - } - newName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, nextSeq, &id) + newName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, 1, &id) if err != nil { return err } updateBody["flock_name"] = newName - if seq != existing.Period { - updateBody["period"] = seq - } } if len(updateBody) > 0 { @@ -467,7 +478,19 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if len(toAttach) > 0 { - if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach); err != nil { + var currentPeriod int + if err := dbTransaction.WithContext(c.Context()). + Table("project_flock_kandangs"). + Where("project_flock_id = ?", id). + Select("COALESCE(MAX(period), 0)"). + Scan(¤tPeriod).Error; err != nil { + return err + } + if currentPeriod <= 0 { + currentPeriod = 1 + } + + if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, currentPeriod); err != nil { return err } } @@ -743,57 +766,90 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u return total, nil } -func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, projectFlockKandangID uint) (*FlockPeriodSummary, error) { - if projectFlockKandangID == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") +// nextLocationPeriod computes the next period number for a given location +// based on the maximum period that has ever been used by any kandang in that location. +func (s projectflockService) nextLocationPeriod(ctx context.Context, tx *gorm.DB, locationID uint) (int, error) { + if locationID == 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, "location_id is required to compute period") } - pivot, err := s.pivotRepo().GetByID(c.Context(), projectFlockKandangID) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") + db := s.Repository.DB() + if tx != nil { + db = tx } + + var maxPeriod int + if err := db.WithContext(ctx). + Table("project_flock_kandangs pfk"). + Joins("JOIN kandangs k ON k.id = pfk.kandang_id"). + Where("k.location_id = ?", locationID). + Select("COALESCE(MAX(pfk.period), 0)"). + Scan(&maxPeriod).Error; err != nil { + s.Log.Errorf("Failed to compute max period for location %d: %+v", locationID, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute period for location") + } + + return maxPeriod + 1, nil +} + +func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) { + if len(projectIDs) == 0 { + return map[uint]int{}, nil + } + return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs) +} + +func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) { + if locationID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required") + } + + exists, err := s.Repository.LocationExists(c.Context(), locationID) if err != nil { - s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", projectFlockKandangID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") + s.Log.Errorf("Failed to validate location %d: %+v", locationID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate location") + } + if !exists { + return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } - var baseName string - var referenceFlock *entity.Flock - if pivot.ProjectFlock.Id != 0 { - baseName = pfutils.DeriveBaseName(pivot.ProjectFlock.FlockName) + type kandangPeriodRow struct { + Id uint + Name string + LatestPeriod int } - if strings.TrimSpace(baseName) != "" { - referenceFlock, err = s.FlockRepo.GetByName(c.Context(), baseName) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Failed to fetch flock %q: %+v", baseName, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock") - } + var rows []kandangPeriodRow + + db := s.Repository.DB().WithContext(c.Context()) + if err := db. + Table("kandangs AS k"). + Select("k.id, k.name, COALESCE(MAX(pfk.period), 0) AS latest_period"). + Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.kandang_id = k.id"). + Where("k.location_id = ?", locationID). + Where("k.deleted_at IS NULL"). + Group("k.id, k.name"). + Order("k.id ASC"). + Scan(&rows).Error; err != nil { + s.Log.Errorf("Failed to fetch kandang period summary for location %d: %+v", locationID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang period summary") } - if referenceFlock == nil { - referenceFlock = &entity.Flock{Name: pivot.ProjectFlock.FlockName} - } - - maxPeriod := pivot.ProjectFlock.Period - if strings.TrimSpace(baseName) != "" { - if headerMax, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), baseName); err != nil { - s.Log.Warnf("Unable to compute header period for base %q: %+v", baseName, err) - } else if headerMax > maxPeriod { - maxPeriod = headerMax + summaries := make([]KandangPeriodSummary, 0, len(rows)) + for _, row := range rows { + nextPeriod := 0 + if row.LatestPeriod > 0 { + nextPeriod = row.LatestPeriod + 1 } - if pivotMax, err := s.pivotRepo().MaxPeriodByBaseName(c.Context(), baseName); err != nil { - s.Log.Warnf("Unable to compute pivot period for base %q: %+v", baseName, err) - } else if pivotMax > maxPeriod { - maxPeriod = pivotMax - } + summaries = append(summaries, KandangPeriodSummary{ + Id: row.Id, + Name: row.Name, + Period: nextPeriod, + }) } - return &FlockPeriodSummary{ - Flock: *referenceFlock, - NextPeriod: maxPeriod + 1, - }, nil + return summaries, nil } func uniqueUintSlice(values []uint) []uint { @@ -869,7 +925,7 @@ func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint return newFlock, nil } -func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error { +func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, period int) error { if len(kandangIDs) == 0 { return nil } @@ -907,6 +963,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction * records = append(records, &entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: id, + Period: period, }) } if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil { diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 719e458a..5bc54762 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -19,7 +19,6 @@ type TransferLayingBaseDTO struct { type ProjectFlockSummaryDTO struct { Id uint `json:"id"` - Period int `json:"period"` Category string `json:"category"` } @@ -86,7 +85,6 @@ func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { return &ProjectFlockSummaryDTO{ Id: pf.Id, - Period: pf.Period, Category: pf.Category, } }