Compare commits

...

7 Commits

Author SHA1 Message Date
giovanni f64839dfe1 add delete snapshoot if change chickin date 2026-06-04 23:46:25 +07:00
giovanni 0ff720453f add api for edit chickin date 2026-06-03 11:56:32 +07:00
Giovanni Gabriel Septriadi ef2f9568ad Merge branch 'rc/01' into 'production'
Rc/01

See merge request mbugroup/lti-api!583
2026-06-01 15:01:07 +00:00
Giovanni Gabriel Septriadi badbe4086a Merge branch 'fix/reconcile-fifo' into 'rc/01'
add command to fix reconcile fifo; fix fifo stock v2

See merge request mbugroup/lti-api!582
2026-06-01 14:46:45 +00:00
Giovanni Gabriel Septriadi 6528739bfd Merge branch 'feat/export-marketing' into 'rc/01'
Feat/export marketing and recording

See merge request mbugroup/lti-api!581
2026-06-01 14:45:41 +00:00
giovanni 68bddd5c78 adjust response list marketing add grand total so dan do 2026-05-31 16:38:22 +07:00
giovanni bfef144668 add filter warehouse to marketing;add detail export recording egg; adjust format export marketing 2026-05-31 16:23:22 +07:00
15 changed files with 557 additions and 100 deletions
@@ -72,6 +72,7 @@ func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
MarketingId: uint(c.QueryInt("marketing_id", 0)), MarketingId: uint(c.QueryInt("marketing_id", 0)),
ProjectFlockID: uint(c.QueryInt("project_flock_id", 0)), ProjectFlockID: uint(c.QueryInt("project_flock_id", 0)),
ProjectFlockKandangID: uint(c.QueryInt("project_flock_kandang_id", 0)), ProjectFlockKandangID: uint(c.QueryInt("project_flock_kandang_id", 0)),
WarehouseID: uint(c.QueryInt("warehouse_id", 0)),
SortBy: sortBy, SortBy: sortBy,
SortOrder: sortOrder, SortOrder: sortOrder,
} }
@@ -70,23 +70,26 @@ func buildMarketingExportWorkbook(items []dto.MarketingListDTO) ([]byte, error)
} }
func setMarketingExportColumns(file *excelize.File, sheet string) error { func setMarketingExportColumns(file *excelize.File, sheet string) error {
// AQ = 17 columns
// E = Sales (new), H = Gudang (new), Satuan (old I) removed
columnWidths := map[string]float64{ columnWidths := map[string]float64{
"A": 16, "A": 16, // No. Order
"B": 14, "B": 14, // Tanggal
"C": 18, "C": 18, // Status
"D": 20, "D": 20, // Customer
"E": 14, "E": 20, // Sales (new)
"F": 40, "F": 14, // Tipe
"G": 10, "G": 40, // Nama Produk
"H": 12, "H": 20, // Gudang (new)
"I": 12, "I": 10, // Week
"J": 12, "J": 12, // Jumlah
"K": 16, "K": 12, // Qty Peti
"L": 16, "L": 16, // Berat Rata-rata (kg)
"M": 18, "M": 16, // Total Berat (kg)
"N": 18, "N": 18, // Harga Satuan
"O": 18, "O": 18, // Total Harga
"P": 24, "P": 18, // Grand Total
"Q": 24, // Catatan
} }
for col, width := range columnWidths { for col, width := range columnWidths {
@@ -104,22 +107,23 @@ func setMarketingExportColumns(file *excelize.File, sheet string) error {
func setMarketingExportHeaders(file *excelize.File, sheet string) error { func setMarketingExportHeaders(file *excelize.File, sheet string) error {
headers := []string{ headers := []string{
"No. Order", // A "No. Order", // A
"Tanggal", // B "Tanggal", // B
"Status", // C "Status", // C
"Customer", // D "Customer", // D
"Tipe", // E "Sales", // E (new)
"Nama Produk", // F "Tipe", // F
"Week", // G "Nama Produk", // G
"Jumlah", // H "Gudang", // H (new)
"Satuan", // I "Week", // I
"Qty Peti", // J "Jumlah Butir", // J
"Berat Rata-rata (kg)", // K "Qty Peti", // K
"Total Berat (kg)", // L "Berat Rata-rata (kg)", // L
"Harga Satuan", // M "Total Berat (kg)", // M
"Total Harga", // N "Harga Satuan", // N
"Grand Total", // O "Total Harga", // O
"Catatan", // P "Grand Total", // P
"Catatan", // Q
} }
for i, header := range headers { for i, header := range headers {
@@ -148,7 +152,7 @@ func setMarketingExportHeaders(file *excelize.File, sheet string) error {
return err return err
} }
return file.SetCellStyle(sheet, "A1", "P1", headerStyle) return file.SetCellStyle(sheet, "A1", "Q1", headerStyle)
} }
func setMarketingExportRows(file *excelize.File, sheet string, items []dto.MarketingListDTO) error { func setMarketingExportRows(file *excelize.File, sheet string, items []dto.MarketingListDTO) error {
@@ -162,17 +166,161 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
soDate := formatMarketingExportDate(item.SoDate) soDate := formatMarketingExportDate(item.SoDate)
status := formatMarketingExportStatus(item) status := formatMarketingExportStatus(item)
customer := safeMarketingExportText(item.Customer.Name) customer := safeMarketingExportText(item.Customer.Name)
grandTotal := sumMarketingGrandTotal(item.SalesOrder)
notes := safeMarketingExportText(item.Notes) notes := safeMarketingExportText(item.Notes)
salesPerson := safeMarketingExportText(item.SalesPerson.Name)
isDeliveryOrder := strings.EqualFold(strings.TrimSpace(status), "delivery order")
// ── Delivery Order branch ──────────────────────────────────────────────
if isDeliveryOrder {
grandTotal := sumDeliveryGrandTotal(item.DeliveryOrder)
if len(item.DeliveryOrder) == 0 {
row++
r := strconv.Itoa(row)
vals := map[string]interface{}{
"A": soNumber, "B": soDate, "C": status, "D": customer, "E": salesPerson,
"F": "-", "G": "-", "H": "-", "I": "-", "J": "-", "K": "-",
"L": "-", "M": "-", "N": "-", "O": "-",
"P": grandTotal, "Q": notes,
}
for col, val := range vals {
if err := file.SetCellValue(sheet, col+r, val); err != nil {
return err
}
}
continue
}
// Build lookup map: MarketingProductId → SO product (for Week & MarketingType)
soProductMap := make(map[uint]*dto.DeliveryMarketingProductDTO, len(item.SalesOrder))
for i := range item.SalesOrder {
soProductMap[item.SalesOrder[i].Id] = &item.SalesOrder[i]
}
for _, group := range item.DeliveryOrder {
doNumber := safeMarketingExportText(group.DoNumber)
doDate := "-"
if group.DeliveryDate != nil {
doDate = formatMarketingExportDate(*group.DeliveryDate)
}
gudang := "-"
if group.Warehouse != nil {
gudang = safeMarketingExportText(group.Warehouse.Name)
}
if len(group.Deliveries) == 0 {
row++
r := strconv.Itoa(row)
vals := map[string]interface{}{
"A": doNumber, "B": doDate, "C": status, "D": customer, "E": salesPerson,
"F": "-", "G": "-", "H": gudang, "I": "-", "J": "-", "K": "-",
"L": "-", "M": "-", "N": "-", "O": "-",
"P": grandTotal, "Q": notes,
}
for col, val := range vals {
if err := file.SetCellValue(sheet, col+r, val); err != nil {
return err
}
}
continue
}
for _, delivery := range group.Deliveries {
row++
r := strconv.Itoa(row)
productName := "-"
if delivery.ProductWarehouse != nil && delivery.ProductWarehouse.Product != nil {
if n := strings.TrimSpace(delivery.ProductWarehouse.Product.Name); n != "" {
productName = n
}
}
week := "-"
marketingType := "-"
if soProduct, ok := soProductMap[delivery.MarketingProductId]; ok {
if soProduct.Week != nil {
week = strconv.Itoa(*soProduct.Week)
}
marketingType = safeMarketingExportText(soProduct.MarketingType)
}
if err := file.SetCellValue(sheet, "A"+r, doNumber); err != nil {
return err
}
if err := file.SetCellValue(sheet, "B"+r, doDate); err != nil {
return err
}
if err := file.SetCellValue(sheet, "C"+r, status); err != nil {
return err
}
if err := file.SetCellValue(sheet, "D"+r, customer); err != nil {
return err
}
if err := file.SetCellValue(sheet, "E"+r, salesPerson); err != nil {
return err
}
if err := file.SetCellValue(sheet, "F"+r, marketingType); err != nil {
return err
}
if err := file.SetCellValue(sheet, "G"+r, productName); err != nil {
return err
}
if err := file.SetCellValue(sheet, "H"+r, gudang); err != nil {
return err
}
if err := file.SetCellValue(sheet, "I"+r, week); err != nil {
return err
}
if err := file.SetCellValue(sheet, "J"+r, delivery.Qty); err != nil {
return err
}
if delivery.TotalPeti != nil {
if err := file.SetCellValue(sheet, "K"+r, *delivery.TotalPeti); err != nil {
return err
}
} else {
if err := file.SetCellValue(sheet, "K"+r, "-"); err != nil {
return err
}
}
if err := file.SetCellValue(sheet, "L"+r, delivery.AvgWeight); err != nil {
return err
}
if err := file.SetCellValue(sheet, "M"+r, delivery.TotalWeight); err != nil {
return err
}
if err := file.SetCellValue(sheet, "N"+r, delivery.UnitPrice); err != nil {
return err
}
if err := file.SetCellValue(sheet, "O"+r, delivery.TotalPrice); err != nil {
return err
}
if err := file.SetCellValue(sheet, "P"+r, grandTotal); err != nil {
return err
}
if err := file.SetCellValue(sheet, "Q"+r, notes); err != nil {
return err
}
}
}
continue
}
// ── Sales Order branch (all other statuses) ───────────────────────────
grandTotal := sumMarketingGrandTotal(item.SalesOrder)
if len(item.SalesOrder) == 0 { if len(item.SalesOrder) == 0 {
row++ row++
r := strconv.Itoa(row) r := strconv.Itoa(row)
vals := map[string]interface{}{ vals := map[string]interface{}{
"A": soNumber, "B": soDate, "C": status, "D": customer, "A": soNumber, "B": soDate, "C": status, "D": customer, "E": salesPerson,
"E": "-", "F": "-", "G": "-", "H": "-", "I": "-", "J": "-", "F": "-", "G": "-", "H": "-", "I": "-", "J": "-", "K": "-",
"K": "-", "L": "-", "M": "-", "N": "-", "L": "-", "M": "-", "N": "-", "O": "-",
"O": grandTotal, "P": notes, "P": grandTotal, "Q": notes,
} }
for col, val := range vals { for col, val := range vals {
if err := file.SetCellValue(sheet, col+r, val); err != nil { if err := file.SetCellValue(sheet, col+r, val); err != nil {
@@ -198,9 +346,9 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
week = strconv.Itoa(*prod.Week) week = strconv.Itoa(*prod.Week)
} }
satuan := "-" gudang := "-"
if prod.ConvertionUnit != nil && strings.TrimSpace(*prod.ConvertionUnit) != "" { if prod.ProductWarehouse != nil {
satuan = *prod.ConvertionUnit gudang = safeMarketingExportText(prod.ProductWarehouse.Warehouse.Name)
} }
if err := file.SetCellValue(sheet, "A"+r, soNumber); err != nil { if err := file.SetCellValue(sheet, "A"+r, soNumber); err != nil {
@@ -215,46 +363,49 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
if err := file.SetCellValue(sheet, "D"+r, customer); err != nil { if err := file.SetCellValue(sheet, "D"+r, customer); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "E"+r, safeMarketingExportText(prod.MarketingType)); err != nil { if err := file.SetCellValue(sheet, "E"+r, salesPerson); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "F"+r, productName); err != nil { if err := file.SetCellValue(sheet, "F"+r, safeMarketingExportText(prod.MarketingType)); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "G"+r, week); err != nil { if err := file.SetCellValue(sheet, "G"+r, productName); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "H"+r, prod.Qty); err != nil { if err := file.SetCellValue(sheet, "H"+r, gudang); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "I"+r, satuan); err != nil { if err := file.SetCellValue(sheet, "I"+r, week); err != nil {
return err
}
if err := file.SetCellValue(sheet, "J"+r, prod.Qty); err != nil {
return err return err
} }
if prod.TotalPeti != nil { if prod.TotalPeti != nil {
if err := file.SetCellValue(sheet, "J"+r, *prod.TotalPeti); err != nil { if err := file.SetCellValue(sheet, "K"+r, *prod.TotalPeti); err != nil {
return err return err
} }
} else { } else {
if err := file.SetCellValue(sheet, "J"+r, "-"); err != nil { if err := file.SetCellValue(sheet, "K"+r, "-"); err != nil {
return err return err
} }
} }
if err := file.SetCellValue(sheet, "K"+r, prod.AvgWeight); err != nil { if err := file.SetCellValue(sheet, "L"+r, prod.AvgWeight); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "L"+r, prod.TotalWeight); err != nil { if err := file.SetCellValue(sheet, "M"+r, prod.TotalWeight); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "M"+r, prod.UnitPrice); err != nil { if err := file.SetCellValue(sheet, "N"+r, prod.UnitPrice); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "N"+r, prod.TotalPrice); err != nil { if err := file.SetCellValue(sheet, "O"+r, prod.TotalPrice); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "O"+r, grandTotal); err != nil { if err := file.SetCellValue(sheet, "P"+r, grandTotal); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "P"+r, notes); err != nil { if err := file.SetCellValue(sheet, "Q"+r, notes); err != nil {
return err return err
} }
} }
@@ -276,7 +427,7 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
if err != nil { if err != nil {
return err return err
} }
if err := file.SetCellStyle(sheet, "A2", "P"+lastRowStr, dataStyle); err != nil { if err := file.SetCellStyle(sheet, "A2", "Q"+lastRowStr, dataStyle); err != nil {
return err return err
} }
@@ -287,7 +438,7 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
if err != nil { if err != nil {
return err return err
} }
if err := file.SetCellStyle(sheet, "K2", "O"+lastRowStr, numberStyle); err != nil { if err := file.SetCellStyle(sheet, "L2", "P"+lastRowStr, numberStyle); err != nil {
return err return err
} }
@@ -298,7 +449,7 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
if err != nil { if err != nil {
return err return err
} }
for _, col := range []string{"G", "H", "J"} { for _, col := range []string{"I", "J", "K"} {
if err := file.SetCellStyle(sheet, col+"2", col+lastRowStr, centerStyle); err != nil { if err := file.SetCellStyle(sheet, col+"2", col+lastRowStr, centerStyle); err != nil {
return err return err
} }
@@ -327,16 +478,23 @@ func formatMarketingExportStatus(item dto.MarketingListDTO) string {
return safeMarketingExportText(item.LatestApproval.StepName) return safeMarketingExportText(item.LatestApproval.StepName)
} }
func sumMarketingGrandTotal(items []dto.DeliveryMarketingProductDTO) float64 { func sumMarketingGrandTotal(items []dto.DeliveryMarketingProductDTO) float64 {
total := 0.0 total := 0.0
for _, item := range items { for _, item := range items {
total += item.TotalPrice total += item.TotalPrice
} }
return total return total
} }
func sumDeliveryGrandTotal(groups []dto.DeliveryGroupDTO) float64 {
total := 0.0
for _, g := range groups {
for _, d := range g.Deliveries {
total += d.TotalPrice
}
}
return total
}
func safeMarketingExportText(value string) string { func safeMarketingExportText(value string) string {
trimmed := strings.TrimSpace(value) trimmed := strings.TrimSpace(value)
@@ -28,6 +28,8 @@ type MarketingListDTO struct {
Customer customerDTO.CustomerRelationDTO `json:"customer"` Customer customerDTO.CustomerRelationDTO `json:"customer"`
SalesPerson userDTO.UserRelationDTO `json:"sales_person"` SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
SoDocs string `json:"so_docs"` SoDocs string `json:"so_docs"`
GrandTotalSO float64 `json:"grand_total_so"`
GrandTotalDO float64 `json:"grand_total_do"`
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"` SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"` DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"` CreatedUser userDTO.UserRelationDTO `json:"created_user"`
@@ -198,11 +200,18 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType) salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType)
} }
} }
var grandTotalSO float64
for _, p := range marketing.Products {
grandTotalSO += p.TotalPrice
}
return MarketingListDTO{ return MarketingListDTO{
MarketingRelationDTO: ToMarketingRelationDTO(marketing), MarketingRelationDTO: ToMarketingRelationDTO(marketing),
Customer: customer, Customer: customer,
SalesPerson: salesPerson, SalesPerson: salesPerson,
SoDocs: marketing.SoDocs, SoDocs: marketing.SoDocs,
GrandTotalSO: grandTotalSO,
GrandTotalDO: marketing.GrandTotal,
SalesOrder: salesOrderProducts, SalesOrder: salesOrderProducts,
DeliveryOrder: extractDeliveryGroupsFromProducts(marketing), DeliveryOrder: extractDeliveryGroupsFromProducts(marketing),
CreatedUser: createdUser, CreatedUser: createdUser,
@@ -287,6 +287,16 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
db = db.Where("marketings.customer_id = ?", params.CustomerId) db = db.Where("marketings.customer_id = ?", params.CustomerId)
} }
if params.WarehouseID != 0 {
db = db.Where(`EXISTS (
SELECT 1
FROM marketing_products mp
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
WHERE mp.marketing_id = marketings.id
AND pw.warehouse_id = ?
)`, params.WarehouseID)
}
db = s.applyMarketingProjectFlockFilter(c.Context(), db, params.ProjectFlockID, params.ProjectFlockKandangID) db = s.applyMarketingProjectFlockFilter(c.Context(), db, params.ProjectFlockID, params.ProjectFlockKandangID)
db = s.applyMarketingSearchFilter(c.Context(), db, params.Search) db = s.applyMarketingSearchFilter(c.Context(), db, params.Search)
@@ -31,6 +31,7 @@ type DeliveryOrderQuery struct {
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"` MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"` ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"` ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
WarehouseID uint `query:"warehouse_id" validate:"omitempty,gt=0"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=so_number so_date status customer grand_total created_at"` SortBy string `query:"sort_by" validate:"omitempty,oneof=so_number so_date status customer grand_total created_at"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
} }
@@ -172,6 +172,23 @@ func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
}) })
} }
func (u *ChickinController) UpdateChickInDate(c *fiber.Ctx) error {
req := new(validation.UpdateChickInDate)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := u.ChickinService.UpdateChickInDate(c, req); err != nil {
return err
}
return c.Status(fiber.StatusOK).JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Chick in date berhasil diperbarui",
})
}
func (u *ChickinController) Approval(c *fiber.Ctx) error { func (u *ChickinController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve) req := new(validation.Approve)
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"time"
"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"
@@ -18,6 +19,7 @@ type ProjectChickinRepository interface {
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error
UpdateChickInDateByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, pfkID uint, newDate time.Time) error
} }
type ChickinRepositoryImpl struct { type ChickinRepositoryImpl struct {
@@ -134,3 +136,10 @@ func (r *ChickinRepositoryImpl) UpdateUsageFields(ctx context.Context, tx *gorm.
"pending_usage_qty": pendingUsageQty, "pending_usage_qty": pendingUsageQty,
}).Error }).Error
} }
func (r *ChickinRepositoryImpl) UpdateChickInDateByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, pfkID uint, newDate time.Time) error {
return tx.WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id = ? AND deleted_at IS NULL", pfkID).
Update("chick_in_date", newDate).Error
}
@@ -17,8 +17,9 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
route.Get("/", m.RequirePermissions(m.P_ChickinsGetAll), ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_ChickinsGetAll), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne) route.Post("/", m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne) route.Patch("/chick-in-date", m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.UpdateChickInDate)
route.Get("/:id", m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne)
// route.Patch("/:id", ctrl.UpdateOne) // route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals",m.RequirePermissions(m.P_ChickinsApproval), ctrl.Approval) route.Post("/approvals", m.RequirePermissions(m.P_ChickinsApproval), ctrl.Approval)
} }
@@ -48,6 +48,7 @@ type ChickinService interface {
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
EnsureChickInExists(ctx context.Context, projectFlockKandangID uint) error EnsureChickInExists(ctx context.Context, projectFlockKandangID uint) error
UpdateChickInDate(ctx *fiber.Ctx, req *validation.UpdateChickInDate) error
} }
type chickinService struct { type chickinService struct {
@@ -2110,3 +2111,38 @@ func (s chickinService) EnsureChickInExists(ctx context.Context, projectFlockKan
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording") return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
} }
func (s chickinService) UpdateChickInDate(ctx *fiber.Ctx, req *validation.UpdateChickInDate) error {
if err := s.Validate.Struct(req); err != nil {
return err
}
newDate, err := time.Parse("2006-01-02", req.ChickInDate)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Format tanggal tidak valid, gunakan YYYY-MM-DD")
}
_, err = s.ProjectflockKandangRepo.GetByID(ctx.Context(), req.ProjectFlockKandangId)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "Project flock kandang tidak ditemukan")
}
if err := s.Repository.DB().WithContext(ctx.Context()).Transaction(func(tx *gorm.DB) error {
if err := s.Repository.UpdateChickInDateByProjectFlockKandangID(ctx.Context(), tx, req.ProjectFlockKandangId, newDate); err != nil {
return err
}
return tx.Exec(`
UPDATE recordings
SET day = GREATEST(0, (record_datetime::date - ?::date)::int),
updated_at = NOW()
WHERE project_flock_kandangs_id = ?
AND deleted_at IS NULL
`, req.ChickInDate, req.ProjectFlockKandangId).Error
}); err != nil {
return err
}
s.invalidateDepreciationSnapshots(ctx.Context(), nil, []uint{req.ProjectFlockKandangId}, newDate)
return nil
}
@@ -27,3 +27,8 @@ type Approve struct {
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type UpdateChickInDate struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,gt=0"`
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
}
@@ -8,10 +8,12 @@ import (
"time" "time"
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress" "gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/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"
) )
@@ -75,6 +77,43 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
} }
listDTO := dto.ToRecordingListDTOs(result) listDTO := dto.ToRecordingListDTOs(result)
recordingIDs := make([]uint, 0, len(result))
for i := range result {
if result[i].Id != 0 {
recordingIDs = append(recordingIDs, result[i].Id)
}
}
if len(recordingIDs) > 0 {
eggs, err := u.RecordingService.GetEggsWithFlagsByRecordingIDs(c.Context(), recordingIDs)
if err != nil {
return err
}
eggByRecording := make(map[uint][]entity.RecordingEgg, len(recordingIDs))
for _, egg := range eggs {
eggByRecording[egg.RecordingId] = append(eggByRecording[egg.RecordingId], egg)
}
for i := range listDTO {
id := listDTO[i].Id
if eggList, ok := eggByRecording[id]; ok {
breakdown := make(map[string]dto.EggExportBreakdownDTO)
for _, egg := range eggList {
flagName := eggTypeFromProductName(egg.ProductWarehouse.Product.Name)
if flagName == "" {
continue
}
entry := breakdown[flagName]
entry.Qty += egg.Qty
if egg.Weight != nil {
entry.Kg += *egg.Weight
}
breakdown[flagName] = entry
}
listDTO[i].EggExportBreakdown = breakdown
}
}
}
if strings.EqualFold(exportType, "excel") { if strings.EqualFold(exportType, "excel") {
return exportRecordingListExcel(c, listDTO) return exportRecordingListExcel(c, listDTO)
} }
@@ -94,6 +133,33 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
}) })
} }
// eggTypeFromProductName maps product name to egg type flag name by keyword matching.
// Falls back to empty string if no keyword matches.
func eggTypeFromProductName(name string) string {
normalized := strings.ToLower(strings.TrimSpace(name))
if normalized == "" {
return ""
}
// Ordered longest-first to prefer "papacal" over partial match of "pacal", etc.
keywords := []struct {
keyword string
flag string
}{
{"papacal", string(utils.FlagTelurPapacal)},
{"jumbo", string(utils.FlagTelurJumbo)},
{"retak", string(utils.FlagTelurRetak)},
{"putih", string(utils.FlagTelurPutih)},
{"pecah", string(utils.FlagTelurPecah)},
{"utuh", string(utils.FlagTelurUtuh)},
}
for _, k := range keywords {
if strings.Contains(normalized, k.keyword) {
return k.flag
}
}
return ""
}
func (u *RecordingController) GetOne(c *fiber.Ctx) error { func (u *RecordingController) GetOne(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
@@ -8,6 +8,7 @@ import (
"time" "time"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2" "github.com/xuri/excelize/v2"
@@ -79,6 +80,18 @@ func setRecordingExportColumns(file *excelize.File, sheet string) error {
"AB": 18, "AB": 18,
"AC": 24, "AC": 24,
"AD": 18, "AD": 18,
"AE": 12,
"AF": 10,
"AG": 12,
"AH": 10,
"AI": 12,
"AJ": 10,
"AK": 12,
"AL": 10,
"AM": 12,
"AN": 10,
"AO": 12,
"AP": 10,
} }
for col, width := range columnWidths { for col, width := range columnWidths {
@@ -208,6 +221,31 @@ func setRecordingExportHeaders(file *excelize.File, sheet string) error {
return err return err
} }
eggTypes := []struct {
col1, col2, label string
}{
{"AE", "AF", "Telur Utuh"},
{"AG", "AH", "Telur Pecah"},
{"AI", "AJ", "Telur Putih"},
{"AK", "AL", "Telur Retak"},
{"AM", "AN", "Telur Papacal"},
{"AO", "AP", "Telur Jumbo"},
}
for _, et := range eggTypes {
if err := file.MergeCell(sheet, et.col1+"1", et.col2+"1"); err != nil {
return err
}
if err := file.SetCellValue(sheet, et.col1+"1", et.label); err != nil {
return err
}
if err := file.SetCellValue(sheet, et.col1+"2", "Butir"); err != nil {
return err
}
if err := file.SetCellValue(sheet, et.col2+"2", "Kg"); err != nil {
return err
}
}
headerStyle, err := file.NewStyle(&excelize.Style{ headerStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{ Font: &excelize.Font{
Bold: true, Bold: true,
@@ -234,7 +272,7 @@ func setRecordingExportHeaders(file *excelize.File, sheet string) error {
return err return err
} }
return file.SetCellStyle(sheet, "A1", "AD2", headerStyle) return file.SetCellStyle(sheet, "A1", "AP2", headerStyle)
} }
func setRecordingExportRows(file *excelize.File, sheet string, items []dto.RecordingListDTO) error { func setRecordingExportRows(file *excelize.File, sheet string, items []dto.RecordingListDTO) error {
@@ -245,7 +283,8 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
columns := []string{ columns := []string{
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
"O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "AB", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "AB",
"AC", "AD", "AC", "AD", "AE", "AF", "AG", "AH", "AI", "AJ", "AK", "AL", "AM", "AN",
"AO", "AP",
} }
currentRow := 3 currentRow := 3
@@ -293,14 +332,14 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
// Expand recordings into one row per sapronak // Expand recordings into one row per sapronak
type sapronakRow struct { type sapronakRow struct {
name string name string
input string input interface{} // float64 for numeric, string "-" for placeholder
} }
sapronaks := make([]sapronakRow, 0) sapronaks := make([]sapronakRow, 0)
if len(item.FeedUsage) > 0 { if len(item.FeedUsage) > 0 {
for _, fu := range item.FeedUsage { for _, fu := range item.FeedUsage {
sapronaks = append(sapronaks, sapronakRow{ sapronaks = append(sapronaks, sapronakRow{
name: safeExportText(fu.ProductName), name: safeExportText(fu.ProductName),
input: formatNumberID(fu.UsageAmount+fu.PendingQty, 2, true), input: fu.UsageAmount + fu.PendingQty,
}) })
} }
} else { } else {
@@ -311,37 +350,66 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
for sIdx, s := range sapronaks { for sIdx, s := range sapronaks {
if sIdx == 0 { if sIdx == 0 {
eggQty := func(flagName string) int {
if item.EggExportBreakdown != nil {
if bd, ok := item.EggExportBreakdown[flagName]; ok {
return bd.Qty
}
}
return 0
}
eggKg := func(flagName string) float64 {
if item.EggExportBreakdown != nil {
if bd, ok := item.EggExportBreakdown[flagName]; ok {
return bd.Kg
}
}
return 0
}
rowValues := []interface{}{ rowValues := []interface{}{
i + 1, // A i + 1, // A
locationName, // B locationName, // B
safeExportText(item.ProjectFlock.FlockName), // C safeExportText(item.ProjectFlock.FlockName), // C
kandangName, // D kandangName, // D
item.ProjectFlock.Period, // E item.ProjectFlock.Period, // E
formatCategoryLabel(item.ProjectFlock.ProjectFlockCategory), // F formatCategoryLabel(item.ProjectFlock.ProjectFlockCategory), // F
formatAgeLabel(item), // G formatAgeLabel(item), // G
formatDateIndonesian(item.RecordDatetime), // H formatDateIndonesian(item.RecordDatetime), // H
formatNumberID(item.ProjectFlock.TotalChickQty, 0, false), // I item.ProjectFlock.TotalChickQty, // I
formatNumberID(item.FcrValue, 2, true), // J item.FcrValue, // J
formatNumberID(fcrStd, 2, true), // K fcrStd, // K
formatNumberID(item.FeedIntake, 2, true), // L item.FeedIntake, // L
formatNumberID(feedIntakeStd, 2, true), // M feedIntakeStd, // M
formatPercentID(item.CumDepletionRate, 2), // N item.CumDepletionRate, // N
formatPercentID(maxDepletionStd, 2), // O maxDepletionStd, // O
formatNumberID(item.TotalDepletionQty, 2, true), // P item.TotalDepletionQty, // P
formatNumberID(item.EggMass, 2, true), // Q item.EggMass, // Q
formatNumberID(eggMassStd, 2, true), // R eggMassStd, // R
formatNumberID(item.EggWeight, 2, true), // S item.EggWeight, // S
formatNumberID(eggWeightStd, 2, true), // T eggWeightStd, // T
formatPercentID(item.HenDay, 2), // U item.HenDay, // U
formatPercentID(henDayStd, 2), // V henDayStd, // V
formatPercentID(item.HenHouse, 2), // W item.HenHouse, // W
formatPercentID(henHouseStd, 2), // X henHouseStd, // X
formatApprovalStatus(item), // Y formatApprovalStatus(item), // Y
safeExportText(pointerString(item.Approval.Notes)), // Z safeExportText(pointerString(item.Approval.Notes)), // Z
createdBy, // AA createdBy, // AA
formatDateIndonesian(item.CreatedAt), // AB formatDateIndonesian(item.CreatedAt), // AB
s.name, // AC s.name, // AC
s.input, // AD s.input, // AD
eggQty(string(utils.FlagTelurUtuh)), // AE
eggKg(string(utils.FlagTelurUtuh)), // AF
eggQty(string(utils.FlagTelurPecah)), // AG
eggKg(string(utils.FlagTelurPecah)), // AH
eggQty(string(utils.FlagTelurPutih)), // AI
eggKg(string(utils.FlagTelurPutih)), // AJ
eggQty(string(utils.FlagTelurRetak)), // AK
eggKg(string(utils.FlagTelurRetak)), // AL
eggQty(string(utils.FlagTelurPapacal)), // AM
eggKg(string(utils.FlagTelurPapacal)), // AN
eggQty(string(utils.FlagTelurJumbo)), // AO
eggKg(string(utils.FlagTelurJumbo)), // AP
} }
for idx, col := range columns { for idx, col := range columns {
@@ -379,7 +447,7 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
if err != nil { if err != nil {
return err return err
} }
if err := file.SetCellStyle(sheet, "A3", fmt.Sprintf("AD%d", lastRow), dataCenterStyle); err != nil { if err := file.SetCellStyle(sheet, "A3", fmt.Sprintf("AP%d", lastRow), dataCenterStyle); err != nil {
return err return err
} }
@@ -445,6 +513,7 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
mergeCols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", mergeCols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
"O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "AB", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "AB",
"AE", "AF", "AG", "AH", "AI", "AJ", "AK", "AL", "AM", "AN", "AO", "AP",
} }
for _, rng := range itemRanges { for _, rng := range itemRanges {
if rng.end > rng.start { if rng.end > rng.start {
@@ -454,6 +523,53 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
} }
file.SetCellStyle(sheet, fmt.Sprintf("AC%d", rng.end), fmt.Sprintf("AC%d", rng.end), borderBottomLeftStyle) file.SetCellStyle(sheet, fmt.Sprintf("AC%d", rng.end), fmt.Sprintf("AC%d", rng.end), borderBottomLeftStyle)
file.SetCellStyle(sheet, fmt.Sprintf("AD%d", rng.end), fmt.Sprintf("AD%d", rng.end), borderBottomCenterStyle) file.SetCellStyle(sheet, fmt.Sprintf("AD%d", rng.end), fmt.Sprintf("AD%d", rng.end), borderBottomCenterStyle)
// Egg columns use center + thick bottom border
for _, col := range []string{"AE", "AF", "AG", "AH", "AI", "AJ", "AK", "AL", "AM", "AN", "AO", "AP"} {
file.SetCellStyle(sheet, fmt.Sprintf("%s%d", col, rng.end), fmt.Sprintf("%s%d", col, rng.end), borderBottomCenterStyle)
}
}
numFmtInt := "0"
numberIntStyle, err := file.NewStyle(&excelize.Style{
CustomNumFmt: &numFmtInt,
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
Border: []excelize.Border{
{Type: "left", Color: "E6E6E6", Style: 1},
{Type: "top", Color: "E6E6E6", Style: 1},
{Type: "bottom", Color: "E6E6E6", Style: 1},
{Type: "right", Color: "E6E6E6", Style: 1},
},
})
if err != nil {
return err
}
numFmtFloat := "0.00"
numberFloatStyle, err := file.NewStyle(&excelize.Style{
CustomNumFmt: &numFmtFloat,
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
Border: []excelize.Border{
{Type: "left", Color: "E6E6E6", Style: 1},
{Type: "top", Color: "E6E6E6", Style: 1},
{Type: "bottom", Color: "E6E6E6", Style: 1},
{Type: "right", Color: "E6E6E6", Style: 1},
},
})
if err != nil {
return err
}
intCols := []string{"E", "I", "AE", "AG", "AI", "AK", "AM", "AO"}
for _, col := range intCols {
if err := file.SetCellStyle(sheet, col+"3", fmt.Sprintf("%s%d", col, lastRow), numberIntStyle); err != nil {
return err
}
}
floatCols := []string{"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "AD", "AF", "AH", "AJ", "AL", "AN", "AP"}
for _, col := range floatCols {
if err := file.SetCellStyle(sheet, col+"3", fmt.Sprintf("%s%d", col, lastRow), numberFloatStyle); err != nil {
return err
}
} }
return nil return nil
@@ -100,14 +100,20 @@ type RecordingFeedUsageDTO struct {
PendingQty float64 `json:"pending_qty"` PendingQty float64 `json:"pending_qty"`
} }
type EggExportBreakdownDTO struct {
Qty int `json:"qty"`
Kg float64 `json:"kg"`
}
type RecordingListDTO struct { type RecordingListDTO struct {
RecordingRelationDTO RecordingRelationDTO
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"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Kandang *RecordingKandangDTO `json:"kandang,omitempty"` Kandang *RecordingKandangDTO `json:"kandang,omitempty"`
Location *RecordingLocationDTO `json:"location,omitempty"` Location *RecordingLocationDTO `json:"location,omitempty"`
FeedUsage []RecordingFeedUsageDTO `json:"feed_usage,omitempty"` FeedUsage []RecordingFeedUsageDTO `json:"feed_usage,omitempty"`
EggExportBreakdown map[string]EggExportBreakdownDTO `json:"egg_breakdown,omitempty"`
} }
type RecordingDetailDTO struct { type RecordingDetailDTO struct {
@@ -51,6 +51,7 @@ type RecordingRepository interface {
UpdateEggTotalQty(tx *gorm.DB, eggID uint, totalQty float64) error UpdateEggTotalQty(tx *gorm.DB, eggID uint, totalQty float64) error
UpdateEggWeight(tx *gorm.DB, eggID uint, weight *float64) error UpdateEggWeight(tx *gorm.DB, eggID uint, weight *float64) error
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error) GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
GetEggsWithFlagsByRecordingIDs(ctx context.Context, recordingIDs []uint) ([]entity.RecordingEgg, error)
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
@@ -581,6 +582,22 @@ func (r *RecordingRepositoryImpl) GetRecordingEggByID(
return &egg, nil return &egg, nil
} }
func (r *RecordingRepositoryImpl) GetEggsWithFlagsByRecordingIDs(ctx context.Context, recordingIDs []uint) ([]entity.RecordingEgg, error) {
if len(recordingIDs) == 0 {
return nil, nil
}
var eggs []entity.RecordingEgg
err := r.DB().WithContext(ctx).
Preload("ProductWarehouse.Product").
Where("recording_eggs.recording_id IN ?", recordingIDs).
Find(&eggs).Error
if err != nil {
return nil, err
}
return eggs, nil
}
func (r *RecordingRepositoryImpl) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) { func (r *RecordingRepositoryImpl) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) {
if projectFlockKandangId == 0 { if projectFlockKandangId == 0 {
return false, nil return false, nil
@@ -46,6 +46,7 @@ type RecordingService interface {
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error)
GetProgressRows(ctx *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error) GetProgressRows(ctx *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error)
GetEggsWithFlagsByRecordingIDs(ctx context.Context, recordingIDs []uint) ([]entity.RecordingEgg, error)
} }
type recordingService struct { type recordingService struct {
@@ -259,6 +260,10 @@ func (s recordingService) GetProgressRows(c *fiber.Ctx, query *exportprogress.Qu
return s.Repository.GetProgressRows(c.Context(), query.StartDate, query.EndDate, scope.IDs, scope.Restrict) return s.Repository.GetProgressRows(c.Context(), query.StartDate, query.EndDate, scope.IDs, scope.Restrict)
} }
func (s recordingService) GetEggsWithFlagsByRecordingIDs(ctx context.Context, recordingIDs []uint) ([]entity.RecordingEgg, error) {
return s.Repository.GetEggsWithFlagsByRecordingIDs(ctx, recordingIDs)
}
func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) { func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) {
if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil { if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
return nil, err return nil, err