mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into refactor-to-serve/with-middleware
This commit is contained in:
@@ -202,21 +202,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
||||
if query.TransactionType != "" {
|
||||
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||
}
|
||||
if query.ProductID > 0 {
|
||||
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
|
||||
Where("product_warehouses.product_id = ?", query.ProductID)
|
||||
}
|
||||
|
||||
if query.WarehouseID > 0 {
|
||||
if query.ProductID > 0 {
|
||||
|
||||
db = db.Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
|
||||
} else {
|
||||
|
||||
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
|
||||
Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
|
||||
}
|
||||
}
|
||||
db = s.StockLogsRepository.ApplyProductWarehouseFilters(db, uint(query.ProductID), uint(query.WarehouseID))
|
||||
|
||||
return db.Order("created_at DESC")
|
||||
})
|
||||
|
||||
+5
@@ -28,6 +28,11 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
ProductId: uint(c.QueryInt("product_id", 0)),
|
||||
WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
|
||||
Flags: c.Query("flags", ""),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
result, totalResults, err := u.ProductWarehouseService.GetAll(c, query)
|
||||
|
||||
+89
-15
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -16,23 +17,36 @@ type ProductWarehouseRepository interface {
|
||||
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
|
||||
ExistsByID(ctx context.Context, id uint) (bool, error)
|
||||
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
|
||||
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
|
||||
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
||||
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
||||
}
|
||||
|
||||
type ProductWarehouseRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.ProductWarehouse]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
|
||||
return &ProductWarehouseRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
|
||||
return repository.Exists[entity.Product](ctx, r.DB(), productId)
|
||||
}
|
||||
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
|
||||
return repository.Exists[entity.Warehouse](ctx, r.DB(), warehouseId)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
|
||||
var count int64
|
||||
query := r.db.WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||
query := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId)
|
||||
if excludeID != nil {
|
||||
query = query.Where("id != ?", *excludeID)
|
||||
@@ -43,20 +57,9 @@ func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Cont
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
|
||||
return repository.Exists[entity.Product](ctx, r.db, productId)
|
||||
}
|
||||
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
|
||||
return repository.Exists[entity.Warehouse](ctx, r.db, warehouseId)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProductWarehouse{}).
|
||||
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
|
||||
Count(&count).Error; err != nil {
|
||||
@@ -72,3 +75,74 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
|
||||
}
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
||||
var productWarehouses []entity.ProductWarehouse
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("product_warehouses").
|
||||
Select("product_warehouses.*").
|
||||
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 = ?", categoryCode, warehouseId).
|
||||
Order("product_warehouses.created_at DESC").
|
||||
Find(&productWarehouses).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return productWarehouses, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
query := r.DB()
|
||||
if db != nil {
|
||||
query = db
|
||||
}
|
||||
fmt.Println(warehouseId)
|
||||
err := query.WithContext(ctx).
|
||||
Table("product_warehouses").
|
||||
Select("product_warehouses.*").
|
||||
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 = ?", categoryCode, warehouseId).
|
||||
Order("product_warehouses.created_at DESC").
|
||||
First(&productWarehouse).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB {
|
||||
if len(flags) == 0 {
|
||||
return db
|
||||
}
|
||||
|
||||
return db.
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
|
||||
Where("flags.name IN ?", flags)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error {
|
||||
if len(deltas) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
base := r.DB().WithContext(ctx)
|
||||
if modifier != nil {
|
||||
base = modifier(base)
|
||||
}
|
||||
|
||||
for id, delta := range deltas {
|
||||
if delta == 0 {
|
||||
continue
|
||||
}
|
||||
if err := base.Model(&entity.ProductWarehouse{}).
|
||||
Where("id = ?", id).
|
||||
Update("quantity", gorm.Expr("COALESCE(quantity,0) + ?", delta)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,8 +49,30 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if params.ProductId > 0 {
|
||||
isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if !isProductExist {
|
||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
|
||||
}
|
||||
}
|
||||
|
||||
if params.WarehouseId > 0 {
|
||||
isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), params.WarehouseId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if !isWarehouseExist {
|
||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
|
||||
}
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
cleanFlags := utils.ParseFlags(params.Flags)
|
||||
|
||||
productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
|
||||
@@ -62,6 +84,8 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
||||
db = db.Where("warehouse_id = ?", params.WarehouseId)
|
||||
}
|
||||
|
||||
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
|
||||
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
|
||||
+5
-4
@@ -13,8 +13,9 @@ type Update struct {
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
|
||||
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
|
||||
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
|
||||
Flags string `query:"flags" validate:"omitempty"`
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *AreaController) GetAll(c *fiber.Ctx) error {
|
||||
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.AreaService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *BankController) GetAll(c *fiber.Ctx) error {
|
||||
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.BankService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type BankRepository interface {
|
||||
repository.BaseRepository[entity.Bank]
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
AccountNumberExists(ctx context.Context, accountNumber string, excludeID *uint) (bool, error)
|
||||
}
|
||||
|
||||
type BankRepositoryImpl struct {
|
||||
@@ -28,3 +29,7 @@ func NewBankRepository(db *gorm.DB) BankRepository {
|
||||
func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID)
|
||||
}
|
||||
|
||||
func (r *BankRepositoryImpl) AccountNumberExists(ctx context.Context, accountNumber string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByField[entity.Bank](ctx, r.db, "account_number", accountNumber, excludeID)
|
||||
}
|
||||
|
||||
@@ -87,6 +87,13 @@ func (s *bankService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.B
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name))
|
||||
}
|
||||
|
||||
if exists, err := s.Repository.AccountNumberExists(c.Context(), req.AccountNumber, nil); err != nil {
|
||||
s.Log.Errorf("Failed to check bank account number: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank account number")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with account number %s already exists", req.AccountNumber))
|
||||
}
|
||||
|
||||
createBody := &entity.Bank{
|
||||
Name: req.Name,
|
||||
Alias: req.Alias,
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *CustomerController) GetAll(c *fiber.Ctx) error {
|
||||
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.CustomerService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *FcrController) GetAll(c *fiber.Ctx) error {
|
||||
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.FcrService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *FlockController) GetAll(c *fiber.Ctx) error {
|
||||
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.FlockService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type FlockRepository interface {
|
||||
repository.BaseRepository[entity.Flock]
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
GetByName(ctx context.Context, name string) (*entity.Flock, error)
|
||||
}
|
||||
|
||||
type FlockRepositoryImpl struct {
|
||||
@@ -28,3 +29,15 @@ func NewFlockRepository(db *gorm.DB) FlockRepository {
|
||||
func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID)
|
||||
}
|
||||
|
||||
func (r *FlockRepositoryImpl) GetByName(ctx context.Context, name string) (*entity.Flock, error) {
|
||||
var flock entity.Flock
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("LOWER(name) = LOWER(?)", name).
|
||||
Where("deleted_at IS NULL").
|
||||
First(&flock).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &flock, nil
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ func (u *KandangController) GetAll(c *fiber.Ctx) error {
|
||||
PicId: c.QueryInt("pic_id", 0),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
result, totalResults, err := u.KandangService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -20,6 +20,7 @@ type KandangRepository interface {
|
||||
HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error)
|
||||
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
|
||||
UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error
|
||||
UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error
|
||||
}
|
||||
|
||||
type KandangRepositoryImpl struct {
|
||||
@@ -122,3 +123,14 @@ func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, p
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *KandangRepositoryImpl) UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error {
|
||||
if len(kandangIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entity.Kandang{}).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Where("deleted_at IS NULL").
|
||||
Update("status", string(status)).Error
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ func (u *LocationController) GetAll(c *fiber.Ctx) error {
|
||||
AreaId: c.QueryInt("area_id", 0),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
result, totalResults, err := u.LocationService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *NonstockController) GetAll(c *fiber.Ctx) error {
|
||||
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.NonstockService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *ProductCategoryController) GetAll(c *fiber.Ctx) error {
|
||||
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.ProductCategoryService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -30,6 +30,10 @@ func (u *ProductController) GetAll(c *fiber.Ctx) error {
|
||||
ProductCategoryID: c.QueryInt("product_category_id", 0),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
result, totalResults, err := u.ProductService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *SupplierController) GetAll(c *fiber.Ctx) error {
|
||||
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.SupplierService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type SupplierRepository interface {
|
||||
repository.BaseRepository[entity.Supplier]
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
AliasExists(ctx context.Context, alias string, excludeID *uint) (bool, error)
|
||||
}
|
||||
|
||||
type SupplierRepositoryImpl struct {
|
||||
@@ -28,3 +29,7 @@ func NewSupplierRepository(db *gorm.DB) SupplierRepository {
|
||||
func (r *SupplierRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.Supplier](ctx, r.db, name, excludeID)
|
||||
}
|
||||
|
||||
func (r *SupplierRepositoryImpl) AliasExists(ctx context.Context, alias string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByField[entity.Supplier](ctx, r.db, "alias", alias, excludeID)
|
||||
}
|
||||
|
||||
@@ -88,6 +88,13 @@ func (s *supplierService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with name %s already exists", req.Name))
|
||||
}
|
||||
|
||||
if exists, err := s.Repository.AliasExists(c.Context(), strings.TrimSpace(strings.ToUpper(req.Alias)), nil); err != nil {
|
||||
s.Log.Errorf("Failed to check supplier alias: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier alias")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with alias %s already exists", strings.TrimSpace(strings.ToUpper(req.Alias))))
|
||||
}
|
||||
|
||||
typ := strings.ToUpper(req.Type)
|
||||
if !utils.IsValidCustomerSupplierType(typ) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier type")
|
||||
@@ -143,6 +150,12 @@ func (s supplierService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
|
||||
}
|
||||
|
||||
if req.Alias != nil {
|
||||
if exists, err := s.Repository.AliasExists(c.Context(), strings.TrimSpace(strings.ToUpper(*req.Alias)), &id); err != nil {
|
||||
s.Log.Errorf("Failed to check supplier alias: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier alias")
|
||||
} else if exists {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with alias %s already exists", strings.TrimSpace(strings.ToUpper(*req.Alias))))
|
||||
}
|
||||
updateBody["alias"] = strings.TrimSpace(strings.ToUpper(*req.Alias))
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ func (u *UomController) GetAll(c *fiber.Ctx) error {
|
||||
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.UomService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -30,6 +30,10 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
|
||||
AreaId: c.QueryInt("area_id", 0),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
result, totalResults, err := u.WarehouseService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||
kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
@@ -88,9 +89,9 @@ func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO {
|
||||
|
||||
func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
|
||||
var flock *flockBaseDTO.FlockBaseDTO
|
||||
if e.Flock.Id != 0 {
|
||||
mapped := flockBaseDTO.ToFlockBaseDTO(e.Flock)
|
||||
flock = &mapped
|
||||
if base := pfutils.DeriveBaseName(e.FlockName); base != "" {
|
||||
summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base}
|
||||
flock = &summary
|
||||
}
|
||||
var area *areaBaseDTO.AreaBaseDTO
|
||||
if e.Area.Id != 0 {
|
||||
|
||||
@@ -63,7 +63,6 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("ProjectFlockKandang.Kandang.Location.Area").
|
||||
Preload("ProjectFlockKandang.Kandang.Pic").
|
||||
Preload("ProjectFlockKandang.ProjectFlock").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Flock").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Area").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Location").
|
||||
@@ -121,14 +120,8 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var productWarehouses []entity.ProductWarehouse
|
||||
err = s.ProductWarehouseRepo.DB().
|
||||
WithContext(c.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", warehouse.Id).
|
||||
Order("created_at DESC").
|
||||
Find(&productWarehouses).Error
|
||||
// move complex DB query into repository for cleaner service
|
||||
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get product warehouses: %+v", err)
|
||||
return nil, err
|
||||
@@ -136,8 +129,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
if len(productWarehouses) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
|
||||
}
|
||||
|
||||
// Jumlahkan semua quantity DOC
|
||||
totalQuantity := 0.0
|
||||
for _, pw := range productWarehouses {
|
||||
totalQuantity += pw.Quantity
|
||||
@@ -147,7 +138,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
|
||||
}
|
||||
|
||||
// Buat satu chickin dengan total quantity
|
||||
chickinDate, err := utils.ParseDateString(req.ChickInDate)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to parse chickin date: %+v", err)
|
||||
@@ -157,7 +147,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
ProjectFlockKandangId: projectflockkandang.Id,
|
||||
ChickInDate: chickinDate,
|
||||
Quantity: totalQuantity,
|
||||
Note: "",
|
||||
Note: req.Note,
|
||||
CreatedBy: 1, //todo: ganti dengan user login
|
||||
}
|
||||
err = s.Repository.CreateOne(c.Context(), newChickin, nil)
|
||||
@@ -176,7 +166,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add ke detail chickin
|
||||
newChickinDetail := &entity.ProjectChickinDetail{
|
||||
ProjectChickinId: newChickin.Id,
|
||||
ProductWarehouseId: pw.Id,
|
||||
@@ -232,6 +221,9 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
if req.ChickInDate != "" {
|
||||
updateBody["chick_in_date"] = req.ChickInDate
|
||||
}
|
||||
if req.Note != "" {
|
||||
updateBody["note"] = req.Note
|
||||
}
|
||||
if len(updateBody) == 0 {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
@@ -293,7 +285,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return rollback(err)
|
||||
}
|
||||
|
||||
// helper: restore quantities from details; returns (restored bool, error)
|
||||
restoreFromDetails := func() (bool, error) {
|
||||
var details []entity.ProjectChickinDetail
|
||||
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
|
||||
@@ -348,15 +339,12 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return rollback(err)
|
||||
}
|
||||
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
err = tx.WithContext(c.Context()).Table("product_warehouses").
|
||||
Select("product_warehouses.*").
|
||||
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", warehouse.Id).
|
||||
Order("product_warehouses.created_at DESC").
|
||||
First(&productWarehouse).Error
|
||||
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(
|
||||
c.Context(),
|
||||
"DOC",
|
||||
warehouse.Id,
|
||||
tx,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse"))
|
||||
|
||||
@@ -3,10 +3,12 @@ package validation
|
||||
type Create struct {
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
|
||||
Note string `json:"note" validate:"omitempty`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
|
||||
Note string `json:"note" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
|
||||
@@ -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))
|
||||
@@ -246,17 +246,39 @@ func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||
projectFlockIdStr := c.Query("project_flock_id", "")
|
||||
kandangIdStr := c.Query("kandang_id", "")
|
||||
projectFlockId := c.QueryInt("project_flock_id", 0)
|
||||
kandangId := c.QueryInt("kandang_id", 0)
|
||||
|
||||
result, err := u.ProjectflockService.GetProjectFlockKandangByParams(c, "", projectFlockIdStr, kandangIdStr)
|
||||
if projectFlockId == 0 || kandangId == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id")
|
||||
}
|
||||
|
||||
result, availableStock, err := u.ProjectflockService.GetProjectFlockKandangByProjectAndKandang(c, uint(projectFlockId), uint(kandangId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dtoResult := dto.ToProjectFlockKandangDTO(*result)
|
||||
dtoResult.AvailableQuantity = float64(availableStock)
|
||||
|
||||
// populate available quantity for each kandang inside project_flock
|
||||
if dtoResult.ProjectFlock != nil {
|
||||
for i := range dtoResult.ProjectFlock.Kandangs {
|
||||
kand := &dtoResult.ProjectFlock.Kandangs[i]
|
||||
if kand.Id == 0 {
|
||||
continue
|
||||
}
|
||||
if q, qerr := u.ProjectflockService.GetAvailableDocQuantity(c, kand.Id); qerr == nil {
|
||||
kand.AvailableQuantity = q
|
||||
}
|
||||
}
|
||||
// remove inner kandangs from project_flock to avoid duplication
|
||||
dtoResult.ProjectFlock.Kandangs = nil
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get projectflock kandang successfully",
|
||||
Data: dto.ToProjectFlockKandangDTO(*result)})
|
||||
Data: dtoResult})
|
||||
}
|
||||
|
||||
@@ -10,19 +10,21 @@ 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 ProjectFlockListDTO struct {
|
||||
ProjectFlockBaseDTO
|
||||
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
|
||||
// Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
|
||||
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
|
||||
Category string `json:"category"`
|
||||
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
|
||||
@@ -58,11 +60,11 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -90,7 +92,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
|
||||
|
||||
return ProjectFlockListDTO{
|
||||
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
|
||||
Flock: flockSummary,
|
||||
// Flock: flockSummary,
|
||||
Area: areaSummary,
|
||||
Kandangs: kandangSummaries,
|
||||
Category: e.Category,
|
||||
@@ -144,8 +146,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,13 +7,13 @@ 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"
|
||||
)
|
||||
|
||||
// internal DTO used only for lookup response: project flock with kandangs carrying pivot ids
|
||||
type KandangWithPivotDTO struct {
|
||||
kandangDTO.KandangBaseDTO
|
||||
ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty"`
|
||||
AvailableQuantity float64 `json:"available_quantity"`
|
||||
}
|
||||
|
||||
type ProjectFlockWithPivotDTO struct {
|
||||
@@ -28,11 +28,13 @@ type ProjectFlockWithPivotDTO struct {
|
||||
}
|
||||
|
||||
type ProjectFlockKandangDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProjectFlockId uint `json:"project_flock_id"`
|
||||
KandangId uint `json:"kandang_id"`
|
||||
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
|
||||
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
|
||||
Id uint `json:"id"`
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||
ProjectFlockId uint `json:"project_flock_id"`
|
||||
KandangId uint `json:"kandang_id"`
|
||||
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
|
||||
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
|
||||
AvailableQuantity float64 `json:"available_quantity"`
|
||||
}
|
||||
|
||||
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
||||
@@ -44,19 +46,19 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
||||
|
||||
var pf *ProjectFlockWithPivotDTO
|
||||
if e.ProjectFlock.Id != 0 {
|
||||
// build project flock with kandangs that include pivot ids
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
// fill related small summaries
|
||||
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,23 +77,11 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
||||
pfLocal.CreatedUser = &mapped
|
||||
}
|
||||
|
||||
// build pivot map
|
||||
pivotMap := make(map[uint]uint)
|
||||
for _, ph := range e.ProjectFlock.KandangHistory {
|
||||
pivotMap[ph.KandangId] = ph.Id
|
||||
}
|
||||
|
||||
// populate kandangs with pivot ids
|
||||
for _, k := range e.ProjectFlock.Kandangs {
|
||||
kb := kandangDTO.ToKandangBaseDTO(k)
|
||||
var pid *uint
|
||||
if v, ok := pivotMap[k.Id]; ok {
|
||||
vv := v
|
||||
pid = &vv
|
||||
}
|
||||
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
|
||||
KandangBaseDTO: kb,
|
||||
ProjectFlockKandangId: pid,
|
||||
KandangBaseDTO: kb,
|
||||
AvailableQuantity: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,10 +89,12 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
||||
}
|
||||
|
||||
return ProjectFlockKandangDTO{
|
||||
Id: e.Id,
|
||||
ProjectFlockId: e.ProjectFlockId,
|
||||
KandangId: e.KandangId,
|
||||
Kandang: kandang,
|
||||
ProjectFlock: pf,
|
||||
Id: e.Id,
|
||||
ProjectFlockKandangId: e.Id,
|
||||
ProjectFlockId: e.ProjectFlockId,
|
||||
KandangId: e.KandangId,
|
||||
Kandang: kandang,
|
||||
ProjectFlock: pf,
|
||||
AvailableQuantity: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import (
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
|
||||
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||
@@ -27,6 +29,8 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
||||
kandangRepo := rKandang.NewKandangRepository(db)
|
||||
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
||||
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
@@ -35,7 +39,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
||||
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
||||
}
|
||||
|
||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, approvalService, validate)
|
||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, approvalService, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ProjectflockRoutes(router, userService, projectflockService)
|
||||
|
||||
+184
-12
@@ -3,19 +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)
|
||||
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 {
|
||||
@@ -28,11 +39,11 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -40,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) {
|
||||
@@ -55,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
|
||||
@@ -67,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).
|
||||
@@ -86,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
|
||||
}
|
||||
|
||||
+66
-3
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
@@ -13,6 +14,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
|
||||
DB() *gorm.DB
|
||||
}
|
||||
@@ -21,6 +26,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}
|
||||
}
|
||||
@@ -45,7 +52,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").
|
||||
@@ -72,7 +78,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").
|
||||
@@ -91,7 +96,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").
|
||||
@@ -104,3 +108,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
|
||||
}
|
||||
|
||||
@@ -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("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary)
|
||||
|
||||
route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
|
||||
|
||||
}
|
||||
|
||||
@@ -10,10 +10,13 @@ import (
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
auth "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||
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,21 +32,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)
|
||||
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, 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
|
||||
PivotRepo 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 {
|
||||
@@ -56,31 +62,25 @@ func NewProjectflockService(
|
||||
flockRepo flockRepository.FlockRepository,
|
||||
kandangRepo kandangRepository.KandangRepository,
|
||||
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,
|
||||
PivotRepo: pivotRepo,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("Flock").
|
||||
Preload("Area").
|
||||
Preload("Fcr").
|
||||
Preload("Location").
|
||||
Preload("Kandangs")
|
||||
}
|
||||
|
||||
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
@@ -95,79 +95,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 project_flock_kandangs pfk
|
||||
WHERE pfk.project_flock_id = project_flocks.id
|
||||
AND pfk.kandang_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 {
|
||||
@@ -194,13 +126,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 {
|
||||
@@ -227,6 +159,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := actorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cat := strings.ToUpper(req.Category)
|
||||
if !utils.IsValidProjectFlockCategory(cat) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
|
||||
@@ -236,15 +173,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(), actorID, 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 {
|
||||
@@ -257,19 +207,13 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||
}
|
||||
// larang kalau ada yg sudah terikat ke project lain
|
||||
if linked, err := s.anyKandangLinkedToOtherProject(c.Context(), s.Repository.DB(), kandangIDs, nil); err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
actorID, err := actorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createBody := &entity.ProjectFlock{
|
||||
FlockId: req.FlockId,
|
||||
AreaId: req.AreaId,
|
||||
Category: cat,
|
||||
FcrId: req.FcrId,
|
||||
@@ -280,11 +224,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
|
||||
@@ -309,11 +258,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)
|
||||
@@ -324,7 +276,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
actorID, err := actorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -335,15 +292,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(), actorID, 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
|
||||
@@ -351,7 +321,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 {
|
||||
@@ -368,7 +338,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 {
|
||||
@@ -377,7 +347,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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -405,7 +375,7 @@ 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")
|
||||
}
|
||||
if linked, err := s.anyKandangLinkedToOtherProject(c.Context(), s.Repository.DB(), newKandangIDs, &id); err != nil {
|
||||
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")
|
||||
@@ -418,14 +388,32 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
actorID, authErr := actorIDFromContext(c)
|
||||
if authErr != nil {
|
||||
return nil, authErr
|
||||
}
|
||||
|
||||
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
|
||||
@@ -513,7 +501,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)
|
||||
@@ -621,7 +612,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")
|
||||
}
|
||||
@@ -655,38 +646,32 @@ 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) GetProjectFlockKandang(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, error) {
|
||||
// keep for backward compatibility; delegate to new consolidated method
|
||||
return s.GetProjectFlockKandangByParams(ctx, fmt.Sprintf("%d", id), "", "")
|
||||
}
|
||||
|
||||
func actorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||
user, ok := auth.AuthenticatedUser(c)
|
||||
if !ok || user == nil || user.Id == 0 {
|
||||
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||
}
|
||||
return user.Id, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
||||
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) {
|
||||
|
||||
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||
}
|
||||
return nil, err
|
||||
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")
|
||||
}
|
||||
return pfk, nil
|
||||
|
||||
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return pfk, availableQuantity, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error) {
|
||||
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)
|
||||
@@ -694,52 +679,107 @@ func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idSt
|
||||
if idStr != "" {
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
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, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||
}
|
||||
return nil, err
|
||||
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")
|
||||
}
|
||||
return pfk, nil
|
||||
|
||||
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return pfk, availableQuantity, nil
|
||||
}
|
||||
|
||||
if projectFlockIdStr == "" || kandangIdStr == "" {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
|
||||
}
|
||||
pfid, err := strconv.Atoi(projectFlockIdStr)
|
||||
if err != nil || pfid <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
kid, err := strconv.Atoi(kandangIdStr)
|
||||
if err != nil || kid <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||
}
|
||||
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
|
||||
|
||||
wh, err := s.WarehouseRepo.GetByKandangID(ctx.Context(), kandangID)
|
||||
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")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
maxPeriod, err := s.Repository.GetMaxPeriodByFlock(c.Context(), flockID)
|
||||
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "DOC", wh.Id)
|
||||
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")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
for _, pw := range productWarehouses {
|
||||
total += pw.Quantity
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
@@ -757,45 +797,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, actorID uint, 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 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",
|
||||
}
|
||||
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: actorID,
|
||||
}
|
||||
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 {
|
||||
@@ -803,20 +862,12 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := dbTransaction.
|
||||
Model(&entity.Kandang{}).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Updates(map[string]any{
|
||||
"status": string(utils.KandangStatusPengajuan),
|
||||
}).Error; err != nil {
|
||||
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusPengajuan); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||
}
|
||||
|
||||
var already []uint
|
||||
if err := dbTransaction.
|
||||
Table("project_flock_kandangs").
|
||||
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
|
||||
Pluck("kandang_id", &already).Error; err != nil {
|
||||
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))
|
||||
@@ -847,6 +898,9 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
||||
})
|
||||
}
|
||||
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
|
||||
@@ -857,13 +911,25 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
||||
return 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 {
|
||||
if err := dbTransaction.
|
||||
Model(&entity.Kandang{}).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Updates(map[string]any{
|
||||
"status": string(utils.KandangStatusNonActive),
|
||||
}).Error; err != nil {
|
||||
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||
}
|
||||
}
|
||||
@@ -875,22 +941,33 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
||||
}
|
||||
|
||||
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
|
||||
if s.PivotRepo == nil {
|
||||
return repository.NewProjectFlockKandangRepository(dbTransaction)
|
||||
if dbTransaction == nil {
|
||||
return s.pivotRepo()
|
||||
}
|
||||
return s.PivotRepo.WithTx(dbTransaction)
|
||||
return s.pivotRepo().WithTx(dbTransaction)
|
||||
}
|
||||
|
||||
func (s projectflockService) anyKandangLinkedToOtherProject(ctx context.Context, db *gorm.DB, kandangIDs []uint, exceptProjectID *uint) (bool, error) {
|
||||
q := db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Where("kandang_id IN ?", kandangIDs)
|
||||
if exceptProjectID != nil {
|
||||
q = q.Where("project_flock_id <> ?", *exceptProjectID)
|
||||
func (s projectflockService) pivotRepo() repository.ProjectFlockKandangRepository {
|
||||
if s.PivotRepo != nil {
|
||||
return s.PivotRepo
|
||||
}
|
||||
var count int64
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
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())
|
||||
}
|
||||
|
||||
func actorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||
user, ok := authmiddleware.AuthenticatedUser(c)
|
||||
if !ok || user == nil || user.Id == 0 {
|
||||
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||
}
|
||||
return user.Id, nil
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -146,6 +146,60 @@ func (u *RecordingController) UpdateOne(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *RecordingController) SubmitGrading(c *fiber.Ctx) error {
|
||||
req := new(validation.SubmitGrading)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.RecordingService.SubmitGrading(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Submit grading eggs successfully",
|
||||
Data: dto.ToRecordingDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *RecordingController) Approve(c *fiber.Ctx) error {
|
||||
req := new(validation.Approve)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
results, err := u.RecordingService.Approval(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
data interface{}
|
||||
message = "Submit recording approvals successfully"
|
||||
)
|
||||
|
||||
if len(results) == 1 {
|
||||
message = "Submit recording approval successfully"
|
||||
data = dto.ToRecordingDetailDTO(results[0])
|
||||
} else {
|
||||
data = dto.ToRecordingListDTOs(results)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *RecordingController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
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"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type RecordingBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||
RecordDatetime time.Time `json:"record_datetime"`
|
||||
RecordDate *time.Time `json:"record_date,omitempty"`
|
||||
Ontime bool `json:"ontime"`
|
||||
Day *int `json:"day,omitempty"`
|
||||
TotalDepletion *int `json:"total_depletion,omitempty"`
|
||||
CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"`
|
||||
DailyGain *float64 `json:"daily_gain,omitempty"`
|
||||
AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"`
|
||||
CumIntake *int64 `json:"cum_intake,omitempty"`
|
||||
FcrValue *float64 `json:"fcr_value,omitempty"`
|
||||
TotalChick *int64 `json:"total_chick,omitempty"`
|
||||
DailyDepletionRate *float64 `json:"daily_depletion_rate,omitempty"`
|
||||
CumDepletion *int `json:"cum_depletion,omitempty"`
|
||||
Id uint `json:"id"`
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||
RecordDatetime time.Time `json:"record_datetime"`
|
||||
Day *int `json:"day,omitempty"`
|
||||
ProjectFlockCategory *string `json:"project_flock_category,omitempty"`
|
||||
TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"`
|
||||
CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"`
|
||||
DailyGain *float64 `json:"daily_gain,omitempty"`
|
||||
AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"`
|
||||
CumIntake *int `json:"cum_intake,omitempty"`
|
||||
FcrValue *float64 `json:"fcr_value,omitempty"`
|
||||
TotalChickQty *float64 `json:"total_chick_qty,omitempty"`
|
||||
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
|
||||
EggGradingStatus *string `json:"egg_grading_status,omitempty"`
|
||||
EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"`
|
||||
EggGradingCompletedQty *int `json:"egg_grading_completed_qty,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingListDTO struct {
|
||||
@@ -39,30 +44,35 @@ type RecordingDetailDTO struct {
|
||||
BodyWeights []RecordingBodyWeightDTO `json:"body_weights"`
|
||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||
Stocks []RecordingStockDTO `json:"stocks"`
|
||||
Eggs []RecordingEggDTO `json:"eggs"`
|
||||
}
|
||||
|
||||
type RecordingBodyWeightDTO struct {
|
||||
Weight float64 `json:"weight"`
|
||||
Qty int `json:"qty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
AvgWeight float64 `json:"avg_weight"`
|
||||
Qty float64 `json:"qty"`
|
||||
TotalWeight float64 `json:"total_weight"`
|
||||
}
|
||||
|
||||
type RecordingDepletionDTO struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
Total int64 `json:"total"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
Qty float64 `json:"qty"`
|
||||
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingStockDTO struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
Increase *float64 `json:"increase,omitempty"`
|
||||
Decrease *float64 `json:"decrease,omitempty"`
|
||||
UsageAmount *int64 `json:"usage_amount,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
UsageAmount *float64 `json:"usage_amount,omitempty"`
|
||||
PendingQty *float64 `json:"pending_qty,omitempty"`
|
||||
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingEggDTO struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
Qty int `json:"qty"`
|
||||
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||
Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingProductWarehouseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProductId uint `json:"product_id"`
|
||||
@@ -71,36 +81,47 @@ type RecordingProductWarehouseDTO struct {
|
||||
WarehouseName string `json:"warehouse_name"`
|
||||
}
|
||||
|
||||
type RecordingEggGradingDTO struct {
|
||||
Grade string `json:"grade,omitempty"`
|
||||
Qty float64 `json:"qty"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
|
||||
recordDate := e.RecordDate
|
||||
if recordDate == nil {
|
||||
rd := time.Date(
|
||||
e.RecordDatetime.Year(),
|
||||
e.RecordDatetime.Month(),
|
||||
e.RecordDatetime.Day(),
|
||||
0, 0, 0, 0,
|
||||
e.RecordDatetime.Location(),
|
||||
)
|
||||
recordDate = &rd
|
||||
var projectFlockCategory *string
|
||||
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||
category := e.ProjectFlockKandang.ProjectFlock.Category
|
||||
if category != "" {
|
||||
projectFlockCategory = &category
|
||||
}
|
||||
}
|
||||
|
||||
latestApproval := defaultRecordingLatestApproval(e)
|
||||
if e.LatestApproval != nil {
|
||||
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||
latestApproval = snapshot
|
||||
}
|
||||
|
||||
gradingStatus, gradingPending, gradingCompleted := computeEggGradingStatus(e)
|
||||
|
||||
return RecordingBaseDTO{
|
||||
Id: e.Id,
|
||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||
RecordDatetime: e.RecordDatetime,
|
||||
RecordDate: recordDate,
|
||||
Ontime: e.Ontime == 1,
|
||||
Day: e.Day,
|
||||
TotalDepletion: e.TotalDepletion,
|
||||
CumDepletionRate: e.CumDepletionRate,
|
||||
DailyGain: e.DailyGain,
|
||||
AvgDailyGain: e.AvgDailyGain,
|
||||
CumIntake: e.CumIntake,
|
||||
FcrValue: e.FcrValue,
|
||||
TotalChick: e.TotalChick,
|
||||
DailyDepletionRate: e.DailyDepletionRate,
|
||||
CumDepletion: e.CumDepletion,
|
||||
Id: e.Id,
|
||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||
RecordDatetime: e.RecordDatetime,
|
||||
Day: e.Day,
|
||||
ProjectFlockCategory: projectFlockCategory,
|
||||
TotalDepletionQty: e.TotalDepletionQty,
|
||||
CumDepletionRate: e.CumDepletionRate,
|
||||
DailyGain: e.DailyGain,
|
||||
AvgDailyGain: e.AvgDailyGain,
|
||||
CumIntake: e.CumIntake,
|
||||
FcrValue: e.FcrValue,
|
||||
TotalChickQty: e.TotalChickQty,
|
||||
Approval: latestApproval,
|
||||
EggGradingStatus: gradingStatus,
|
||||
EggGradingPendingQty: gradingPending,
|
||||
EggGradingCompletedQty: gradingCompleted,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +154,7 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
||||
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
|
||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||
Eggs: ToRecordingEggDTOs(e.Eggs),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,9 +162,9 @@ func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBody
|
||||
result := make([]RecordingBodyWeightDTO, len(bodyWeights))
|
||||
for i, bw := range bodyWeights {
|
||||
result[i] = RecordingBodyWeightDTO{
|
||||
Weight: bw.Weight,
|
||||
Qty: bw.Qty,
|
||||
Notes: bw.Notes,
|
||||
AvgWeight: bw.AvgWeight,
|
||||
Qty: bw.Qty,
|
||||
TotalWeight: bw.TotalWeight,
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -153,8 +175,7 @@ func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []Recordin
|
||||
for i, d := range depletions {
|
||||
result[i] = RecordingDepletionDTO{
|
||||
ProductWarehouseId: d.ProductWarehouseId,
|
||||
Total: d.Total,
|
||||
Notes: d.Notes,
|
||||
Qty: d.Qty,
|
||||
ProductWarehouse: toRecordingProductWarehouseDTO(&d.ProductWarehouse),
|
||||
}
|
||||
}
|
||||
@@ -166,16 +187,43 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
|
||||
for i, s := range stocks {
|
||||
result[i] = RecordingStockDTO{
|
||||
ProductWarehouseId: s.ProductWarehouseId,
|
||||
Increase: s.Increase,
|
||||
Decrease: s.Decrease,
|
||||
UsageAmount: s.UsageAmount,
|
||||
Notes: s.Notes,
|
||||
UsageAmount: s.UsageQty,
|
||||
PendingQty: s.PendingQty,
|
||||
ProductWarehouse: toRecordingProductWarehouseDTO(&s.ProductWarehouse),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
|
||||
result := make([]RecordingEggDTO, len(eggs))
|
||||
for i, egg := range eggs {
|
||||
result[i] = RecordingEggDTO{
|
||||
ProductWarehouseId: egg.ProductWarehouseId,
|
||||
Qty: egg.Qty,
|
||||
ProductWarehouse: toRecordingProductWarehouseDTO(&egg.ProductWarehouse),
|
||||
Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToRecordingEggGradingDTOs(gradings []entity.GradingEgg) []RecordingEggGradingDTO {
|
||||
if len(gradings) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]RecordingEggGradingDTO, len(gradings))
|
||||
for i, grading := range gradings {
|
||||
result[i] = RecordingEggGradingDTO{
|
||||
Grade: grading.Grade,
|
||||
Qty: grading.Qty,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProductWarehouseDTO {
|
||||
if pw == nil || pw.Id == 0 {
|
||||
return nil
|
||||
@@ -196,3 +244,81 @@ func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProdu
|
||||
|
||||
return &dto
|
||||
}
|
||||
|
||||
const goodEggProductWarehouseID uint = 5
|
||||
|
||||
func computeEggGradingStatus(e entity.Recording) (*string, *int, *int) {
|
||||
goodEggs := filterGoodEggs(e.Eggs)
|
||||
if len(goodEggs) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
totalEggs := 0
|
||||
totalGraded := 0.0
|
||||
for _, egg := range goodEggs {
|
||||
totalEggs += egg.Qty
|
||||
for _, grading := range egg.GradingEggs {
|
||||
totalGraded += grading.Qty
|
||||
}
|
||||
}
|
||||
|
||||
if totalEggs == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
pendingFloat := float64(totalEggs) - totalGraded
|
||||
if pendingFloat < 0 {
|
||||
pendingFloat = 0
|
||||
}
|
||||
pendingInt := int(math.Round(pendingFloat))
|
||||
completedInt := int(math.Round(totalGraded))
|
||||
if completedInt < 0 {
|
||||
completedInt = 0
|
||||
}
|
||||
|
||||
if pendingInt > 0 {
|
||||
status := "GRADING_TELUR"
|
||||
return &status, &pendingInt, &completedInt
|
||||
}
|
||||
|
||||
status := "GRADING_SELESAI"
|
||||
zero := 0
|
||||
return &status, &zero, &completedInt
|
||||
}
|
||||
|
||||
func filterGoodEggs(eggs []entity.RecordingEgg) []entity.RecordingEgg {
|
||||
if len(eggs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingEgg, 0, len(eggs))
|
||||
for _, egg := range eggs {
|
||||
if egg.ProductWarehouseId == goodEggProductWarehouseID {
|
||||
result = append(result, egg)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalBaseDTO {
|
||||
result := approvalDTO.ApprovalBaseDTO{}
|
||||
|
||||
step := utils.RecordingStepPengajuan
|
||||
result.StepNumber = uint16(step)
|
||||
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowRecording, step); ok {
|
||||
result.StepName = label
|
||||
} else if label, ok := utils.RecordingApprovalSteps[step]; ok {
|
||||
result.StepName = label
|
||||
}
|
||||
|
||||
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||
result.ActionBy = userDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||
} else if e.CreatedBy != 0 {
|
||||
result.ActionBy = userDTO.UserBaseDTO{
|
||||
Id: e.CreatedBy,
|
||||
IdUser: int64(e.CreatedBy),
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package recordings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
@@ -18,11 +23,26 @@ type RecordingModule struct{}
|
||||
|
||||
func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowRecording, utils.RecordingApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register recording approval workflow: %v", err))
|
||||
}
|
||||
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
recordingService := sRecording.NewRecordingService(recordingRepo, projectFlockKandangRepo, productWarehouseRepo, validate)
|
||||
recordingService := sRecording.NewRecordingService(
|
||||
recordingRepo,
|
||||
projectFlockKandangRepo,
|
||||
productWarehouseRepo,
|
||||
projectFlockPopulationRepo,
|
||||
approvalRepo,
|
||||
approvalService,
|
||||
validate,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
RecordingRoutes(router, userService, recordingService)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package recordings
|
||||
|
||||
const (
|
||||
PermissionRecordingRead = "recording.read"
|
||||
PermissionRecordingCreate = "recording.write"
|
||||
PermissionRecordingUpdate = "recording.update"
|
||||
PermissionRecordingDelete = "recording.delete"
|
||||
)
|
||||
@@ -1,10 +1,12 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -22,11 +24,22 @@ type RecordingRepository interface {
|
||||
|
||||
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
|
||||
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
||||
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
||||
|
||||
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
||||
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
||||
ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error)
|
||||
|
||||
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error)
|
||||
CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error
|
||||
DeleteEggs(tx *gorm.DB, recordingID uint) error
|
||||
ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error)
|
||||
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
|
||||
CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error
|
||||
DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error
|
||||
|
||||
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
|
||||
|
||||
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error)
|
||||
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
|
||||
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
|
||||
GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error)
|
||||
@@ -58,13 +71,18 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("Stocks").
|
||||
Preload("Stocks.ProductWarehouse").
|
||||
Preload("Stocks.ProductWarehouse.Product").
|
||||
Preload("Stocks.ProductWarehouse.Warehouse")
|
||||
Preload("Stocks.ProductWarehouse.Warehouse").
|
||||
Preload("Eggs").
|
||||
Preload("Eggs.ProductWarehouse").
|
||||
Preload("Eggs.ProductWarehouse.Product").
|
||||
Preload("Eggs.ProductWarehouse.Warehouse").
|
||||
Preload("Eggs.GradingEggs")
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||
var days []int
|
||||
if err := tx.Model(&entity.Recording{}).
|
||||
Where("project_flock_id = ?", projectFlockKandangId).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||
Where("day IS NOT NULL").
|
||||
Pluck("day", &days).Error; err != nil {
|
||||
return 0, err
|
||||
@@ -94,6 +112,14 @@ func (r *RecordingRepositoryImpl) DeleteStocks(tx *gorm.DB, recordingID uint) er
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) {
|
||||
var items []entity.RecordingStock
|
||||
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
||||
if len(depletions) == 0 {
|
||||
return nil
|
||||
@@ -105,11 +131,100 @@ func (r *RecordingRepositoryImpl) DeleteDepletions(tx *gorm.DB, recordingID uint
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingDepletion{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error) {
|
||||
var result int64
|
||||
func (r *RecordingRepositoryImpl) ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error) {
|
||||
var items []entity.RecordingDepletion
|
||||
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error {
|
||||
if len(eggs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&eggs).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteEggs(tx *gorm.DB, recordingID uint) error {
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingEgg{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error) {
|
||||
var items []entity.RecordingEgg
|
||||
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetRecordingEggByID(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (*entity.RecordingEgg, error) {
|
||||
if id == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
db := r.DB()
|
||||
if modifier != nil {
|
||||
db = modifier(db)
|
||||
}
|
||||
|
||||
var egg entity.RecordingEgg
|
||||
query := db.WithContext(ctx).
|
||||
Preload("Recording").
|
||||
Preload("Recording.ProjectFlockKandang").
|
||||
Preload("Recording.ProjectFlockKandang.ProjectFlock").
|
||||
Preload("ProductWarehouse").
|
||||
Preload("GradingEggs").
|
||||
Where("id = ?", id)
|
||||
|
||||
if err := query.First(&egg).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &egg, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error {
|
||||
if len(gradings) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&gradings).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error {
|
||||
return tx.Where("recording_egg_id = ?", recordingEggID).Delete(&entity.GradingEgg{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) {
|
||||
if projectFlockKandangId == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ref := recordTime.In(time.UTC)
|
||||
startOfDay := time.Date(ref.Year(), ref.Month(), ref.Day(), 0, 0, 0, 0, time.UTC)
|
||||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||||
|
||||
var count int64
|
||||
err := r.DB().
|
||||
WithContext(ctx).
|
||||
Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||
Where("record_datetime >= ? AND record_datetime < ?", startOfDay, endOfDay).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var result float64
|
||||
if err := tx.Model(&entity.RecordingDepletion{}).
|
||||
Where("recording_id = ?", recordingID).
|
||||
Select("COALESCE(SUM(total), 0)").
|
||||
Select("COALESCE(SUM(qty), 0)").
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -123,7 +238,7 @@ func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFloc
|
||||
|
||||
var prev entity.Recording
|
||||
err := tx.
|
||||
Where("project_flock_id = ? AND day < ?", projectFlockKandangId, currentDay).
|
||||
Where("project_flock_kandangs_id = ? AND day < ?", projectFlockKandangId, currentDay).
|
||||
Where("day IS NOT NULL").
|
||||
Order("day DESC").
|
||||
Limit(1).
|
||||
@@ -159,7 +274,7 @@ func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID
|
||||
TotalQty float64
|
||||
}
|
||||
if err := tx.Model(&entity.RecordingBW{}).
|
||||
Select("COALESCE(SUM(weight * qty), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
|
||||
Select("COALESCE(SUM(total_weight), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
|
||||
Where("recording_id = ?", recordingID).
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
@@ -172,13 +287,13 @@ func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var rows []struct {
|
||||
UsageAmount float64
|
||||
UomName string
|
||||
UsageQty float64
|
||||
UomName string
|
||||
}
|
||||
|
||||
if err := tx.
|
||||
Table("recording_stocks").
|
||||
Select("COALESCE(recording_stocks.usage_amount, 0) AS usage_amount, LOWER(uoms.name) AS uom_name").
|
||||
Select("COALESCE(recording_stocks.usage_qty, 0) AS usage_qty, LOWER(uoms.name) AS uom_name").
|
||||
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
||||
@@ -189,16 +304,16 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u
|
||||
|
||||
var total float64
|
||||
for _, row := range rows {
|
||||
if row.UsageAmount <= 0 {
|
||||
if row.UsageQty <= 0 {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(row.UomName) {
|
||||
case "kilogram", "kg", "kilograms", "kilo":
|
||||
total += row.UsageAmount * 1000
|
||||
total += row.UsageQty * 1000
|
||||
case "gram", "g", "grams":
|
||||
total += row.UsageAmount
|
||||
total += row.UsageQty
|
||||
default:
|
||||
total += row.UsageAmount
|
||||
total += row.UsageQty
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
|
||||
@@ -18,7 +18,9 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Get("/next-day", ctrl.GetNextDay)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Post("/gradings", ctrl.SubmitGrading)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Post("/approvals", ctrl.Approve)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,23 +2,25 @@ package validation
|
||||
|
||||
type (
|
||||
BodyWeight struct {
|
||||
Weight float64 `json:"weight" validate:"required"`
|
||||
Qty int `json:"qty" validate:"required,number,min=1"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty"`
|
||||
AvgWeight float64 `json:"avg_weight" validate:"required"`
|
||||
Qty float64 `json:"qty" validate:"required,gt=0"`
|
||||
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gt=0"`
|
||||
}
|
||||
|
||||
Stock struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
Increase *float64 `json:"increase,omitempty" validate:"omitempty"`
|
||||
Decrease *float64 `json:"decrease,omitempty" validate:"omitempty"`
|
||||
UsageAmount *int64 `json:"usage_amount,omitempty" validate:"omitempty,min=0"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty"`
|
||||
Qty *float64 `json:"qty,omitempty" validate:"required_without=UsageAmount,gte=0"`
|
||||
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
|
||||
}
|
||||
|
||||
Depletion struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
Total int64 `json:"total" validate:"required,number,min=0"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty"`
|
||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||
}
|
||||
|
||||
Egg struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
Qty int `json:"qty" validate:"required,number,min=0"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -27,12 +29,14 @@ type Create struct {
|
||||
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
|
||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
|
||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
@@ -40,3 +44,19 @@ type Query struct {
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||
}
|
||||
|
||||
type EggGrading struct {
|
||||
RecordingEggId uint `json:"recording_egg_id" validate:"required,number,min=1"`
|
||||
Grade string `json:"grade" validate:"required"`
|
||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||
}
|
||||
|
||||
type SubmitGrading struct {
|
||||
EggsGrading []EggGrading `json:"eggs_grading" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type Approve struct {
|
||||
Action string `json:"action" validate:"required_strict"`
|
||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ type StockLogRepository interface {
|
||||
GetByFlaggable(ctx context.Context, logType string, logId uint) ([]*entity.StockLog, error)
|
||||
GetByProductWarehouse(ctx context.Context, productWarehouseId uint, limit int) ([]*entity.StockLog, error)
|
||||
GetByTransactionType(ctx context.Context, transactionType string, limit int) ([]*entity.StockLog, error)
|
||||
ApplyProductWarehouseFilters(db *gorm.DB, productID, warehouseID uint) *gorm.DB
|
||||
}
|
||||
|
||||
type StockLogRepositoryImpl struct {
|
||||
@@ -86,3 +87,20 @@ func (r *StockLogRepositoryImpl) GetByTransactionType(ctx context.Context, trans
|
||||
|
||||
return stockLogs, nil
|
||||
}
|
||||
|
||||
func (r *StockLogRepositoryImpl) ApplyProductWarehouseFilters(db *gorm.DB, productID, warehouseID uint) *gorm.DB {
|
||||
if productID == 0 && warehouseID == 0 {
|
||||
return db
|
||||
}
|
||||
|
||||
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id")
|
||||
|
||||
if productID > 0 {
|
||||
db = db.Where("product_warehouses.product_id = ?", productID)
|
||||
}
|
||||
if warehouseID > 0 {
|
||||
db = db.Where("product_warehouses.warehouse_id = ?", warehouseID)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -211,7 +211,6 @@ func (h *Controller) Callback(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadGateway, "missing access token")
|
||||
}
|
||||
|
||||
fmt.Println(tokenResp.AccessToken)
|
||||
verification, err := sso.VerifyAccessToken(tokenResp.AccessToken)
|
||||
if err != nil {
|
||||
utils.Log.Errorf("access token verification failed: %v", err)
|
||||
@@ -308,6 +307,13 @@ func (h *Controller) UserInfo(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadGateway, "invalid user profile response")
|
||||
}
|
||||
|
||||
// if sanitized, perms, ok := sanitizeUserInfoPayload(body); ok {
|
||||
// if caps := capabilities.FromPermissions(perms); len(caps) > 0 {
|
||||
// injectCapabilities(sanitized, caps)
|
||||
// }
|
||||
// return c.Status(resp.StatusCode).JSON(sanitized)
|
||||
// }
|
||||
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||
c.Set("Content-Type", ct)
|
||||
} else {
|
||||
@@ -545,6 +551,99 @@ func normalizeClientParam(raw string) string {
|
||||
return strings.ToLower(value)
|
||||
}
|
||||
|
||||
func sanitizeUserInfoPayload(body []byte) (map[string]any, []string, bool) {
|
||||
if len(body) == 0 {
|
||||
return map[string]any{}, nil, true
|
||||
}
|
||||
|
||||
var payload any
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
perms := collectPermissionNames(payload)
|
||||
|
||||
sensitive := map[string]struct{}{
|
||||
"roles": {},
|
||||
"permissions": {},
|
||||
}
|
||||
payload = scrubSensitiveKeys(payload, sensitive)
|
||||
|
||||
sanitized, ok := payload.(map[string]any)
|
||||
if !ok {
|
||||
sanitized = map[string]any{"data": payload}
|
||||
}
|
||||
|
||||
return sanitized, perms, true
|
||||
}
|
||||
|
||||
func scrubSensitiveKeys(value any, sensitive map[string]struct{}) any {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
for key, val := range v {
|
||||
if _, ok := sensitive[strings.ToLower(key)]; ok {
|
||||
delete(v, key)
|
||||
continue
|
||||
}
|
||||
v[key] = scrubSensitiveKeys(val, sensitive)
|
||||
}
|
||||
return v
|
||||
case []any:
|
||||
for i, item := range v {
|
||||
v[i] = scrubSensitiveKeys(item, sensitive)
|
||||
}
|
||||
return v
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func collectPermissionNames(value any) []string {
|
||||
names := make(map[string]struct{})
|
||||
collectPermissionRec(value, names)
|
||||
out := make([]string, 0, len(names))
|
||||
for name := range names {
|
||||
out = append(out, name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func collectPermissionRec(value any, acc map[string]struct{}) {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
for key, val := range v {
|
||||
if strings.EqualFold(key, "permissions") {
|
||||
if arr, ok := val.([]any); ok {
|
||||
for _, item := range arr {
|
||||
if perm, ok := item.(map[string]any); ok {
|
||||
if name, ok := perm["name"].(string); ok && strings.TrimSpace(name) != "" {
|
||||
acc[strings.ToLower(strings.TrimSpace(name))] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
collectPermissionRec(val, acc)
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, item := range v {
|
||||
collectPermissionRec(item, acc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func injectCapabilities(payload map[string]any, caps map[string]bool) {
|
||||
if len(caps) == 0 {
|
||||
return
|
||||
}
|
||||
if data, ok := payload["data"].(map[string]any); ok {
|
||||
data["capabilities"] = caps
|
||||
return
|
||||
}
|
||||
payload["capabilities"] = caps
|
||||
}
|
||||
|
||||
func findSSOClientConfig(requestedAlias string) (string, config.SSOClientConfig, bool) {
|
||||
if requestedAlias == "" {
|
||||
return "", config.SSOClientConfig{}, false
|
||||
|
||||
Reference in New Issue
Block a user