mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
add export excel to get all recording
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user