Merge branch 'feat/export-recording-a' into 'development'

[FEAT][BE]: add feed use at export excel recording

See merge request mbugroup/lti-api!511
This commit is contained in:
Giovanni Gabriel Septriadi
2026-05-07 03:58:19 +00:00
3 changed files with 100 additions and 23 deletions
@@ -77,6 +77,10 @@ func setRecordingExportColumns(file *excelize.File, sheet string) error {
"Z": 22, "Z": 22,
"AA": 16, "AA": 16,
"AB": 18, "AB": 18,
"AC": 24,
"AD": 18,
"AE": 18,
"AF": 18,
} }
for col, width := range columnWidths { for col, width := range columnWidths {
@@ -96,7 +100,7 @@ func setRecordingExportColumns(file *excelize.File, sheet string) error {
} }
func setRecordingExportHeaders(file *excelize.File, sheet string) error { func setRecordingExportHeaders(file *excelize.File, sheet string) error {
verticalHeaderCols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "Y", "Z", "AA", "AB"} verticalHeaderCols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "Y", "Z", "AA", "AB", "AC", "AD", "AE", "AF"}
for _, col := range verticalHeaderCols { for _, col := range verticalHeaderCols {
if err := file.MergeCell(sheet, col+"1", col+"2"); err != nil { if err := file.MergeCell(sheet, col+"1", col+"2"); err != nil {
return err return err
@@ -104,19 +108,23 @@ func setRecordingExportHeaders(file *excelize.File, sheet string) error {
} }
headerValues := map[string]string{ headerValues := map[string]string{
"A1": "No", "A1": "No",
"B1": "Lokasi", "B1": "Lokasi",
"C1": "Flock", "C1": "Flock",
"D1": "Kandang", "D1": "Kandang",
"E1": "Periode", "E1": "Periode",
"F1": "Kategori", "F1": "Kategori",
"G1": "Umur (hari)", "G1": "Umur (hari)",
"H1": "Waktu Recording", "H1": "Waktu Recording",
"I1": "Populasi Akhir", "I1": "Populasi Akhir",
"Y1": "Status Approval", "Y1": "Status Approval",
"Z1": "Catatan Approval", "Z1": "Catatan Approval",
"AA1": "Dibuat Oleh", "AA1": "Dibuat Oleh",
"AB1": "Tanggal Submit", "AB1": "Tanggal Submit",
"AC1": "Nama Pakan",
"AD1": "Jumlah Input Pakan",
"AE1": "Jumlah Penggunaan",
"AF1": "Pending Qty",
} }
for cell, value := range headerValues { for cell, value := range headerValues {
if err := file.SetCellValue(sheet, cell, value); err != nil { if err := file.SetCellValue(sheet, cell, value); err != nil {
@@ -230,7 +238,7 @@ func setRecordingExportHeaders(file *excelize.File, sheet string) error {
return err return err
} }
return file.SetCellStyle(sheet, "A1", "AB2", headerStyle) return file.SetCellStyle(sheet, "A1", "AF2", headerStyle)
} }
func setRecordingExportRows(file *excelize.File, sheet string, items []dto.RecordingListDTO) error { func setRecordingExportRows(file *excelize.File, sheet string, items []dto.RecordingListDTO) error {
@@ -241,6 +249,7 @@ 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", "AE", "AF",
} }
for i, item := range items { for i, item := range items {
@@ -283,6 +292,29 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
createdBy = safeExportText(item.Approval.ActionBy.Name) createdBy = safeExportText(item.Approval.ActionBy.Name)
} }
// Build feed usage columns — concatenate multiple feeds with newline
feedNames := make([]string, 0, len(item.FeedUsage))
usageAmounts := make([]string, 0, len(item.FeedUsage))
pendingQtys := make([]string, 0, len(item.FeedUsage))
inputQtys := make([]string, 0, len(item.FeedUsage))
for _, fu := range item.FeedUsage {
feedNames = append(feedNames, safeExportText(fu.ProductName))
usageAmounts = append(usageAmounts, formatNumberID(fu.UsageAmount, 2, true))
pendingQtys = append(pendingQtys, formatNumberID(fu.PendingQty, 2, true))
inputQtys = append(inputQtys, formatNumberID(fu.UsageAmount+fu.PendingQty, 2, true))
}
feedNameCol := "-"
usageCol := "-"
pendingCol := "-"
inputCol := "-"
if len(feedNames) > 0 {
feedNameCol = strings.Join(feedNames, "\n")
usageCol = strings.Join(usageAmounts, "\n")
pendingCol = strings.Join(pendingQtys, "\n")
inputCol = strings.Join(inputQtys, "\n")
}
rowValues := []interface{}{ rowValues := []interface{}{
i + 1, i + 1,
locationName, locationName,
@@ -312,6 +344,10 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
safeExportText(pointerString(item.Approval.Notes)), safeExportText(pointerString(item.Approval.Notes)),
createdBy, createdBy,
formatDateIndonesian(item.CreatedAt), formatDateIndonesian(item.CreatedAt),
feedNameCol, // AC
inputCol, // AD - Jumlah Input Pakan
usageCol, // AE - Jumlah Penggunaan
pendingCol, // AF - Pending Qty
} }
for idx, col := range columns { for idx, col := range columns {
@@ -339,7 +375,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("AB%d", lastRow), dataCenterStyle); err != nil { if err := file.SetCellStyle(sheet, "A3", fmt.Sprintf("AF%d", lastRow), dataCenterStyle); err != nil {
return err return err
} }
@@ -360,7 +396,7 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
return err return err
} }
leftColumns := []string{"B", "C", "D", "F", "G", "H", "Y", "Z", "AA", "AB"} leftColumns := []string{"B", "C", "D", "F", "G", "H", "Y", "Z", "AA", "AB", "AC"}
for _, col := range leftColumns { for _, col := range leftColumns {
if err := file.SetCellStyle(sheet, col+"3", fmt.Sprintf("%s%d", col, lastRow), dataLeftStyle); err != nil { if err := file.SetCellStyle(sheet, col+"3", fmt.Sprintf("%s%d", col, lastRow), dataLeftStyle); err != nil {
return err return err
@@ -92,13 +92,20 @@ type RecordingRelationDTO struct {
Approval approvalDTO.ApprovalRelationDTO `json:"approval"` Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
} }
type RecordingFeedUsageDTO struct {
ProductName string `json:"product_name"`
UsageAmount float64 `json:"usage_amount"`
PendingQty float64 `json:"pending_qty"`
}
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"`
} }
type RecordingDetailDTO struct { type RecordingDetailDTO struct {
@@ -192,6 +199,36 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
return result return result
} }
func ToRecordingFeedUsageDTOs(stocks []entity.RecordingStock) []RecordingFeedUsageDTO {
return toRecordingFeedUsageDTOs(stocks)
}
func toRecordingFeedUsageDTOs(stocks []entity.RecordingStock) []RecordingFeedUsageDTO {
result := make([]RecordingFeedUsageDTO, 0, len(stocks))
for _, s := range stocks {
productName := ""
if s.ProductWarehouse.Product.Id != 0 {
productName = s.ProductWarehouse.Product.Name
}
var usageAmount float64
if s.UsageQty != nil {
usageAmount = *s.UsageQty
}
var pendingQty float64
if s.PendingQty != nil {
pendingQty = *s.PendingQty
}
result = append(result, RecordingFeedUsageDTO{
ProductName: productName,
UsageAmount: usageAmount,
PendingQty: pendingQty,
})
}
return result
}
func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO { func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
result := make([]RecordingEggDTO, len(eggs)) result := make([]RecordingEggDTO, len(eggs))
for i, egg := range eggs { for i, egg := range eggs {
@@ -222,6 +259,7 @@ func toRecordingListDTO(e entity.Recording) RecordingListDTO {
CreatedUser: createdUser, CreatedUser: createdUser,
Kandang: recordingKandangDTO(e), Kandang: recordingKandangDTO(e),
Location: recordingKandangLocationDTO(e), Location: recordingKandangLocationDTO(e),
FeedUsage: toRecordingFeedUsageDTOs(e.Stocks),
} }
} }
@@ -147,7 +147,10 @@ func (r *RecordingRepositoryImpl) WithRelationsList(db *gorm.DB) *gorm.DB {
Preload("ProjectFlockKandang.Kandang"). Preload("ProjectFlockKandang.Kandang").
Preload("ProjectFlockKandang.Kandang.Location"). Preload("ProjectFlockKandang.Kandang.Location").
Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard") Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
Preload("Stocks").
Preload("Stocks.ProductWarehouse").
Preload("Stocks.ProductWarehouse.Product")
} }
func (r *RecordingRepositoryImpl) latestApprovalSubQuery(db *gorm.DB) *gorm.DB { func (r *RecordingRepositoryImpl) latestApprovalSubQuery(db *gorm.DB) *gorm.DB {