package report import ( "bytes" "context" "database/sql" "fmt" "log/slog" "reflect" "strconv" "time" "unicode/utf8" "vctp/db" "github.com/xuri/excelize/v2" ) func CreateInventoryReport(logger *slog.Logger, Database db.Database, ctx context.Context) ([]byte, error) { //var xlsx *excelize.File sheetName := "Inventory Report" var buffer bytes.Buffer var cell string logger.Debug("Querying inventory table") results, err := Database.Queries().GetReportInventory(ctx) if err != nil { logger.Error("Unable to query inventory table", "error", err) return nil, err } if len(results) == 0 { logger.Error("Empty inventory results") return nil, fmt.Errorf("Empty inventory results") } // Create excel workbook xlsx := excelize.NewFile() err = xlsx.SetSheetName("Sheet1", sheetName) if err != nil { logger.Error("Error setting sheet name", "error", err, "sheet_name", sheetName) return nil, err } // Set the document properties err = xlsx.SetDocProps(&excelize.DocProperties{ Creator: "json2excel", Created: time.Now().Format(time.RFC3339), }) if err != nil { logger.Error("Error setting document properties", "error", err, "sheet_name", sheetName) } // Use reflection to determine column headings from the first item firstItem := results[0] v := reflect.ValueOf(firstItem) typeOfItem := v.Type() // Create column headers dynamically for i := 0; i < v.NumField(); i++ { column := string(rune('A'+i)) + "1" // A1, B1, C1, etc. xlsx.SetCellValue(sheetName, column, typeOfItem.Field(i).Name) } // Set autofilter on heading row cell, _ = excelize.CoordinatesToCellName(v.NumField(), 1) filterRange := "A1:" + cell logger.Debug("Setting autofilter", "range", filterRange) // As per docs any filters applied need to be manually processed by us (eg hiding rows with blanks) err = xlsx.AutoFilter(sheetName, filterRange, nil) if err != nil { logger.Error("Error setting autofilter", "error", err) } // Bold top row headerStyle, err := xlsx.NewStyle(&excelize.Style{ Font: &excelize.Font{ Bold: true, }, }) if err != nil { logger.Error("Error generating header style", "error", err) } else { err = xlsx.SetRowStyle(sheetName, 1, 1, headerStyle) if err != nil { logger.Error("Error setting header style", "error", err) } } // Populate the Excel file with data from the Inventory table for i, item := range results { v = reflect.ValueOf(item) for j := 0; j < v.NumField(); j++ { column := string(rune('A'+j)) + strconv.Itoa(i+2) // Start from row 2 value := getFieldValue(v.Field(j)) xlsx.SetCellValue(sheetName, column, value) } } // Freeze top row err = xlsx.SetPanes(sheetName, &excelize.Panes{ Freeze: true, Split: false, XSplit: 0, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft", Selection: []excelize.Selection{ {SQRef: "A2", ActiveCell: "A2", Pane: "bottomLeft"}, }, }) if err != nil { logger.Error("Error freezing top row", "error", err) } // Set column autowidth /* err = SetColAutoWidth(xlsx, sheetName) if err != nil { fmt.Printf("Error setting auto width : '%s'\n", err) } */ // Save the Excel file into a byte buffer if err := xlsx.Write(&buffer); err != nil { return nil, err } return buffer.Bytes(), nil } func CreateUpdatesReport(logger *slog.Logger, Database db.Database, ctx context.Context) ([]byte, error) { //var xlsx *excelize.File sheetName := "Updates Report" var buffer bytes.Buffer var cell string logger.Debug("Querying updates table") results, err := Database.Queries().GetReportUpdates(ctx) if err != nil { logger.Error("Unable to query updates table", "error", err) return nil, err } if len(results) == 0 { logger.Error("Empty updates results") return nil, fmt.Errorf("Empty updates results") } // Create excel workbook xlsx := excelize.NewFile() err = xlsx.SetSheetName("Sheet1", sheetName) if err != nil { logger.Error("Error setting sheet name", "error", err, "sheet_name", sheetName) return nil, err } // Set the document properties err = xlsx.SetDocProps(&excelize.DocProperties{ Creator: "json2excel", Created: time.Now().Format(time.RFC3339), }) if err != nil { logger.Error("Error setting document properties", "error", err, "sheet_name", sheetName) } // Use reflection to determine column headings from the first item firstItem := results[0] v := reflect.ValueOf(firstItem) typeOfItem := v.Type() // Create column headers dynamically for i := 0; i < v.NumField(); i++ { column := string(rune('A'+i)) + "1" // A1, B1, C1, etc. xlsx.SetCellValue(sheetName, column, typeOfItem.Field(i).Name) } // Set autofilter on heading row cell, _ = excelize.CoordinatesToCellName(v.NumField(), 1) filterRange := "A1:" + cell logger.Debug("Setting autofilter", "range", filterRange) // As per docs any filters applied need to be manually processed by us (eg hiding rows with blanks) err = xlsx.AutoFilter(sheetName, filterRange, nil) if err != nil { logger.Error("Error setting autofilter", "error", err) } // Bold top row headerStyle, err := xlsx.NewStyle(&excelize.Style{ Font: &excelize.Font{ Bold: true, }, }) if err != nil { logger.Error("Error generating header style", "error", err) } else { err = xlsx.SetRowStyle(sheetName, 1, 1, headerStyle) if err != nil { logger.Error("Error setting header style", "error", err) } } // Populate the Excel file with data from the Inventory table for i, item := range results { v = reflect.ValueOf(item) for j := 0; j < v.NumField(); j++ { column := string(rune('A'+j)) + strconv.Itoa(i+2) // Start from row 2 value := getFieldValue(v.Field(j)) xlsx.SetCellValue(sheetName, column, value) } } // Freeze top row err = xlsx.SetPanes(sheetName, &excelize.Panes{ Freeze: true, Split: false, XSplit: 0, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft", Selection: []excelize.Selection{ {SQRef: "A2", ActiveCell: "A2", Pane: "bottomLeft"}, }, }) if err != nil { logger.Error("Error freezing top row", "error", err) } // Set column autowidth /* err = SetColAutoWidth(xlsx, sheetName) if err != nil { fmt.Printf("Error setting auto width : '%s'\n", err) } */ // Save the Excel file into a byte buffer if err := xlsx.Write(&buffer); err != nil { return nil, err } return buffer.Bytes(), nil } // Helper function to get the actual value of sql.Null types func getFieldValue(field reflect.Value) interface{} { switch field.Kind() { case reflect.Struct: // Handle sql.Null types based on their concrete type switch field.Interface().(type) { case sql.NullString: ns := field.Interface().(sql.NullString) if ns.Valid { return ns.String } return "" case sql.NullInt64: ni := field.Interface().(sql.NullInt64) if ni.Valid { return ni.Int64 } return 0 case sql.NullFloat64: nf := field.Interface().(sql.NullFloat64) if nf.Valid { return nf.Float64 } return nil case sql.NullBool: nb := field.Interface().(sql.NullBool) if nb.Valid { return nb.Bool } return false } } return field.Interface() // Return the value as-is for non-sql.Null types } // Taken from https://github.com/qax-os/excelize/issues/92#issuecomment-821578446 func SetColAutoWidth(xlsx *excelize.File, sheetName string) error { // Autofit all columns according to their text content cols, err := xlsx.GetCols(sheetName) if err != nil { return err } for idx, col := range cols { largestWidth := 0 for _, rowCell := range col { cellWidth := utf8.RuneCountInString(rowCell) + 2 // + 2 for margin if cellWidth > largestWidth { largestWidth = cellWidth } } //fmt.Printf("SetColAutoWidth calculated largest width for column index '%d' is '%d'\n", idx, largestWidth) name, err := excelize.ColumnNumberToName(idx + 1) if err != nil { return err } xlsx.SetColWidth(sheetName, name, name, float64(largestWidth)) } // No errors at this point return nil }