Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh

This commit is contained in:
aguhh18
2025-11-05 14:03:45 +07:00
47 changed files with 3474 additions and 1810 deletions
@@ -222,11 +222,11 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
}
func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
param := c.Params("flock_id")
param := c.Params("project_flock_kandang_id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Flock Id")
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id))
@@ -10,14 +10,17 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
)
type ProjectFlockBaseDTO struct {
Id uint `json:"id"`
Period int `json:"period"`
Id uint `json:"id"`
Period int `json:"period"`
FlockName string `json:"flock_name"`
}
type KandangWithProjectFlockIdDTO struct {
@@ -29,16 +32,16 @@ type KandangWithProjectFlockIdDTO struct {
type ProjectFlockListDTO struct {
ProjectFlockBaseDTO
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
// Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
}
type ProjectFlockDetailDTO struct {
@@ -57,32 +60,19 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
createdUser = &mapped
}
var kandangSummaries []KandangWithProjectFlockIdDTO
var kandangSummaries []kandangDTO.KandangBaseDTO
if len(e.Kandangs) > 0 {
kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs))
// Create a map of KandangId -> ProjectFlockKandangId from KandangHistory
kandangIdToProjectFlockKandangId := make(map[uint]uint)
for _, kh := range e.KandangHistory {
kandangIdToProjectFlockKandangId[kh.KandangId] = kh.Id
}
kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs))
for i, kandang := range e.Kandangs {
baseDTO := kandangDTO.ToKandangBaseDTO(kandang)
kandangSummaries[i] = KandangWithProjectFlockIdDTO{
Id: baseDTO.Id,
Name: baseDTO.Name,
Status: baseDTO.Status,
ProjectFlockKandangId: kandangIdToProjectFlockKandangId[kandang.Id],
}
kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang)
}
}
var flockSummary *flockDTO.FlockBaseDTO
if e.Flock.Id != 0 {
mapped := flockDTO.ToFlockBaseDTO(e.Flock)
flockSummary = &mapped
}
// var flockSummary *flockDTO.FlockBaseDTO
// if baseName := pfutils.DeriveBaseName(e.FlockName); baseName != "" {
// summary := flockDTO.FlockBaseDTO{Id: 0, Name: baseName}
// flockSummary = &summary
// }
var areaSummary *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
@@ -110,16 +100,16 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
return ProjectFlockListDTO{
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
Flock: flockSummary,
Area: areaSummary,
Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Approval: latestApproval,
// Flock: flockSummary,
Area: areaSummary,
Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Approval: latestApproval,
}
}
@@ -164,8 +154,9 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv
func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
return ProjectFlockBaseDTO{
Id: e.Id,
Period: e.Period,
Id: e.Id,
Period: e.Period,
FlockName: e.FlockName,
}
}
@@ -7,6 +7,7 @@ import (
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
@@ -48,15 +49,16 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
pfLocal := ProjectFlockWithPivotDTO{
ProjectFlockBaseDTO: ProjectFlockBaseDTO{
Id: e.ProjectFlock.Id,
Period: e.ProjectFlock.Period,
Id: e.ProjectFlock.Id,
Period: e.ProjectFlock.Period,
FlockName: e.ProjectFlock.FlockName,
},
Category: e.ProjectFlock.Category,
}
if e.ProjectFlock.Flock.Id != 0 {
mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock)
pfLocal.Flock = &mapped
if base := pfutils.DeriveBaseName(e.ProjectFlock.FlockName); base != "" {
summary := flockDTO.FlockBaseDTO{Id: 0, Name: base}
pfLocal.Flock = &summary
}
if e.ProjectFlock.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.ProjectFlock.Area)
@@ -75,11 +77,6 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
pfLocal.CreatedUser = &mapped
}
pivotMap := make(map[uint]uint)
for _, ph := range e.ProjectFlock.KandangHistory {
pivotMap[ph.KandangId] = ph.Id
}
for _, k := range e.ProjectFlock.Kandangs {
kb := kandangDTO.ToKandangBaseDTO(k)
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
@@ -3,20 +3,30 @@ package repository
import (
"context"
"errors"
"fmt"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
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]
GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error)
GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error)
GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error)
GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error)
IdExists(ctx context.Context, id uint) (bool, error)
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)
AreaExists(ctx context.Context, id uint) (bool, error)
FcrExists(ctx context.Context, id uint) (bool, error)
LocationExists(ctx context.Context, id uint) (bool, error)
}
type ProjectflockRepositoryImpl struct {
@@ -29,15 +39,11 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
}
}
func (r *ProjectflockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProjectFlock](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) {
func (r *ProjectflockRepositoryImpl) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) {
var records []entity.ProjectFlock
if err := r.DB().WithContext(ctx).
Unscoped().
Where("flock_id = ?", flockID).
Where(baseNameExpression+" = LOWER(?)", baseName).
Order("period ASC").
Find(&records).Error; err != nil {
return nil, err
@@ -45,10 +51,10 @@ func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID
return records, nil
}
func (r *ProjectflockRepositoryImpl) GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) {
func (r *ProjectflockRepositoryImpl) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) {
var record entity.ProjectFlock
err := r.DB().WithContext(ctx).
Where("flock_id = ?", flockID).
Where(baseNameExpression+" = LOWER(?)", baseName).
Order("period DESC").
First(&record).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -60,11 +66,11 @@ func (r *ProjectflockRepositoryImpl) GetActiveByFlock(ctx context.Context, flock
return &record, nil
}
func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) {
func (r *ProjectflockRepositoryImpl) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
var max int
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlock{}).
Where("flock_id = ?", flockID).
Where(baseNameExpression+" = LOWER(?)", baseName).
Select("COALESCE(MAX(period), 0)").
Scan(&max).Error; err != nil {
return 0, err
@@ -72,13 +78,13 @@ func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, fl
return max, nil
}
func (r *ProjectflockRepositoryImpl) GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) {
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("flock_id = ?", flockID).
Where(baseNameExpression+" = LOWER(?)", baseName).
Clauses(clause.Locking{Strength: "UPDATE"}).
Order("period DESC").
Limit(1).
@@ -91,3 +97,164 @@ func (r *ProjectflockRepositoryImpl) GetNextPeriodForFlock(ctx context.Context,
}
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)
return r.applyQueryFilters(db, params)
})
}
func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withDefaultRelations(db)
}
}
func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs")
}
func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB {
if params == nil {
return db
}
if params.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", params.AreaId)
}
if params.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", params.LocationId)
}
if params.Period > 0 {
db = db.Where("project_flocks.period = ?", params.Period)
}
if len(params.KandangIds) > 0 {
db = db.Where(`
EXISTS (
SELECT 1
FROM project_flock_kandangs pfk
WHERE pfk.project_flock_id = project_flocks.id
AND pfk.kandang_id IN ?
)`, params.KandangIds)
}
db = r.applySearchFilters(db, params.Search)
for _, expr := range r.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
}
func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB {
if rawSearch == "" {
return db
}
normalized := strings.ToLower(strings.TrimSpace(rawSearch))
if normalized == "" {
return db
}
likeQuery := "%" + normalized + "%"
return db.
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(`
LOWER(areas.name) LIKE ?
OR LOWER(project_flocks.category) LIKE ?
OR LOWER(fcrs.name) LIKE ?
OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ?
OR LOWER(created_users.name) LIKE ?
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 kandangs
WHERE kandangs.project_flock_id = project_flocks.id
AND LOWER(kandangs.name) LIKE ?
)
`,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
)
}
func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Area](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) FcrExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Fcr](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) LocationExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Location](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder string) []string {
direction := "ASC"
if strings.ToLower(sortOrder) == "desc" {
direction = "DESC"
}
switch sortBy {
case "area":
return []string{
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "location":
return []string{
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "kandangs":
return []string{
fmt.Sprintf("(SELECT COUNT(*) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "period":
return []string{
fmt.Sprintf("project_flocks.period %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
default:
return []string{
"project_flocks.created_at DESC",
"project_flocks.updated_at DESC",
}
}
}
func (r *ProjectflockRepositoryImpl) ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) {
var count int64
q := r.DB().WithContext(ctx).Model(&entity.ProjectFlock{}).Where("flock_name = ?", flockName)
if excludeID != nil && *excludeID != 0 {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
@@ -2,6 +2,7 @@ package repository
import (
"context"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -14,6 +15,10 @@ type ProjectFlockKandangRepository interface {
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
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)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
IdExists(ctx context.Context, id uint) (bool, error)
DB() *gorm.DB
@@ -23,6 +28,8 @@ type projectFlockKandangRepositoryImpl struct {
db *gorm.DB
}
const flockBaseNameExpression = "LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')))"
func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository {
return &projectFlockKandangRepositoryImpl{db: db}
}
@@ -47,7 +54,6 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
var records []entity.ProjectFlockKandang
if err := r.db.WithContext(ctx).
Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
@@ -80,7 +86,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
record := new(entity.ProjectFlockKandang)
if err := r.db.WithContext(ctx).
Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
@@ -102,7 +107,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
if err := r.db.WithContext(ctx).
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
@@ -118,3 +122,62 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
}
return record, nil
}
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
if len(kandangIDs) == 0 {
return nil, nil
}
var existing []uint
err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
Pluck("kandang_id", &existing).Error
return existing, err
}
func (r *projectFlockKandangRepositoryImpl) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) {
if len(kandangIDs) == 0 {
return false, nil
}
q := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("kandang_id IN ?", kandangIDs)
if exceptProjectID != nil {
q = q.Where("project_flock_id <> ?", *exceptProjectID)
}
var count int64
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *projectFlockKandangRepositoryImpl) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) {
if len(kandangIDs) == 0 {
return nil, nil
}
var kandangs []entity.Kandang
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("pfk.kandang_id AS id, COALESCE(k.name, '') AS name").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("LEFT JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("pfk.project_flock_id = ? AND pfk.kandang_id IN ?", projectFlockID, kandangIDs).
Group("pfk.kandang_id, k.name").
Scan(&kandangs).Error
return kandangs, err
}
func (r *projectFlockKandangRepositoryImpl) MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
if strings.TrimSpace(baseName) == "" {
return 0, nil
}
var max int
err := r.db.WithContext(ctx).
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)").
Scan(&max).Error
return max, err
}
@@ -27,5 +27,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("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary)
route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
}
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
@@ -14,6 +15,7 @@ import (
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -29,24 +31,24 @@ type ProjectflockService interface {
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, int, error)
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)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
}
type projectflockService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository
WarehouseRepo warehouseRepository.WarehouseRepository
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
ProjectFlockKandangRepo repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository
WarehouseRepo warehouseRepository.WarehouseRepository
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
PivotRepo repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
type FlockPeriodSummary struct {
@@ -58,23 +60,23 @@ func NewProjectflockService(
repo repository.ProjectflockRepository,
flockRepo flockRepository.FlockRepository,
kandangRepo kandangRepository.KandangRepository,
ProjectFlockKandangRepo repository.ProjectFlockKandangRepository,
pivotRepo repository.ProjectFlockKandangRepository,
warehouseRepo warehouseRepository.WarehouseRepository,
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) ProjectflockService {
return &projectflockService{
Log: utils.Log,
Validate: validate,
Repository: repo,
FlockRepo: flockRepo,
KandangRepo: kandangRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
ProjectFlockKandangRepo: ProjectFlockKandangRepo,
ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
Log: utils.Log,
Validate: validate,
Repository: repo,
FlockRepo: flockRepo,
KandangRepo: kandangRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
PivotRepo: pivotRepo,
ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
}
}
@@ -104,74 +106,11 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
offset := (params.Page - 1) * params.Limit
projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", params.AreaId)
}
if params.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", params.LocationId)
}
if params.Period > 0 {
db = db.Where("project_flocks.period = ?", params.Period)
}
if len(params.KandangIds) > 0 {
db = db.Where("EXISTS (SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds)
}
if params.Search != "" {
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
if normalizedSearch == "" {
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
}
likeQuery := "%" + normalizedSearch + "%"
db = db.
Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id").
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(`
LOWER(flocks.name) LIKE ?
OR LOWER(areas.name) LIKE ?
OR LOWER(project_flocks.category) LIKE ?
OR LOWER(fcrs.name) LIKE ?
OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ?
OR LOWER(created_users.name) LIKE ?
OR LOWER(created_users.email) LIKE ?
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
OR EXISTS (
SELECT 1 FROM kandangs
WHERE kandangs.project_flock_id = project_flocks.id
AND LOWER(kandangs.name) LIKE ?
)
`,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
)
}
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
})
projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
if err != nil {
s.Log.Errorf("Failed to get projectflocks: %+v", err)
return nil, 0, err
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks")
}
if s.ApprovalSvc != nil && len(projectflocks) > 0 {
@@ -198,13 +137,13 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
}
func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
projectflock, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
if err != nil {
s.Log.Errorf("Failed get projectflock by id: %+v", err)
return nil, err
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
if s.ApprovalSvc != nil {
@@ -240,15 +179,28 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
}
baseName := strings.TrimSpace(req.FlockName)
if baseName == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())},
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())},
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())},
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists},
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
); err != nil {
return nil, err
}
canonicalBase := baseName
if s.FlockRepo != nil {
baseFlock, err := s.ensureFlockByName(c.Context(), baseName)
if err != nil {
return nil, err
}
canonicalBase = baseFlock.Name
}
kandangIDs := uniqueUintSlice(req.KandangIds)
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
if err != nil {
@@ -260,14 +212,14 @@ 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.ProjectFlockId != nil {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah memiliki project flock", kandang.Name))
}
// 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")
} else if linked {
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
}
createBody := &entity.ProjectFlock{
FlockId: req.FlockId,
AreaId: req.AreaId,
Category: cat,
FcrId: req.FcrId,
@@ -278,11 +230,16 @@ 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)
period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId)
nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), canonicalBase)
if err != nil {
return err
}
createBody.Period = period
generatedName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, nextSeq, nil)
if err != nil {
return err
}
createBody.FlockName = generatedName
createBody.Period = seq
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
return err
@@ -308,11 +265,14 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
}
s.Log.Errorf("Failed to create projectflock: %+v", err)
return nil, err
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project flock")
}
return s.GetOne(c, createBody.Id)
@@ -323,7 +283,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, err
}
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
@@ -334,15 +294,28 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
updateBody := make(map[string]any)
hasBodyChanges := false
var relationChecks []commonSvc.RelationCheck
existingBase := pfutils.DeriveBaseName(existing.FlockName)
targetBaseName := existingBase
needFlockNameRegenerate := false
if req.FlockId != nil {
updateBody["flock_id"] = *req.FlockId
hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Flock",
ID: req.FlockId,
Exists: relationExistsChecker[entity.Flock](s.Repository.DB()),
})
if req.FlockName != nil {
trimmed := strings.TrimSpace(*req.FlockName)
if trimmed == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
canonicalBase := trimmed
if s.FlockRepo != nil {
flockEntity, err := s.ensureFlockByName(c.Context(), trimmed)
if err != nil {
return nil, err
}
canonicalBase = flockEntity.Name
}
if !strings.EqualFold(canonicalBase, existingBase) {
needFlockNameRegenerate = true
targetBaseName = canonicalBase
hasBodyChanges = true
}
}
if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId
@@ -350,7 +323,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Area",
ID: req.AreaId,
Exists: relationExistsChecker[entity.Area](s.Repository.DB()),
Exists: s.Repository.AreaExists,
})
}
if req.Category != nil {
@@ -367,7 +340,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "FCR",
ID: req.FcrId,
Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()),
Exists: s.Repository.FcrExists,
})
}
if req.LocationId != nil {
@@ -376,7 +349,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Location",
ID: req.LocationId,
Exists: relationExistsChecker[entity.Location](s.Repository.DB()),
Exists: s.Repository.LocationExists,
})
}
@@ -404,11 +377,12 @@ 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")
}
for _, k := range kandangs {
if k.ProjectFlockId != nil && *k.ProjectFlockId != id {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah terikat dengan project flock lain", k.Name))
}
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 {
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
}
}
hasChanges := hasBodyChanges || hasKandangChanges
@@ -419,6 +393,29 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction)
baseForGeneration := targetBaseName
if strings.TrimSpace(baseForGeneration) == "" {
baseForGeneration = existingBase
}
if strings.TrimSpace(baseForGeneration) == "" {
baseForGeneration = strings.TrimSpace(existing.FlockName)
}
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)
if err != nil {
return err
}
updateBody["flock_name"] = newName
if seq != existing.Period {
updateBody["period"] = seq
}
}
if len(updateBody) > 0 {
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return err
@@ -507,7 +504,10 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
s.Log.Errorf("Failed to update projectflock %d: %+v", id, err)
return nil, err
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock")
}
return s.GetOne(c, id)
@@ -611,7 +611,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
}
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
@@ -645,28 +645,70 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
return fiberErr
}
s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err)
return err
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete project flock")
}
return nil
}
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, int, error) {
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) {
availableStock, err := s.GetAvailableDocQuantity(ctx, kandangID)
if err != nil {
return nil, 0, err
}
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
s.Log.Errorf("Failed to fetch project_flock_kandang by project %d and kandang %d: %+v", projectFlockID, kandangID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
if err != nil {
return nil, 0, err
}
return projectFlockKandang, int(availableStock), nil
return pfk, availableQuantity, nil
}
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
idStr = strings.TrimSpace(idStr)
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
kandangIdStr = strings.TrimSpace(kandangIdStr)
if idStr != "" {
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", id, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
if err != nil {
return nil, 0, err
}
return pfk, availableQuantity, nil
}
if projectFlockIdStr == "" || kandangIdStr == "" {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
}
pfid, err := strconv.Atoi(projectFlockIdStr)
if err != nil || pfid <= 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
kid, err := strconv.Atoi(kandangIdStr)
if err != nil || kid <= 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
}
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
@@ -676,14 +718,7 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
return 0, err
}
var productWarehouses []entity.ProductWarehouse
err = s.ProductWarehouseRepo.DB().
WithContext(ctx.Context()).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", wh.Id).
Order("created_at DESC").
Find(&productWarehouses).Error
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "DOC", wh.Id)
if err != nil {
return 0, err
}
@@ -695,26 +730,55 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
return total, nil
}
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) {
flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
})
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Flock not found")
}
if err != nil {
s.Log.Errorf("Failed get flock %d for period summary: %+v", flockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
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")
}
maxPeriod, err := s.Repository.GetMaxPeriodByFlock(c.Context(), flockID)
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")
}
if err != nil {
s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period")
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")
}
var baseName string
var referenceFlock *entity.Flock
if pivot.ProjectFlock.Id != 0 {
baseName = pfutils.DeriveBaseName(pivot.ProjectFlock.FlockName)
}
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")
}
}
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
}
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
}
}
return &FlockPeriodSummary{
Flock: *flock,
Flock: *referenceFlock,
NextPeriod: maxPeriod + 1,
}, nil
}
@@ -732,45 +796,64 @@ func uniqueUintSlice(values []uint) []uint {
return result
}
func relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool, error) {
return func(ctx context.Context, id uint) (bool, error) {
return commonRepo.Exists[T](ctx, db, id)
func (s projectflockService) generateSequentialFlockName(ctx context.Context, repo repository.ProjectflockRepository, baseName string, startNumber int, excludeID *uint) (string, int, error) {
name := strings.TrimSpace(baseName)
if name == "" {
return "", 0, fiber.NewError(fiber.StatusBadRequest, "Base flock name cannot be empty")
}
number := startNumber
if number <= 0 {
number = 1
}
attempts := 0
for {
candidate := fmt.Sprintf("%s %03d", name, number)
exists, err := repo.ExistsByFlockName(ctx, candidate, excludeID)
if err != nil {
s.Log.Errorf("Failed checking project flock name uniqueness for %q: %+v", candidate, err)
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate flock name")
}
if !exists {
return candidate, number, nil
}
number++
attempts++
if attempts > 9999 {
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Unable to generate unique flock name")
}
}
}
func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []string {
direction := "ASC"
if strings.ToLower(sortOrder) == "desc" {
direction = "DESC"
func (s projectflockService) ensureFlockByName(ctx context.Context, name string) (*entity.Flock, error) {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
switch sortBy {
case "area":
return []string{
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "location":
return []string{
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "kandangs":
return []string{
fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "period":
return []string{
fmt.Sprintf("project_flocks.period %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
default:
return []string{
"project_flocks.created_at DESC",
"project_flocks.updated_at DESC",
}
flock, err := s.FlockRepo.GetByName(ctx, trimmed)
if err == nil {
return flock, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch flock by name %q: %+v", trimmed, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
}
newFlock := &entity.Flock{
Name: trimmed,
CreatedBy: 1, // TODO: replace with authenticated user
}
if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return s.FlockRepo.GetByName(ctx, trimmed)
}
s.Log.Errorf("Failed to create flock %q: %+v", trimmed, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
}
return newFlock, nil
}
func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error {
@@ -778,24 +861,45 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
return nil
}
if err := dbTransaction.Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs).
Updates(map[string]any{
"project_flock_id": projectFlockID,
"status": string(utils.KandangStatusPengajuan),
}).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusPengajuan); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
}
ProjectFlockKandangRepo := s.ProjectFlockKandangRepoWithTx(dbTransaction)
records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
for i, id := range kandangIDs {
records[i] = &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: id,
already, err := s.pivotRepoWithTx(dbTransaction).ListExistingKandangIDs(ctx, projectFlockID, kandangIDs)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot")
}
exists := make(map[uint]struct{}, len(already))
for _, id := range already {
exists[id] = struct{}{}
}
var toAttach []uint
seen := make(map[uint]struct{}, len(kandangIDs))
for _, id := range kandangIDs {
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
if _, ok := exists[id]; !ok {
toAttach = append(toAttach, id)
}
}
if err := ProjectFlockKandangRepo.CreateMany(ctx, records); err != nil {
if len(toAttach) == 0 {
return nil
}
records := make([]*entity.ProjectFlockKandang, 0, len(toAttach))
for _, id := range toAttach {
records = append(records, &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: id,
})
}
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terhubung dengan project flock ini")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
}
return nil
@@ -806,26 +910,55 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return nil
}
updates := map[string]any{"project_flock_id": nil}
blocked, err := s.pivotRepoWithTx(dbTransaction).FindKandangsWithRecordings(ctx, projectFlockID, kandangIDs)
if err != nil {
s.Log.Errorf("Failed to check recordings before detaching kandangs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandang detachment")
}
if len(blocked) > 0 {
names := make([]string, 0, len(blocked))
for _, item := range blocked {
label := fmt.Sprintf("ID %d", item.Id)
if strings.TrimSpace(item.Name) != "" {
label = fmt.Sprintf("%s (%s)", label, item.Name)
}
names = append(names, label)
}
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
}
if resetStatus {
updates["status"] = string(utils.KandangStatusNonActive)
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
}
}
if err := dbTransaction.Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs).
Updates(updates).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
}
if err := s.ProjectFlockKandangRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
}
return nil
}
func (s projectflockService) ProjectFlockKandangRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
if s.ProjectFlockKandangRepo == nil {
return repository.NewProjectFlockKandangRepository(dbTransaction)
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
if dbTransaction == nil {
return s.pivotRepo()
}
return s.ProjectFlockKandangRepo.WithTx(dbTransaction)
return s.pivotRepo().WithTx(dbTransaction)
}
func (s projectflockService) pivotRepo() repository.ProjectFlockKandangRepository {
if s.PivotRepo != nil {
return s.PivotRepo
}
return repository.NewProjectFlockKandangRepository(s.Repository.DB())
}
func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.KandangRepository {
if tx != nil {
return kandangRepository.NewKandangRepository(tx)
}
if s.KandangRepo != nil {
return s.KandangRepo
}
return kandangRepository.NewKandangRepository(s.Repository.DB())
}
@@ -0,0 +1,25 @@
package utils
import (
"strconv"
"strings"
)
// DeriveBaseName removes trailing numeric tokens from the flock name.
func DeriveBaseName(name string) string {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return ""
}
parts := strings.Fields(trimmed)
for len(parts) > 0 {
if _, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
parts = parts[:len(parts)-1]
continue
}
break
}
return strings.TrimSpace(strings.Join(parts, " "))
}
@@ -1,7 +1,7 @@
package validation
type Create struct {
FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"`
FlockName string `json:"flock_name" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
@@ -10,7 +10,7 @@ type Create struct {
}
type Update struct {
FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"`
FlockName *string `json:"flock_name,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
Category *string `json:"category,omitempty" validate:"omitempty"`
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`