add export excel to get all recording

This commit is contained in:
giovanni
2026-04-09 11:18:05 +07:00
parent aad4f7dc28
commit abc0ac8258
2 changed files with 524 additions and 1 deletions
@@ -26,6 +26,7 @@ func NewRecordingController(recordingService service.RecordingService) *Recordin
func (u *RecordingController) GetAll(c *fiber.Ctx) error {
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
exportType := strings.TrimSpace(c.Query("export"))
page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 10)
@@ -46,6 +47,11 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
return err
}
listDTO := dto.ToRecordingListDTOs(result)
if strings.EqualFold(exportType, "excel") {
return exportRecordingListExcel(c, listDTO)
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.RecordingListDTO]{
Code: fiber.StatusOK,
@@ -57,7 +63,7 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToRecordingListDTOs(result),
Data: listDTO,
})
}
@@ -0,0 +1,517 @@
package controller
import (
"fmt"
"math"
"strconv"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto"
"github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2"
)
func exportRecordingListExcel(c *fiber.Ctx, items []dto.RecordingListDTO) error {
file := excelize.NewFile()
defer file.Close()
const sheetName = "Recordings"
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
if defaultSheet != sheetName {
if err := file.SetSheetName(defaultSheet, sheetName); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare excel sheet")
}
}
if err := setRecordingExportColumns(file, sheetName); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare excel columns")
}
if err := setRecordingExportHeaders(file, sheetName); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare excel headers")
}
if err := setRecordingExportRows(file, sheetName, items); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare excel rows")
}
buffer, err := file.WriteToBuffer()
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
}
filename := fmt.Sprintf("recordings_%s.xlsx", time.Now().Format("20060102_150405"))
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
return c.Status(fiber.StatusOK).Send(buffer.Bytes())
}
func setRecordingExportColumns(file *excelize.File, sheet string) error {
columnWidths := map[string]float64{
"A": 6,
"B": 18,
"C": 24,
"D": 18,
"E": 10,
"F": 12,
"G": 20,
"H": 18,
"I": 16,
"J": 12,
"K": 12,
"L": 16,
"M": 16,
"N": 18,
"O": 18,
"P": 16,
"Q": 16,
"R": 16,
"S": 16,
"T": 16,
"U": 16,
"V": 16,
"W": 18,
"X": 18,
"Y": 18,
"Z": 22,
"AA": 16,
"AB": 18,
}
for col, width := range columnWidths {
if err := file.SetColWidth(sheet, col, col, width); err != nil {
return err
}
}
if err := file.SetRowHeight(sheet, 1, 30); err != nil {
return err
}
if err := file.SetRowHeight(sheet, 2, 30); err != nil {
return err
}
return nil
}
func setRecordingExportHeaders(file *excelize.File, sheet string) error {
verticalHeaderCols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "Y", "Z", "AA", "AB"}
for _, col := range verticalHeaderCols {
if err := file.MergeCell(sheet, col+"1", col+"2"); err != nil {
return err
}
}
headerValues := map[string]string{
"A1": "No",
"B1": "Lokasi",
"C1": "Flock",
"D1": "Kandang",
"E1": "Periode",
"F1": "Kategori",
"G1": "Umur (hari)",
"H1": "Waktu Recording",
"I1": "Populasi Akhir",
"Y1": "Status Approval",
"Z1": "Catatan Approval",
"AA1": "Dibuat Oleh",
"AB1": "Tanggal Submit",
}
for cell, value := range headerValues {
if err := file.SetCellValue(sheet, cell, value); err != nil {
return err
}
}
if err := file.MergeCell(sheet, "J1", "K1"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "J1", "FCR"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "J2", "Actual"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "K2", "Standard"); err != nil {
return err
}
if err := file.MergeCell(sheet, "L1", "M1"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "L1", "Feed Intake (KG)"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "L2", "Actual"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "M2", "Standard"); err != nil {
return err
}
if err := file.MergeCell(sheet, "N1", "P1"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "N1", "Mortality"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "N2", "Cum Depletion Rate"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "O2", "Max Depletion Std"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "P2", "Total Depletion"); err != nil {
return err
}
if err := file.MergeCell(sheet, "Q1", "T1"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "Q1", "Egg Production"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "Q2", "Egg Mass Actual"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "R2", "Egg Mass Standar"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "S2", "Egg Weight Actual"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "T2", "Egg Weight Standar"); err != nil {
return err
}
if err := file.MergeCell(sheet, "U1", "X1"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "U1", "Hen Performance"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "U2", "Hen Day Actual"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "V2", "Hen Day Standar"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "W2", "Hen House Actual"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "X2", "Hen House Standar"); err != nil {
return err
}
headerStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
Color: "7A7A7A",
},
Fill: excelize.Fill{
Type: "pattern",
Pattern: 1,
Color: []string{"F5F5F5"},
},
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
WrapText: true,
},
Border: []excelize.Border{
{Type: "left", Color: "DDDDDD", Style: 1},
{Type: "top", Color: "DDDDDD", Style: 1},
{Type: "bottom", Color: "DDDDDD", Style: 1},
{Type: "right", Color: "DDDDDD", Style: 1},
},
})
if err != nil {
return err
}
return file.SetCellStyle(sheet, "A1", "AB2", headerStyle)
}
func setRecordingExportRows(file *excelize.File, sheet string, items []dto.RecordingListDTO) error {
if len(items) == 0 {
return nil
}
columns := []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",
}
for i, item := range items {
rowNumber := i + 3
fcrStd := 0.0
if item.ProjectFlock.Fcr != nil {
fcrStd = item.ProjectFlock.Fcr.FcrStd
}
maxDepletionStd := 0.0
eggMassStd := 0.0
eggWeightStd := 0.0
henDayStd := 0.0
henHouseStd := 0.0
feedIntakeStd := 0.0
if item.ProjectFlock.ProductionStandart != nil {
maxDepletionStd = item.ProjectFlock.ProductionStandart.MaxDepletionStd
eggMassStd = item.ProjectFlock.ProductionStandart.EggMassStd
eggWeightStd = item.ProjectFlock.ProductionStandart.EggWeightStd
henDayStd = item.ProjectFlock.ProductionStandart.HenDayStd
henHouseStd = item.ProjectFlock.ProductionStandart.HenHouseStd
feedIntakeStd = item.ProjectFlock.ProductionStandart.FeedIntakeStd
}
locationName := "-"
if item.Location != nil {
locationName = safeExportText(item.Location.Name)
}
kandangName := "-"
if item.Kandang != nil {
kandangName = safeExportText(item.Kandang.Name)
}
createdBy := "-"
if item.CreatedUser != nil {
createdBy = safeExportText(item.CreatedUser.Name)
} else if strings.TrimSpace(item.Approval.ActionBy.Name) != "" {
createdBy = safeExportText(item.Approval.ActionBy.Name)
}
rowValues := []interface{}{
i + 1,
locationName,
safeExportText(item.ProjectFlock.FlockName),
kandangName,
item.ProjectFlock.Period,
formatCategoryLabel(item.ProjectFlock.ProjectFlockCategory),
formatAgeLabel(item),
formatDateIndonesian(item.RecordDatetime),
formatNumberID(item.ProjectFlock.TotalChickQty, 0, false),
formatNumberID(item.FcrValue, 2, true),
formatNumberID(fcrStd, 2, true),
formatNumberID(item.FeedIntake, 2, true),
formatNumberID(feedIntakeStd, 2, true),
formatPercentID(item.CumDepletionRate, 2),
formatPercentID(maxDepletionStd, 2),
formatNumberID(item.TotalDepletionQty, 2, true),
formatNumberID(item.EggMass, 2, true),
formatNumberID(eggMassStd, 2, true),
formatNumberID(item.EggWeight, 2, true),
formatNumberID(eggWeightStd, 2, true),
formatPercentID(item.HenDay, 2),
formatPercentID(henDayStd, 2),
formatPercentID(item.HenHouse, 2),
formatPercentID(henHouseStd, 2),
formatApprovalStatus(item),
safeExportText(pointerString(item.Approval.Notes)),
createdBy,
formatDateIndonesian(item.CreatedAt),
}
for idx, col := range columns {
cell := fmt.Sprintf("%s%d", col, rowNumber)
if err := file.SetCellValue(sheet, cell, rowValues[idx]); err != nil {
return err
}
}
}
lastRow := len(items) + 2
dataCenterStyle, err := file.NewStyle(&excelize.Style{
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
}
if err := file.SetCellStyle(sheet, "A3", fmt.Sprintf("AB%d", lastRow), dataCenterStyle); err != nil {
return err
}
dataLeftStyle, err := file.NewStyle(&excelize.Style{
Alignment: &excelize.Alignment{
Horizontal: "left",
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
}
leftColumns := []string{"B", "C", "D", "F", "G", "H", "Y", "Z", "AA", "AB"}
for _, col := range leftColumns {
if err := file.SetCellStyle(sheet, col+"3", fmt.Sprintf("%s%d", col, lastRow), dataLeftStyle); err != nil {
return err
}
}
return nil
}
func formatAgeLabel(item dto.RecordingListDTO) string {
if item.Day <= 0 {
return "-"
}
week := 0
if item.ProjectFlock.ProductionStandart != nil && item.ProjectFlock.ProductionStandart.Week > 0 {
week = item.ProjectFlock.ProductionStandart.Week
} else {
week = ((item.Day - 1) / 7) + 1
}
return fmt.Sprintf("%d (Minggu ke-%d)", item.Day, week)
}
func formatDateIndonesian(t time.Time) string {
if t.IsZero() {
return "-"
}
loc, err := time.LoadLocation("Asia/Jakarta")
if err == nil {
t = t.In(loc)
}
monthNames := []string{
"",
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember",
}
month := int(t.Month())
monthLabel := strconv.Itoa(month)
if month > 0 && month < len(monthNames) {
monthLabel = monthNames[month]
}
return fmt.Sprintf("%02d %s %d", t.Day(), monthLabel, t.Year())
}
func formatCategoryLabel(value string) string {
normalized := strings.TrimSpace(strings.ReplaceAll(value, "_", " "))
if normalized == "" {
return "-"
}
parts := strings.Fields(strings.ToLower(normalized))
for i, part := range parts {
if len(part) == 0 {
continue
}
parts[i] = strings.ToUpper(part[:1]) + part[1:]
}
return strings.Join(parts, " ")
}
func formatPercentID(value float64, decimals int) string {
return fmt.Sprintf("%s%%", formatNumberID(value, decimals, false))
}
func formatNumberID(value float64, decimals int, trim bool) string {
if math.IsNaN(value) || math.IsInf(value, 0) {
return "0"
}
if decimals < 0 {
decimals = 0
}
raw := strconv.FormatFloat(value, 'f', decimals, 64)
if trim && strings.Contains(raw, ".") {
raw = strings.TrimRight(raw, "0")
raw = strings.TrimRight(raw, ".")
}
parts := strings.SplitN(raw, ".", 2)
intPart := parts[0]
sign := ""
if strings.HasPrefix(intPart, "-") {
sign = "-"
intPart = strings.TrimPrefix(intPart, "-")
}
if intPart == "" {
intPart = "0"
}
var grouped strings.Builder
rem := len(intPart) % 3
if rem > 0 {
grouped.WriteString(intPart[:rem])
if len(intPart) > rem {
grouped.WriteString(".")
}
}
for i := rem; i < len(intPart); i += 3 {
grouped.WriteString(intPart[i : i+3])
if i+3 < len(intPart) {
grouped.WriteString(".")
}
}
result := sign + grouped.String()
if len(parts) == 2 && parts[1] != "" {
result += "," + parts[1]
}
return result
}
func safeExportText(value string) string {
normalized := strings.TrimSpace(value)
if normalized == "" {
return "-"
}
return normalized
}
func pointerString(value *string) string {
if value == nil {
return ""
}
return *value
}
func formatApprovalStatus(item dto.RecordingListDTO) string {
action := strings.ToUpper(strings.TrimSpace(pointerString(item.Approval.Action)))
switch action {
case "UPDATED":
return "Diperbarui"
case "CREATED":
return safeExportText(item.Approval.StepName)
default:
return safeExportText(item.Approval.StepName)
}
}