mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'dev/teguh' into 'development'
FEAT[BE]: transfer to laying, stock transfer, refactor adjustment, fix age closing penjualan, filter transfer to laying See merge request mbugroup/lti-api!256
This commit is contained in:
+3
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE adjustment_stocks ADD COLUMN stock_log_id INTEGER;
|
||||||
|
|
||||||
|
CREATE INDEX idx_adjustment_stocks_stock_log_id ON adjustment_stocks (stock_log_id);
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE adjustment_stocks DROP COLUMN IF EXISTS stock_log_id;
|
||||||
@@ -4,7 +4,6 @@ import "time"
|
|||||||
|
|
||||||
type AdjustmentStock struct {
|
type AdjustmentStock struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
StockLogId uint `gorm:"column:stock_log_id;not null;index"`
|
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
TotalQty float64 `gorm:"column:total_qty;default:0"`
|
TotalQty float64 `gorm:"column:total_qty;default:0"`
|
||||||
TotalUsed float64 `gorm:"column:total_used;default:0"`
|
TotalUsed float64 `gorm:"column:total_used;default:0"`
|
||||||
@@ -13,6 +12,6 @@ type AdjustmentStock struct {
|
|||||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
|
||||||
|
|
||||||
StockLog *StockLog `gorm:"foreignKey:StockLogId;references:Id"`
|
|
||||||
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === Response DTO ===
|
// === Response DTO ===
|
||||||
@@ -49,7 +50,12 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
|||||||
productFlags[i] = f.Name
|
productFlags[i] = f.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags)
|
var category string
|
||||||
|
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
||||||
|
category = e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category
|
||||||
|
}
|
||||||
|
|
||||||
|
ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category)
|
||||||
|
|
||||||
var product *productDTO.ProductRelationDTO
|
var product *productDTO.ProductRelationDTO
|
||||||
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||||
@@ -131,14 +137,27 @@ func ToPenjualanRealisasiResponseDTO(e []entity.MarketingDeliveryProduct) Penjua
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string) (int, int) {
|
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string, category string) (int, int) {
|
||||||
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, flag := range productFlags {
|
for _, flag := range productFlags {
|
||||||
if flag == "OVK" || flag == "PAKAN" {
|
if flag == string(utils.FlagOVK) ||
|
||||||
return 0, 0 //
|
flag == string(utils.FlagPakan) ||
|
||||||
|
flag == string(utils.FlagPreStarter) ||
|
||||||
|
flag == string(utils.FlagStarter) ||
|
||||||
|
flag == string(utils.FlagFinisher) ||
|
||||||
|
flag == string(utils.FlagObat) ||
|
||||||
|
flag == string(utils.FlagVitamin) ||
|
||||||
|
flag == string(utils.FlagKimia) ||
|
||||||
|
flag == string(utils.FlagEkspedisi) ||
|
||||||
|
flag == string(utils.FlagTelur) ||
|
||||||
|
flag == string(utils.FlagTelurUtuh) ||
|
||||||
|
flag == string(utils.FlagTelurPecah) ||
|
||||||
|
flag == string(utils.FlagTelurPutih) ||
|
||||||
|
flag == string(utils.FlagTelurRetak) {
|
||||||
|
return 0, 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +175,12 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, de
|
|||||||
if ageInDays <= 0 {
|
if ageInDays <= 0 {
|
||||||
ageInWeeks = 0
|
ageInWeeks = 0
|
||||||
} else {
|
} else {
|
||||||
|
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
ageInWeeks = ((ageInDays - 1) / 7) + 1
|
ageInDays = ageInDays + 119
|
||||||
|
ageInWeeks = ((ageInDays - 1) / 7) + 1
|
||||||
|
} else {
|
||||||
|
ageInWeeks = ((ageInDays - 1) / 7) + 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ageInDays, ageInWeeks
|
return ageInDays, ageInWeeks
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
|
|||||||
func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO {
|
func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO {
|
||||||
return AdjustmentRelationDTO{
|
return AdjustmentRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Note: e.StockLog.Notes,
|
Note: "",
|
||||||
Increase: e.TotalQty,
|
Increase: e.TotalQty,
|
||||||
Decrease: e.UsageQty,
|
Decrease: e.UsageQty,
|
||||||
ProductWarehouseId: e.ProductWarehouseId,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
@@ -113,24 +113,17 @@ func ToAdjustmentRelationDTO(e *entity.AdjustmentStock) AdjustmentRelationDTO {
|
|||||||
|
|
||||||
func ToAdjustmentListDTO(e *entity.AdjustmentStock) AdjustmentListDTO {
|
func ToAdjustmentListDTO(e *entity.AdjustmentStock) AdjustmentListDTO {
|
||||||
var createdUser *userDTO.UserRelationDTO
|
var createdUser *userDTO.UserRelationDTO
|
||||||
if e.StockLog != nil && e.StockLog.CreatedUser != nil {
|
|
||||||
createdUser = &userDTO.UserRelationDTO{
|
|
||||||
Id: e.StockLog.CreatedUser.Id,
|
|
||||||
IdUser: e.StockLog.CreatedUser.IdUser,
|
|
||||||
Email: e.StockLog.CreatedUser.Email,
|
|
||||||
Name: e.StockLog.CreatedUser.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createdAt := time.Time{}
|
// Get created user from StockLog
|
||||||
if e.StockLog != nil {
|
if e.StockLog != nil && e.StockLog.CreatedUser != nil {
|
||||||
createdAt = e.StockLog.CreatedAt
|
mapped := userDTO.ToUserRelationDTO(*e.StockLog.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
return AdjustmentListDTO{
|
return AdjustmentListDTO{
|
||||||
AdjustmentRelationDTO: ToAdjustmentRelationDTO(e),
|
AdjustmentRelationDTO: ToAdjustmentRelationDTO(e),
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
CreatedAt: createdAt,
|
CreatedAt: e.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
type AdjustmentStockRepository interface {
|
type AdjustmentStockRepository interface {
|
||||||
CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error
|
CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error)
|
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error)
|
||||||
WithTx(tx *gorm.DB) AdjustmentStockRepository
|
WithTx(tx *gorm.DB) AdjustmentStockRepository
|
||||||
DB() *gorm.DB
|
DB() *gorm.DB
|
||||||
}
|
}
|
||||||
@@ -30,19 +30,13 @@ func (r *adjustmentStockRepositoryImpl) CreateOne(ctx context.Context, data *ent
|
|||||||
return q.Create(data).Error
|
return q.Create(data).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *adjustmentStockRepositoryImpl) GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error) {
|
func (r *adjustmentStockRepositoryImpl) GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) {
|
||||||
var record entity.AdjustmentStock
|
var record entity.AdjustmentStock
|
||||||
err := r.db.WithContext(ctx).
|
q := r.db.WithContext(ctx)
|
||||||
Preload("StockLog").
|
if modifier != nil {
|
||||||
Preload("StockLog.ProductWarehouse").
|
q = modifier(q)
|
||||||
Preload("StockLog.ProductWarehouse.Product").
|
}
|
||||||
Preload("StockLog.ProductWarehouse.Warehouse").
|
err := q.First(&record, id).Error
|
||||||
Preload("StockLog.CreatedUser").
|
|
||||||
Preload("ProductWarehouse").
|
|
||||||
Preload("ProductWarehouse.Product").
|
|
||||||
Preload("ProductWarehouse.Warehouse").
|
|
||||||
Where("stock_log_id = ?", stockLogID).
|
|
||||||
First(&record).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("ProductWarehouse").
|
Preload("ProductWarehouse").
|
||||||
Preload("ProductWarehouse.Product").
|
Preload("ProductWarehouse.Product").
|
||||||
Preload("ProductWarehouse.Warehouse").
|
Preload("ProductWarehouse.Warehouse").
|
||||||
Preload("CreatedUser")
|
Preload("StockLog.CreatedUser")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) {
|
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) {
|
||||||
adjustmentStock, err := s.AdjustmentStockRepository.GetByStockLogID(c.Context(), id)
|
adjustmentStock, err := s.AdjustmentStockRepository.GetByID(c.Context(), id, s.withRelations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
||||||
@@ -164,13 +164,13 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
|
|
||||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||||
afterQuantity += req.Quantity
|
afterQuantity += req.Quantity
|
||||||
newLog.Increase = afterQuantity
|
newLog.Increase = req.Quantity
|
||||||
} else {
|
} else {
|
||||||
if productWarehouse.Quantity < req.Quantity {
|
if productWarehouse.Quantity < req.Quantity {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity))
|
||||||
}
|
}
|
||||||
afterQuantity -= req.Quantity
|
afterQuantity -= req.Quantity
|
||||||
newLog.Decrease = afterQuantity
|
newLog.Decrease = req.Quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
||||||
@@ -179,7 +179,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
adjustmentStock := &entity.AdjustmentStock{
|
adjustmentStock := &entity.AdjustmentStock{
|
||||||
StockLogId: newLog.Id,
|
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
}
|
}
|
||||||
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
||||||
@@ -187,6 +186,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newLog.LoggableType = string(utils.StockLogTypeAdjustment)
|
||||||
|
newLog.LoggableId = adjustmentStock.Id
|
||||||
|
if err := s.StockLogsRepository.WithTx(tx).UpdateOne(ctx, newLog.Id, newLog, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to link stock log")
|
||||||
|
}
|
||||||
|
|
||||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||||
|
|
||||||
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
||||||
@@ -216,7 +221,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LEGACY: Update ProductWarehouse quantity (for backward compatibility/reporting)
|
|
||||||
productWarehouse.Quantity = afterQuantity
|
productWarehouse.Quantity = afterQuantity
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
||||||
@@ -295,29 +299,23 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
|||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
q := s.AdjustmentStockRepository.DB().WithContext(c.Context()).Model(&entity.AdjustmentStock{}).
|
q := s.AdjustmentStockRepository.DB().WithContext(c.Context()).Model(&entity.AdjustmentStock{}).
|
||||||
Preload("StockLog").
|
|
||||||
Preload("StockLog.ProductWarehouse").
|
|
||||||
Preload("StockLog.ProductWarehouse.Product").
|
|
||||||
Preload("StockLog.ProductWarehouse.Warehouse").
|
|
||||||
Preload("StockLog.CreatedUser").
|
|
||||||
Preload("ProductWarehouse").
|
Preload("ProductWarehouse").
|
||||||
Preload("ProductWarehouse.Product").
|
Preload("ProductWarehouse.Product").
|
||||||
Preload("ProductWarehouse.Warehouse")
|
Preload("ProductWarehouse.Warehouse").
|
||||||
|
Preload("StockLog.CreatedUser")
|
||||||
|
|
||||||
if query.ProductID > 0 {
|
if query.ProductID > 0 {
|
||||||
q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id").
|
q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id").
|
||||||
Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
|
|
||||||
Where("product_warehouses.product_id = ?", query.ProductID)
|
Where("product_warehouses.product_id = ?", query.ProductID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.WarehouseID > 0 {
|
if query.WarehouseID > 0 {
|
||||||
q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id").
|
q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id").
|
||||||
Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
|
|
||||||
Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
|
Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.TransactionType != "" {
|
if query.TransactionType != "" {
|
||||||
q = q.Joins("JOIN stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id").
|
q = q.Joins("JOIN stock_logs ON stock_logs.loggable_type = ? AND stock_logs.loggable_id = adjustment_stocks.id", "ADJUSTMENT").
|
||||||
Where("stock_logs.transaction_type = ?", strings.ToUpper(query.TransactionType))
|
Where("stock_logs.transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx)
|
stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx)
|
||||||
stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx)
|
stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx)
|
||||||
productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx)
|
productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx)
|
||||||
|
stocklogsRepoTx := s.StockLogsRepository.WithTx(tx)
|
||||||
|
|
||||||
if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -405,6 +406,19 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stockLogDecrease := &entity.StockLog{
|
||||||
|
ProductWarehouseId: uint(*detail.SourceProductWarehouseID),
|
||||||
|
CreatedBy: uint(actorID),
|
||||||
|
Increase: 0,
|
||||||
|
Decrease: product.ProductQty,
|
||||||
|
LoggableType: string(utils.StockLogTypeTransfer),
|
||||||
|
LoggableId: uint(detail.Id),
|
||||||
|
Notes: "",
|
||||||
|
}
|
||||||
|
if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
|
||||||
|
}
|
||||||
|
|
||||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||||
StockableKey: fifo.StockableKeyStockTransferIn,
|
StockableKey: fifo.StockableKeyStockTransferIn,
|
||||||
@@ -427,6 +441,19 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
s.Log.Errorf("Failed to update tracking total for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
s.Log.Errorf("Failed to update tracking total for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stockLogIncrease := &entity.StockLog{
|
||||||
|
ProductWarehouseId: uint(*detail.DestProductWarehouseID),
|
||||||
|
CreatedBy: uint(actorID),
|
||||||
|
Increase: product.ProductQty,
|
||||||
|
Decrease: 0,
|
||||||
|
LoggableType: string(utils.StockLogTypeTransfer),
|
||||||
|
LoggableId: uint(detail.Id),
|
||||||
|
Notes: "",
|
||||||
|
}
|
||||||
|
if err := stocklogsRepoTx.CreateOne(c.Context(), stockLogIncrease, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Deliveries) > 0 {
|
if len(req.Deliveries) > 0 {
|
||||||
|
|||||||
+1
@@ -85,6 +85,7 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Cont
|
|||||||
var records []entity.ProjectFlockKandang
|
var records []entity.ProjectFlockKandang
|
||||||
if err := r.db.WithContext(ctx).
|
if err := r.db.WithContext(ctx).
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Preload("Kandang").
|
||||||
Find(&records).Error; err != nil {
|
Find(&records).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-3
@@ -9,6 +9,7 @@ import (
|
|||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
@@ -28,9 +29,11 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
|
|||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
TransferDate: c.Query("transfer_date", ""),
|
StartDate: c.Query("start_date", ""),
|
||||||
FlockSource: uint(c.QueryInt("flock_source", 0)),
|
EndDate: c.Query("end_date", ""),
|
||||||
FlockDestination: uint(c.QueryInt("flock_destination", 0)),
|
FlockSource: utils.ParseQueryUintArray(c.Query("flock_source", "")),
|
||||||
|
FlockDestination: utils.ParseQueryUintArray(c.Query("flock_destination", "")),
|
||||||
|
Status: utils.ParseQueryArray(c.Query("status", "")),
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
@@ -218,3 +221,29 @@ func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error
|
|||||||
Data: resp,
|
Data: resp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *TransferLayingController) GetMaxTargetQtyPerKandang(c *fiber.Ctx) error {
|
||||||
|
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
|
}
|
||||||
|
kandangMaxTargetQty, err := u.TransferLayingService.GetMaxTargetQtyPerKandang(c, uint(projectFlockID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangs := make([]dto.KandangMaxTargetQtyDTO, 0, len(kandangMaxTargetQty))
|
||||||
|
for pfkId, maxTargetQty := range kandangMaxTargetQty {
|
||||||
|
kandangs = append(kandangs, dto.ToKandangMaxTargetQtyDTO(pfkId, maxTargetQty))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := dto.ToMaxTargetQtyForTransferDTO(uint(projectFlockID), kandangs)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get max target quantity successfully",
|
||||||
|
Data: resp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import (
|
|||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
|
productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
|
||||||
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
|
projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,60 +20,35 @@ type TransferLayingRelationDTO struct {
|
|||||||
Notes string `json:"notes"`
|
Notes string `json:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectFlockSummaryDTO struct {
|
type ProjectFlockKandangWithKandangDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
FlockName string `json:"flock_name"`
|
KandangId uint `json:"kandang_id"`
|
||||||
Category string `json:"category"`
|
ProjectFlockId uint `json:"project_flock_id"`
|
||||||
}
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
|
|
||||||
type ProductSummaryDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WarehouseSummaryDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProductWarehouseSummaryDTO struct {
|
|
||||||
Product *ProductSummaryDTO `json:"product,omitempty"`
|
|
||||||
Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProjectFlockKandangSummaryDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Kandang *KandangSummaryDTO `json:"kandang,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type KandangSummaryDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayingTransferSourceDTO struct {
|
type LayingTransferSourceDTO struct {
|
||||||
SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"`
|
SourceProjectFlockKandang *ProjectFlockKandangWithKandangDTO `json:"source_project_flock_kandang,omitempty"`
|
||||||
Qty float64 `json:"qty"`
|
Qty float64 `json:"qty"`
|
||||||
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
|
ProductWarehouse *productWarehouseDTO.ProductWarehouseRelationDTO `json:"product_warehouse,omitempty"`
|
||||||
Note string `json:"note,omitempty"`
|
Note string `json:"note,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayingTransferTargetDTO struct {
|
type LayingTransferTargetDTO struct {
|
||||||
TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"`
|
TargetProjectFlockKandang *ProjectFlockKandangWithKandangDTO `json:"target_project_flock_kandang,omitempty"`
|
||||||
Qty float64 `json:"qty"`
|
Qty float64 `json:"qty"`
|
||||||
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
|
ProductWarehouse *productWarehouseDTO.ProductWarehouseRelationDTO `json:"product_warehouse,omitempty"`
|
||||||
Note string `json:"note,omitempty"`
|
Note string `json:"note,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferLayingListDTO struct {
|
type TransferLayingListDTO struct {
|
||||||
TransferLayingRelationDTO
|
TransferLayingRelationDTO
|
||||||
FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"`
|
FromProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"from_project_flock,omitempty"`
|
||||||
ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"`
|
ToProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"to_project_flock,omitempty"`
|
||||||
CreatedBy uint `json:"created_by"`
|
CreatedBy uint `json:"created_by"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferLayingDetailDTO struct {
|
type TransferLayingDetailDTO struct {
|
||||||
@@ -94,70 +72,26 @@ type AvailableQtyForTransferDTO struct {
|
|||||||
Kandangs []KandangAvailableQtyDTO `json:"kandangs"`
|
Kandangs []KandangAvailableQtyDTO `json:"kandangs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Max Target Quantity DTOs ===
|
||||||
|
|
||||||
|
type KandangMaxTargetQtyDTO struct {
|
||||||
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
|
MaxTargetQty float64 `json:"max_target_qty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaxTargetQtyForTransferDTO struct {
|
||||||
|
ProjectFlockId uint `json:"project_flock_id"`
|
||||||
|
ProjectFlockKandangs []KandangMaxTargetQtyDTO `json:"project_flock_kandangs"`
|
||||||
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO {
|
func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO {
|
||||||
if pf == nil || pf.Id == 0 {
|
return TransferLayingRelationDTO{
|
||||||
return nil
|
Id: e.Id,
|
||||||
}
|
TransferNumber: e.TransferNumber,
|
||||||
|
TransferDate: e.TransferDate,
|
||||||
return &ProjectFlockSummaryDTO{
|
Notes: e.Notes,
|
||||||
Id: pf.Id,
|
|
||||||
FlockName: pf.FlockName,
|
|
||||||
Category: pf.Category,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO {
|
|
||||||
if pfk == nil || pfk.Id == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var kandang *KandangSummaryDTO
|
|
||||||
if pfk.Kandang.Id != 0 {
|
|
||||||
kandang = &KandangSummaryDTO{
|
|
||||||
Id: pfk.Kandang.Id,
|
|
||||||
Name: pfk.Kandang.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ProjectFlockKandangSummaryDTO{
|
|
||||||
Id: pfk.Id,
|
|
||||||
Kandang: kandang,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO {
|
|
||||||
if product == nil || product.Id == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ProductSummaryDTO{
|
|
||||||
Id: product.Id,
|
|
||||||
Name: product.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO {
|
|
||||||
if warehouse == nil || warehouse.Id == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &WarehouseSummaryDTO{
|
|
||||||
Id: warehouse.Id,
|
|
||||||
Name: warehouse.Name,
|
|
||||||
Type: warehouse.Type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO {
|
|
||||||
if pw == nil || pw.Id == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ProductWarehouseSummaryDTO{
|
|
||||||
Product: ToProductSummaryDTO(&pw.Product),
|
|
||||||
Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,10 +106,29 @@ func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransfe
|
|||||||
displayQty = source.RequestedQty
|
displayQty = source.RequestedQty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pfkDTO *ProjectFlockKandangWithKandangDTO
|
||||||
|
if source.SourceProjectFlockKandang != nil && source.SourceProjectFlockKandang.Id != 0 {
|
||||||
|
pfkDTO = &ProjectFlockKandangWithKandangDTO{
|
||||||
|
Id: source.SourceProjectFlockKandang.Id,
|
||||||
|
KandangId: source.SourceProjectFlockKandang.KandangId,
|
||||||
|
ProjectFlockId: source.SourceProjectFlockKandang.ProjectFlockId,
|
||||||
|
}
|
||||||
|
if source.SourceProjectFlockKandang.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(source.SourceProjectFlockKandang.Kandang)
|
||||||
|
pfkDTO.Kandang = &mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pwDTO *productWarehouseDTO.ProductWarehouseRelationDTO
|
||||||
|
if source.ProductWarehouse != nil && source.ProductWarehouse.Id != 0 {
|
||||||
|
mapped := productWarehouseDTO.ToProductWarehouseRelationDTO(*source.ProductWarehouse)
|
||||||
|
pwDTO = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return LayingTransferSourceDTO{
|
return LayingTransferSourceDTO{
|
||||||
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
|
SourceProjectFlockKandang: pfkDTO,
|
||||||
Qty: displayQty,
|
Qty: displayQty,
|
||||||
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
|
ProductWarehouse: pwDTO,
|
||||||
Note: source.Note,
|
Note: source.Note,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,10 +145,29 @@ func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingT
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
|
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
|
||||||
|
var pfkDTO *ProjectFlockKandangWithKandangDTO
|
||||||
|
if target.TargetProjectFlockKandang != nil && target.TargetProjectFlockKandang.Id != 0 {
|
||||||
|
pfkDTO = &ProjectFlockKandangWithKandangDTO{
|
||||||
|
Id: target.TargetProjectFlockKandang.Id,
|
||||||
|
KandangId: target.TargetProjectFlockKandang.KandangId,
|
||||||
|
ProjectFlockId: target.TargetProjectFlockKandang.ProjectFlockId,
|
||||||
|
}
|
||||||
|
if target.TargetProjectFlockKandang.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(target.TargetProjectFlockKandang.Kandang)
|
||||||
|
pfkDTO.Kandang = &mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pwDTO *productWarehouseDTO.ProductWarehouseRelationDTO
|
||||||
|
if target.ProductWarehouse != nil && target.ProductWarehouse.Id != 0 {
|
||||||
|
mapped := productWarehouseDTO.ToProductWarehouseRelationDTO(*target.ProductWarehouse)
|
||||||
|
pwDTO = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return LayingTransferTargetDTO{
|
return LayingTransferTargetDTO{
|
||||||
TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang),
|
TargetProjectFlockKandang: pfkDTO,
|
||||||
Qty: target.TotalQty, // Ambil dari TotalQty (FIFO replenished quantity)
|
Qty: target.TotalQty,
|
||||||
ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse),
|
ProductWarehouse: pwDTO,
|
||||||
Note: target.Note,
|
Note: target.Note,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,15 +183,6 @@ func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingT
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO {
|
|
||||||
return TransferLayingRelationDTO{
|
|
||||||
Id: e.Id,
|
|
||||||
TransferNumber: e.TransferNumber,
|
|
||||||
TransferDate: e.TransferDate,
|
|
||||||
Notes: e.Notes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
|
func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
|
||||||
var createdUser *userDTO.UserRelationDTO
|
var createdUser *userDTO.UserRelationDTO
|
||||||
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
@@ -227,26 +190,52 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
|
|||||||
createdUser = &mapped
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var approval *approvalDTO.ApprovalRelationDTO
|
||||||
|
if e.LatestApproval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||||
|
approval = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build from project flock DTO
|
||||||
|
var fromProjectFlock *projectFlockDTO.ProjectFlockRelationDTO
|
||||||
|
if e.FromProjectFlock != nil && e.FromProjectFlock.Id != 0 {
|
||||||
|
fromProjectFlock = &projectFlockDTO.ProjectFlockRelationDTO{
|
||||||
|
Id: e.FromProjectFlock.Id,
|
||||||
|
FlockName: e.FromProjectFlock.FlockName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toProjectFlock *projectFlockDTO.ProjectFlockRelationDTO
|
||||||
|
if e.ToProjectFlock != nil && e.ToProjectFlock.Id != 0 {
|
||||||
|
toProjectFlock = &projectFlockDTO.ProjectFlockRelationDTO{
|
||||||
|
Id: e.ToProjectFlock.Id,
|
||||||
|
FlockName: e.ToProjectFlock.FlockName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return TransferLayingListDTO{
|
return TransferLayingListDTO{
|
||||||
TransferLayingRelationDTO: ToTransferLayingRelationDTO(e),
|
TransferLayingRelationDTO: ToTransferLayingRelationDTO(e),
|
||||||
FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock),
|
FromProjectFlock: fromProjectFlock,
|
||||||
ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock),
|
ToProjectFlock: toProjectFlock,
|
||||||
CreatedBy: e.CreatedBy,
|
CreatedBy: e.CreatedBy,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
|
Approval: approval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
|
func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
|
||||||
var latestApproval *approvalDTO.ApprovalRelationDTO
|
var latestApproval *approvalDTO.ApprovalRelationDTO
|
||||||
|
|
||||||
if e.LatestApproval != nil {
|
// Prioritas: e.LatestApproval > approvals slice
|
||||||
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
approvalToMap := e.LatestApproval
|
||||||
|
if approvalToMap == nil && len(approvals) > 0 {
|
||||||
|
approvalToMap = &approvals[len(approvals)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if approvalToMap != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*approvalToMap)
|
||||||
latestApproval = &mapped
|
latestApproval = &mapped
|
||||||
} else if len(approvals) > 0 {
|
|
||||||
// Fallback to approvals slice
|
|
||||||
latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1])
|
|
||||||
latestApproval = &latest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return TransferLayingDetailDTO{
|
return TransferLayingDetailDTO{
|
||||||
@@ -260,13 +249,14 @@ func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Appro
|
|||||||
func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO {
|
func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO {
|
||||||
var mappedApproval *approvalDTO.ApprovalRelationDTO
|
var mappedApproval *approvalDTO.ApprovalRelationDTO
|
||||||
|
|
||||||
// Prefer LatestApproval from entity
|
// Prioritas: e.LatestApproval > approval parameter
|
||||||
if e.LatestApproval != nil && e.LatestApproval.Id != 0 {
|
approvalToMap := e.LatestApproval
|
||||||
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
if approvalToMap == nil && approval != nil {
|
||||||
mappedApproval = &mapped
|
approvalToMap = approval
|
||||||
} else if approval != nil && approval.Id != 0 {
|
}
|
||||||
// Fallback to passed approval parameter
|
|
||||||
mapped := approvalDTO.ToApprovalDTO(*approval)
|
if approvalToMap != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*approvalToMap)
|
||||||
mappedApproval = &mapped
|
mappedApproval = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,3 +275,17 @@ func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingLis
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToKandangMaxTargetQtyDTO(pfkId uint, maxTargetQTY float64) KandangMaxTargetQtyDTO {
|
||||||
|
return KandangMaxTargetQtyDTO{
|
||||||
|
ProjectFlockKandangId: uint(pfkId),
|
||||||
|
MaxTargetQty: maxTargetQTY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToMaxTargetQtyForTransferDTO(pfId uint, kandangs []KandangMaxTargetQtyDTO) MaxTargetQtyForTransferDTO {
|
||||||
|
return MaxTargetQtyForTransferDTO{
|
||||||
|
ProjectFlockId: pfId,
|
||||||
|
ProjectFlockKandangs: kandangs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+94
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -12,6 +13,9 @@ type TransferLayingRepository interface {
|
|||||||
repository.BaseRepository[entity.LayingTransfer]
|
repository.BaseRepository[entity.LayingTransfer]
|
||||||
GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error)
|
GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error)
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
|
|
||||||
|
// Tambah method baru untuk query dengan filter lengkap
|
||||||
|
GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferLayingRepositoryImpl struct {
|
type TransferLayingRepositoryImpl struct {
|
||||||
@@ -40,3 +44,93 @@ func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context,
|
|||||||
}
|
}
|
||||||
return &transfer, nil
|
return &transfer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetAllFilterParams struct {
|
||||||
|
Search string
|
||||||
|
StartDate string
|
||||||
|
EndDate string
|
||||||
|
FlockSource []uint
|
||||||
|
FlockDestination []uint
|
||||||
|
Status []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) {
|
||||||
|
var records []entity.LayingTransfer
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
q := r.db.WithContext(ctx).Model(&entity.LayingTransfer{})
|
||||||
|
|
||||||
|
if params.Search != "" {
|
||||||
|
searchPattern := "%" + params.Search + "%"
|
||||||
|
q = q.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id").
|
||||||
|
Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id").
|
||||||
|
Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?",
|
||||||
|
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.StartDate != "" && params.EndDate != "" {
|
||||||
|
q = q.Where("transfer_date::date >= ?::date AND transfer_date::date <= ?::date",
|
||||||
|
params.StartDate, params.EndDate)
|
||||||
|
} else if params.StartDate != "" {
|
||||||
|
q = q.Where("transfer_date::date >= ?::date", params.StartDate)
|
||||||
|
} else if params.EndDate != "" {
|
||||||
|
q = q.Where("transfer_date::date <= ?::date", params.EndDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params.FlockSource) > 0 {
|
||||||
|
q = q.Where("from_project_flock_id IN ?", params.FlockSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params.FlockDestination) > 0 {
|
||||||
|
q = q.Where("to_project_flock_id IN ?", params.FlockDestination)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params.Status) > 0 {
|
||||||
|
statusConditions := []string{}
|
||||||
|
statusValues := []interface{}{}
|
||||||
|
|
||||||
|
for _, status := range params.Status {
|
||||||
|
switch status {
|
||||||
|
case "PENDING":
|
||||||
|
statusConditions = append(statusConditions,
|
||||||
|
"NOT EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id)")
|
||||||
|
|
||||||
|
case "APPROVED":
|
||||||
|
statusConditions = append(statusConditions,
|
||||||
|
"EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id AND action = 'APPROVED' ORDER BY created_at DESC LIMIT 1)")
|
||||||
|
|
||||||
|
case "REJECTED":
|
||||||
|
statusConditions = append(statusConditions,
|
||||||
|
"EXISTS (SELECT 1 FROM approvals WHERE approvable_type = 'TRANSFER_TO_LAYINGS' AND approvable_id = laying_transfers.id AND action = 'REJECTED' ORDER BY created_at DESC LIMIT 1)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(statusConditions) > 0 {
|
||||||
|
q = q.Where("("+strings.Join(statusConditions, " OR ")+")", statusValues...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q = q.Offset(offset).Limit(limit).
|
||||||
|
Preload("FromProjectFlock").
|
||||||
|
Preload("ToProjectFlock").
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("Sources").
|
||||||
|
Preload("Sources.SourceProjectFlockKandang").
|
||||||
|
Preload("Sources.SourceProjectFlockKandang.Kandang").
|
||||||
|
Preload("Sources.ProductWarehouse").
|
||||||
|
Preload("Targets").
|
||||||
|
Preload("Targets.TargetProjectFlockKandang").
|
||||||
|
Preload("Targets.TargetProjectFlockKandang.Kandang").
|
||||||
|
Preload("Targets.ProductWarehouse").
|
||||||
|
Order("laying_transfers.created_at DESC")
|
||||||
|
|
||||||
|
if err := q.Find(&records).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, total, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
|
|||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
// route.Post("/approval", m.Auth(u), ctrl.Approval)
|
// route.Post("/approval", m.Auth(u), ctrl.Approval)
|
||||||
|
|
||||||
route.Get("/",m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll)
|
route.Get("/", m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll)
|
||||||
route.Post("/",m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne)
|
route.Post("/", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne)
|
||||||
route.Get("/:id",m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne)
|
route.Get("/:id", m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne)
|
||||||
route.Patch("/:id",m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne)
|
route.Patch("/:id", m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne)
|
||||||
route.Delete("/:id",m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne)
|
route.Delete("/:id", m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne)
|
||||||
route.Post("/approvals",m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval)
|
route.Post("/approvals", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval)
|
||||||
route.Get("/project-flocks/:project_flock_id/available-qty",m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang)
|
route.Get("/project-flocks/:project_flock_id/available-qty", m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang)
|
||||||
|
route.Get("/project-flocks/:project_flock_id/max-target-qty", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.GetMaxTargetQtyPerKandang)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type TransferLayingService interface {
|
|||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
|
||||||
GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error)
|
GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error)
|
||||||
|
GetMaxTargetQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (map[uint]float64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type transferLayingService struct {
|
type transferLayingService struct {
|
||||||
@@ -108,34 +109,20 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
|||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
filterParams := &repository.GetAllFilterParams{
|
||||||
// Apply search and filters
|
Search: params.Search,
|
||||||
if params.Search != "" {
|
StartDate: params.StartDate,
|
||||||
searchPattern := "%" + params.Search + "%"
|
EndDate: params.EndDate,
|
||||||
db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id").
|
FlockSource: params.FlockSource,
|
||||||
Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id").
|
FlockDestination: params.FlockDestination,
|
||||||
Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?",
|
Status: params.Status,
|
||||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if params.TransferDate != "" {
|
transferLayings, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, filterParams)
|
||||||
db = db.Where("transfer_date::date = ?::date", params.TransferDate)
|
if err != nil {
|
||||||
}
|
s.Log.Errorf("Failed to get transferLayings: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
if params.FlockSource > 0 {
|
}
|
||||||
db = db.Where("from_project_flock_id = ?", params.FlockSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.FlockDestination > 0 {
|
|
||||||
db = db.Where("to_project_flock_id = ?", params.FlockDestination)
|
|
||||||
}
|
|
||||||
|
|
||||||
db = db.Order("created_at DESC")
|
|
||||||
|
|
||||||
db = s.withRelations(db)
|
|
||||||
|
|
||||||
return db
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get transferLayings: %+v", err)
|
s.Log.Errorf("Failed to get transferLayings: %+v", err)
|
||||||
@@ -888,3 +875,39 @@ func (s *transferLayingService) validateKandangOwnership(
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) {
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangMaxTargetQty := make(map[uint]float64)
|
||||||
|
for _, projectFlockKandang := range projectFlockKandangs {
|
||||||
|
|
||||||
|
capacity := projectFlockKandang.Kandang.Capacity
|
||||||
|
|
||||||
|
availableQty, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(
|
||||||
|
c.Context(),
|
||||||
|
projectFlockKandang.Id,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangMaxTargetQty[projectFlockKandang.Id] = capacity - availableQty
|
||||||
|
|
||||||
|
if kandangMaxTargetQty[projectFlockKandang.Id] < 0 {
|
||||||
|
kandangMaxTargetQty[projectFlockKandang.Id] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return kandangMaxTargetQty, nil
|
||||||
|
}
|
||||||
|
|||||||
+8
-6
@@ -29,12 +29,14 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty"`
|
Search string `query:"search" validate:"omitempty"`
|
||||||
TransferDate string `query:"transfer_date" validate:"omitempty"`
|
StartDate string `query:"start_date" validate:"omitempty"`
|
||||||
FlockSource uint `query:"flock_source" validate:"omitempty,number"`
|
EndDate string `query:"end_date" validate:"omitempty"`
|
||||||
FlockDestination uint `query:"flock_destination" validate:"omitempty,number"`
|
FlockSource []uint `query:"flock_source" validate:"omitempty"`
|
||||||
|
FlockDestination []uint `query:"flock_destination" validate:"omitempty"`
|
||||||
|
Status []string `query:"status" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Approve struct {
|
type Approve struct {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,3 +48,54 @@ func ParseFlags(raw string) []string {
|
|||||||
sort.Strings(res)
|
sort.Strings(res)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseQueryArray parses comma-separated string values and returns a slice of trimmed strings
|
||||||
|
// Example: "a, b, c" → ["a", "b", "c"]
|
||||||
|
func ParseQueryArray(raw string) []string {
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
result := make([]string, 0, len(parts))
|
||||||
|
|
||||||
|
for _, p := range parts {
|
||||||
|
trimmed := strings.TrimSpace(p)
|
||||||
|
if trimmed != "" {
|
||||||
|
result = append(result, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseQueryUintArray parses comma-separated string values and returns a slice of uint
|
||||||
|
// Invalid values are skipped
|
||||||
|
// Example: "1, 2, 3" → [1, 2, 3]
|
||||||
|
func ParseQueryUintArray(raw string) []uint {
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
result := make([]uint, 0, len(parts))
|
||||||
|
|
||||||
|
for _, p := range parts {
|
||||||
|
trimmed := strings.TrimSpace(p)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if num, err := strconv.ParseUint(trimmed, 10, 32); err == nil {
|
||||||
|
result = append(result, uint(num))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user