mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
605 lines
19 KiB
Go
605 lines
19 KiB
Go
package exportprogress
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
const (
|
|
UnassignedKandangName = "Farm-level / Unassigned"
|
|
jakartaTZ = "Asia/Jakarta"
|
|
)
|
|
|
|
type Query struct {
|
|
StartDate time.Time
|
|
EndDate time.Time
|
|
StartDateRaw string
|
|
EndDateRaw string
|
|
}
|
|
|
|
type Row struct {
|
|
Module string
|
|
FarmName string
|
|
KandangName string
|
|
ActivityDate time.Time
|
|
Count int
|
|
}
|
|
|
|
type monthBlock struct {
|
|
Start time.Time
|
|
Weeks int
|
|
}
|
|
|
|
func IsProgressExportRequest(c *fiber.Ctx) bool {
|
|
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") &&
|
|
strings.EqualFold(strings.TrimSpace(c.Query("type")), "progress")
|
|
}
|
|
|
|
func ParseQuery(c *fiber.Ctx) (*Query, error) {
|
|
location, err := time.LoadLocation(jakartaTZ)
|
|
if err != nil {
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
|
}
|
|
|
|
startRaw := strings.TrimSpace(c.Query("start_date"))
|
|
endRaw := strings.TrimSpace(c.Query("end_date"))
|
|
if startRaw == "" || endRaw == "" {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "start_date and end_date are required")
|
|
}
|
|
|
|
startDate, err := time.ParseInLocation("2006-01-02", startRaw, location)
|
|
if err != nil {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "start_date must use format YYYY-MM-DD")
|
|
}
|
|
endDate, err := time.ParseInLocation("2006-01-02", endRaw, location)
|
|
if err != nil {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "end_date must use format YYYY-MM-DD")
|
|
}
|
|
if endDate.Before(startDate) {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "end_date must be greater than or equal to start_date")
|
|
}
|
|
|
|
return &Query{
|
|
StartDate: startDate,
|
|
EndDate: endDate,
|
|
StartDateRaw: startRaw,
|
|
EndDateRaw: endRaw,
|
|
}, nil
|
|
}
|
|
|
|
func BuildWorkbook(moduleTitle string, query *Query, rows []Row) ([]byte, error) {
|
|
file := excelize.NewFile()
|
|
defer file.Close()
|
|
|
|
sheetName := moduleTitle
|
|
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
|
|
if defaultSheet != sheetName {
|
|
if err := file.SetSheetName(defaultSheet, sheetName); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
location, err := time.LoadLocation(jakartaTZ)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
titleStyle, metaStyle, monthStyle, weekStyle, dayHeaderStyle, farmStyle, textStyle, numberStyle, subtotalStyle, err := buildStyles(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
months := monthBlocksBetween(query.StartDate, query.EndDate)
|
|
maxWeeks := 4
|
|
for _, block := range months {
|
|
if block.Weeks > maxWeeks {
|
|
maxWeeks = block.Weeks
|
|
}
|
|
}
|
|
lastColName, err := excelize.ColumnNumberToName(1 + (maxWeeks * 7) + 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := file.MergeCell(sheetName, "A1", lastColName+"1"); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellValue(sheetName, "A1", moduleTitle); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, "A1", lastColName+"1", titleStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
metaValue := fmt.Sprintf(
|
|
"Range: %s to %s | Generated at: %s",
|
|
query.StartDateRaw,
|
|
query.EndDateRaw,
|
|
time.Now().In(location).Format("2006-01-02 15:04:05 MST"),
|
|
)
|
|
if err := file.MergeCell(sheetName, "A2", lastColName+"2"); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellValue(sheetName, "A2", metaValue); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, "A2", lastColName+"2", metaStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := applyColumnWidths(file, sheetName, maxWeeks); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
grouped := groupRows(rows)
|
|
currentRow := 4
|
|
for _, month := range months {
|
|
lastColIndex := 1 + (month.Weeks * 7) + 1
|
|
monthLastCol, err := excelize.ColumnNumberToName(lastColIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := renderMonthHeader(file, sheetName, currentRow, month, monthLastCol, monthStyle, weekStyle, dayHeaderStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
currentRow += 4
|
|
|
|
monthData := grouped[month.Start.Format("2006-01")]
|
|
if len(monthData) == 0 {
|
|
if err := file.MergeCell(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow)); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellValue(sheetName, "A"+fmt.Sprint(currentRow), "No progress data"); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow), textStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
currentRow += 2
|
|
continue
|
|
}
|
|
|
|
farms := sortedKeys(monthData)
|
|
for _, farm := range farms {
|
|
if err := file.MergeCell(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow)); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellValue(sheetName, "A"+fmt.Sprint(currentRow), farm); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow), farmStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
currentRow++
|
|
|
|
kandangs := sortedKeys(monthData[farm])
|
|
farmTotals := make(map[string]int)
|
|
farmGrandTotal := 0
|
|
for _, kandang := range kandangs {
|
|
rowCounts := monthData[farm][kandang]
|
|
rowTotal := 0
|
|
|
|
if err := file.SetCellValue(sheetName, "A"+fmt.Sprint(currentRow), kandang); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), "A"+fmt.Sprint(currentRow), textStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for dayKey, count := range rowCounts {
|
|
activityDate, err := time.ParseInLocation("2006-01-02", dayKey, location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
colIndex := dayColumnIndex(month, activityDate)
|
|
colName, err := excelize.ColumnNumberToName(colIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellValue(sheetName, colName+fmt.Sprint(currentRow), count); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, colName+fmt.Sprint(currentRow), colName+fmt.Sprint(currentRow), numberStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
rowTotal += count
|
|
farmTotals[dayKey] += count
|
|
farmGrandTotal += count
|
|
}
|
|
|
|
if err := file.SetCellValue(sheetName, monthLastCol+fmt.Sprint(currentRow), rowTotal); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, monthLastCol+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow), subtotalStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, "B"+fmt.Sprint(currentRow), prevColumn(monthLastCol)+fmt.Sprint(currentRow), numberStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
currentRow++
|
|
}
|
|
|
|
if err := file.SetCellValue(sheetName, "A"+fmt.Sprint(currentRow), "Subtotal"); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), "A"+fmt.Sprint(currentRow), subtotalStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
for dayKey, count := range farmTotals {
|
|
activityDate, err := time.ParseInLocation("2006-01-02", dayKey, location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
colIndex := dayColumnIndex(month, activityDate)
|
|
colName, err := excelize.ColumnNumberToName(colIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellValue(sheetName, colName+fmt.Sprint(currentRow), count); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := file.SetCellValue(sheetName, monthLastCol+fmt.Sprint(currentRow), farmGrandTotal); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.SetCellStyle(sheetName, "A"+fmt.Sprint(currentRow), monthLastCol+fmt.Sprint(currentRow), subtotalStyle); err != nil {
|
|
return nil, err
|
|
}
|
|
currentRow += 2
|
|
}
|
|
}
|
|
|
|
if err := file.SetPanes(sheetName, &excelize.Panes{
|
|
Freeze: true,
|
|
YSplit: 2,
|
|
TopLeftCell: "A3",
|
|
ActivePane: "bottomLeft",
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buffer, err := file.WriteToBuffer()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return buffer.Bytes(), nil
|
|
}
|
|
|
|
func ParseActivityDate(value string) (time.Time, error) {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
return time.Time{}, fmt.Errorf("empty activity date")
|
|
}
|
|
|
|
layouts := []string{
|
|
"2006-01-02",
|
|
time.RFC3339,
|
|
time.RFC3339Nano,
|
|
"2006-01-02 15:04:05Z07:00",
|
|
"2006-01-02 15:04:05.999999999Z07:00",
|
|
}
|
|
for _, layout := range layouts {
|
|
if parsed, err := time.Parse(layout, trimmed); err == nil {
|
|
return parsed, nil
|
|
}
|
|
}
|
|
|
|
if len(trimmed) >= len("2006-01-02") {
|
|
if parsed, err := time.Parse("2006-01-02", trimmed[:10]); err == nil {
|
|
return parsed, nil
|
|
}
|
|
}
|
|
|
|
return time.Time{}, fmt.Errorf("unsupported activity date format: %s", value)
|
|
}
|
|
|
|
func buildStyles(file *excelize.File) (int, int, int, int, int, int, int, int, int, error) {
|
|
titleStyle, err := file.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Size: 18, Color: "1F2937"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"DCEBFA"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
|
|
}
|
|
metaStyle, err := file.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Italic: true, Color: "4B5563"},
|
|
Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center"},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
|
|
}
|
|
monthStyle, err := file.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Color: "FFFFFF"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"1D4ED8"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center"},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
|
|
}
|
|
weekStyle, err := file.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Color: "1F2937"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"DBEAFE"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
|
Border: []excelize.Border{{Type: "bottom", Color: "93C5FD", Style: 1}},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
|
|
}
|
|
dayHeaderStyle, err := file.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Color: "374151"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"EFF6FF"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "BFDBFE", Style: 1},
|
|
{Type: "top", Color: "BFDBFE", Style: 1},
|
|
{Type: "bottom", Color: "BFDBFE", Style: 1},
|
|
{Type: "right", Color: "BFDBFE", Style: 1},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
|
|
}
|
|
farmStyle, err := file.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Color: "111827"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E5E7EB"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center"},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
|
|
}
|
|
textStyle, err := file.NewStyle(&excelize.Style{
|
|
Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center"},
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "D1D5DB", Style: 1},
|
|
{Type: "top", Color: "D1D5DB", Style: 1},
|
|
{Type: "bottom", Color: "D1D5DB", Style: 1},
|
|
{Type: "right", Color: "D1D5DB", Style: 1},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
|
|
}
|
|
numberStyle, err := file.NewStyle(&excelize.Style{
|
|
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "D1D5DB", Style: 1},
|
|
{Type: "top", Color: "D1D5DB", Style: 1},
|
|
{Type: "bottom", Color: "D1D5DB", Style: 1},
|
|
{Type: "right", Color: "D1D5DB", Style: 1},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
|
|
}
|
|
subtotalStyle, err := file.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Color: "1F2937"},
|
|
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"F3F4F6"}},
|
|
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "9CA3AF", Style: 1},
|
|
{Type: "top", Color: "9CA3AF", Style: 1},
|
|
{Type: "bottom", Color: "9CA3AF", Style: 1},
|
|
{Type: "right", Color: "9CA3AF", Style: 1},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, 0, 0, 0, 0, err
|
|
}
|
|
|
|
return titleStyle, metaStyle, monthStyle, weekStyle, dayHeaderStyle, farmStyle, textStyle, numberStyle, subtotalStyle, nil
|
|
}
|
|
|
|
func applyColumnWidths(file *excelize.File, sheet string, maxWeeks int) error {
|
|
if err := file.SetColWidth(sheet, "A", "A", 28); err != nil {
|
|
return err
|
|
}
|
|
for col := 2; col <= 1+(maxWeeks*7); col++ {
|
|
colName, err := excelize.ColumnNumberToName(col)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetColWidth(sheet, colName, colName, 6); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
totalCol, err := excelize.ColumnNumberToName(1 + (maxWeeks * 7) + 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return file.SetColWidth(sheet, totalCol, totalCol, 10)
|
|
}
|
|
|
|
func renderMonthHeader(file *excelize.File, sheet string, startRow int, block monthBlock, monthLastCol string, monthStyle, weekStyle, dayHeaderStyle int) error {
|
|
if err := file.MergeCell(sheet, "A"+fmt.Sprint(startRow), monthLastCol+fmt.Sprint(startRow)); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellValue(sheet, "A"+fmt.Sprint(startRow), block.Start.Format("January 2006")); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellStyle(sheet, "A"+fmt.Sprint(startRow), monthLastCol+fmt.Sprint(startRow), monthStyle); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := file.MergeCell(sheet, "A"+fmt.Sprint(startRow+1), "A"+fmt.Sprint(startRow+3)); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellValue(sheet, "A"+fmt.Sprint(startRow+1), "Kandang"); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellStyle(sheet, "A"+fmt.Sprint(startRow+1), "A"+fmt.Sprint(startRow+3), dayHeaderStyle); err != nil {
|
|
return err
|
|
}
|
|
|
|
totalColIndex := 1 + (block.Weeks * 7) + 1
|
|
totalColName, err := excelize.ColumnNumberToName(totalColIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := file.MergeCell(sheet, totalColName+fmt.Sprint(startRow+1), totalColName+fmt.Sprint(startRow+3)); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellValue(sheet, totalColName+fmt.Sprint(startRow+1), "Total"); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellStyle(sheet, totalColName+fmt.Sprint(startRow+1), totalColName+fmt.Sprint(startRow+3), dayHeaderStyle); err != nil {
|
|
return err
|
|
}
|
|
|
|
weekdayNames := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
|
|
for week := 0; week < block.Weeks; week++ {
|
|
startCol := 2 + (week * 7)
|
|
endCol := startCol + 6
|
|
startColName, err := excelize.ColumnNumberToName(startCol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
endColName, err := excelize.ColumnNumberToName(endCol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := file.MergeCell(sheet, startColName+fmt.Sprint(startRow+1), endColName+fmt.Sprint(startRow+1)); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellValue(sheet, startColName+fmt.Sprint(startRow+1), fmt.Sprintf("Week %d", week+1)); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellStyle(sheet, startColName+fmt.Sprint(startRow+1), endColName+fmt.Sprint(startRow+1), weekStyle); err != nil {
|
|
return err
|
|
}
|
|
|
|
for weekday := 0; weekday < 7; weekday++ {
|
|
colIndex := startCol + weekday
|
|
colName, err := excelize.ColumnNumberToName(colIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellValue(sheet, colName+fmt.Sprint(startRow+2), weekdayNames[weekday]); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellStyle(sheet, colName+fmt.Sprint(startRow+2), colName+fmt.Sprint(startRow+2), dayHeaderStyle); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
daysInMonth := time.Date(block.Start.Year(), block.Start.Month()+1, 0, 0, 0, 0, 0, block.Start.Location()).Day()
|
|
for day := 1; day <= daysInMonth; day++ {
|
|
date := time.Date(block.Start.Year(), block.Start.Month(), day, 0, 0, 0, 0, block.Start.Location())
|
|
colIndex := dayColumnIndex(block, date)
|
|
colName, err := excelize.ColumnNumberToName(colIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellValue(sheet, colName+fmt.Sprint(startRow+3), day); err != nil {
|
|
return err
|
|
}
|
|
if err := file.SetCellStyle(sheet, colName+fmt.Sprint(startRow+3), colName+fmt.Sprint(startRow+3), dayHeaderStyle); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func groupRows(rows []Row) map[string]map[string]map[string]map[string]int {
|
|
grouped := make(map[string]map[string]map[string]map[string]int)
|
|
for _, row := range rows {
|
|
monthKey := row.ActivityDate.Format("2006-01")
|
|
if _, exists := grouped[monthKey]; !exists {
|
|
grouped[monthKey] = make(map[string]map[string]map[string]int)
|
|
}
|
|
farmName := strings.TrimSpace(row.FarmName)
|
|
if farmName == "" {
|
|
farmName = "Unknown Farm"
|
|
}
|
|
if _, exists := grouped[monthKey][farmName]; !exists {
|
|
grouped[monthKey][farmName] = make(map[string]map[string]int)
|
|
}
|
|
kandangName := strings.TrimSpace(row.KandangName)
|
|
if kandangName == "" {
|
|
kandangName = UnassignedKandangName
|
|
}
|
|
if _, exists := grouped[monthKey][farmName][kandangName]; !exists {
|
|
grouped[monthKey][farmName][kandangName] = make(map[string]int)
|
|
}
|
|
dayKey := row.ActivityDate.Format("2006-01-02")
|
|
grouped[monthKey][farmName][kandangName][dayKey] += row.Count
|
|
}
|
|
return grouped
|
|
}
|
|
|
|
func monthBlocksBetween(startDate, endDate time.Time) []monthBlock {
|
|
location := startDate.Location()
|
|
current := time.Date(startDate.Year(), startDate.Month(), 1, 0, 0, 0, 0, location)
|
|
last := time.Date(endDate.Year(), endDate.Month(), 1, 0, 0, 0, 0, location)
|
|
|
|
blocks := make([]monthBlock, 0)
|
|
for !current.After(last) {
|
|
blocks = append(blocks, monthBlock{
|
|
Start: current,
|
|
Weeks: monthWeeks(current),
|
|
})
|
|
current = current.AddDate(0, 1, 0)
|
|
}
|
|
return blocks
|
|
}
|
|
|
|
func monthWeeks(monthStart time.Time) int {
|
|
daysInMonth := time.Date(monthStart.Year(), monthStart.Month()+1, 0, 0, 0, 0, 0, monthStart.Location()).Day()
|
|
offset := mondayIndex(monthStart.Weekday())
|
|
totalSlots := offset + daysInMonth
|
|
weeks := totalSlots / 7
|
|
if totalSlots%7 != 0 {
|
|
weeks++
|
|
}
|
|
if weeks < 4 {
|
|
return 4
|
|
}
|
|
return weeks
|
|
}
|
|
|
|
func dayColumnIndex(block monthBlock, date time.Time) int {
|
|
day := date.Day()
|
|
offset := mondayIndex(block.Start.Weekday())
|
|
position := offset + (day - 1)
|
|
return 2 + position
|
|
}
|
|
|
|
func mondayIndex(weekday time.Weekday) int {
|
|
switch weekday {
|
|
case time.Sunday:
|
|
return 6
|
|
default:
|
|
return int(weekday) - 1
|
|
}
|
|
}
|
|
|
|
func sortedKeys[V any](input map[string]V) []string {
|
|
keys := make([]string, 0, len(input))
|
|
for key := range input {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func prevColumn(col string) string {
|
|
index, err := excelize.ColumnNameToNumber(col)
|
|
if err != nil || index <= 1 {
|
|
return col
|
|
}
|
|
result, err := excelize.ColumnNumberToName(index - 1)
|
|
if err != nil {
|
|
return col
|
|
}
|
|
return result
|
|
}
|