feat: excel export for primitive fields

This commit is contained in:
2025-03-11 16:10:18 +07:00
parent 5c372e8029
commit 1ed106f167
9 changed files with 149 additions and 55 deletions

3
go.mod
View File

@@ -1,10 +1,11 @@
module app module app
go 1.22.4 go 1.22.12
toolchain go1.23.4 toolchain go1.23.4
require ( require (
github.com/kuzgoga/fogg v0.1.2
github.com/wailsapp/wails/v3 v3.0.0-alpha.9 github.com/wailsapp/wails/v3 v3.0.0-alpha.9
github.com/xuri/excelize/v2 v2.9.0 github.com/xuri/excelize/v2 v2.9.0
gorm.io/driver/sqlite v1.5.7 gorm.io/driver/sqlite v1.5.7

2
go.sum
View File

@@ -82,6 +82,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kuzgoga/fogg v0.1.2 h1:zXOZEaSFNiC3xU/r10UYAQ9CpR2A2kx9F1bFC8O62AQ=
github.com/kuzgoga/fogg v0.1.2/go.mod h1:x0cKa6kIaweLKtzMWXw0xZZnl2PrLDpMmi+yL3xEIEg=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI= github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=

View File

@@ -2,7 +2,9 @@ package excel
import ( import (
"app/internal/dialogs" "app/internal/dialogs"
"errors"
"fmt" "fmt"
"github.com/kuzgoga/fogg"
"github.com/xuri/excelize/v2" "github.com/xuri/excelize/v2"
"log/slog" "log/slog"
"reflect" "reflect"
@@ -13,10 +15,10 @@ import (
type TableHeaders struct { type TableHeaders struct {
Headers []string Headers []string
IgnoredFieldsIndices []int IgnoredFieldsIndexes []int
} }
func isPrimitive(valueType reflect.Type) bool { func isPrimitiveType(valueType reflect.Type) bool {
switch valueType.Kind() { switch valueType.Kind() {
case reflect.Bool, case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
@@ -29,7 +31,20 @@ func isPrimitive(valueType reflect.Type) bool {
} }
} }
func ExportEntityToSpreadsheet[T any](filename, sheetName string, entity T, provider func() ([]*T, error)) error { func DeleteDefaultSheet(file *excelize.File) error {
sheetId, err := file.GetSheetIndex("Sheet1")
if err != nil {
return err
}
if sheetId != -1 {
if err := file.DeleteSheet("Sheet1"); err != nil {
return err
}
}
return nil
}
func ExportEntitiesToSpreadsheet(filename string, exporters ...ExporterInterface) error {
file := excelize.NewFile() file := excelize.NewFile()
defer func() { defer func() {
if err := file.Close(); err != nil { if err := file.Close(); err != nil {
@@ -37,10 +52,27 @@ func ExportEntityToSpreadsheet[T any](filename, sheetName string, entity T, prov
} }
}() }()
if _, err := file.NewSheet(sheetName); err != nil { for _, exporter := range exporters {
err := ExportEntityToSpreadsheet(file, exporter.GetSheetName(), exporter.GetEntity(), exporter.GetProvider())
if err != nil {
return err return err
} }
if err := file.DeleteSheet("Sheet1"); err != nil { }
if err := DeleteDefaultSheet(file); err != nil {
return err
}
err := WriteData(file, filename)
if err != nil {
return err
}
return nil
}
func ExportEntityToSpreadsheet[T any](file *excelize.File, sheetName string, entity T, provider func() ([]any, error)) error {
_, err := file.NewSheet(sheetName)
if err != nil {
return err return err
} }
@@ -59,33 +91,38 @@ func ExportEntityToSpreadsheet[T any](filename, sheetName string, entity T, prov
for i, item := range items { for i, item := range items {
structValue := reflect.ValueOf(item).Elem() structValue := reflect.ValueOf(item).Elem()
columnOffset := 0
for j := 0; j < structValue.NumField(); j++ { for j := 0; j < structValue.NumField(); j++ {
if slices.Contains(headers.IgnoredFieldsIndices, j) { if slices.Contains(headers.IgnoredFieldsIndexes, j) {
columnOffset--
continue continue
} }
field := structValue.Field(j) field := structValue.Field(j)
if isPrimitive(field.Type()) { tagLiteral := string(structValue.Type().Field(j).Tag)
tag, err := fogg.Parse(tagLiteral)
if err != nil {
return err
}
if isPrimitiveType(field.Type()) {
fieldValue := field.Interface() fieldValue := field.Interface()
cellName, err := GetCellNameByIndices(j, i+1) cellName, err := GetCellNameByIndices(j+columnOffset, i+1)
if err != nil { if err != nil {
return err return err
} }
cellType := structValue.Type().Field(j).Tag.Get(CellTypeTag) datatype := tag.GetTag("ui").GetParamOr("datatype", "")
var cellValue any if datatype == timestampTag {
err = file.SetCellValue(sheetName, cellName, time.Unix(fieldValue.(int64), 0))
switch cellType { } else {
case TimestampTag: err = file.SetCellValue(sheetName, cellName, fieldValue)
cellValue = time.Unix(field.Int(), 0)
case DurationTag:
cellValue = time.Duration(field.Int())
default:
cellValue = fieldValue
} }
slog.Debug("Field %s value: %v, %s\n", cellName, cellValue, cellType) slog.Info(fmt.Sprintf("Field %s value: %v, %s\n", cellName, fieldValue, datatype))
err = file.SetCellValue(sheetName, cellName, cellValue)
if err != nil { if err != nil {
return err return err
} }
@@ -93,10 +130,6 @@ func ExportEntityToSpreadsheet[T any](filename, sheetName string, entity T, prov
} }
} }
if err := WriteData(file, filename); err != nil {
return err
}
return nil return nil
} }
@@ -124,23 +157,43 @@ func GetCellNameByIndices(column int, row int) (string, error) {
return cellName, nil return cellName, nil
} }
func ExportHeaders(entity any) TableHeaders { func ExportHeaders(entity any) (TableHeaders, error) {
headers := TableHeaders{} headers := TableHeaders{}
v := reflect.TypeOf(entity) v := reflect.TypeOf(entity)
for i := range v.NumField() { for i := range v.NumField() {
field := v.Field(i) tag, err := fogg.Parse(string(v.Field(i).Tag))
displayName := field.Tag.Get("displayName") if err != nil {
if displayName != "" { return headers, errors.New(fmt.Sprintf("Error occured while tag parsing `%s`: %s", err, string(v.Field(i).Tag)))
headers.Headers = append(headers.Headers, displayName) }
uiTag := tag.GetTag("ui")
if uiTag == nil {
headers.IgnoredFieldsIndexes = append(headers.IgnoredFieldsIndexes, i)
continue
}
if !isPrimitiveType(v.Field(i).Type) {
headers.IgnoredFieldsIndexes = append(headers.IgnoredFieldsIndexes, i)
continue
}
label := uiTag.GetParamOr("label", uiTag.GetParamOr(excelNameTag, ""))
if label != "" {
headers.Headers = append(headers.Headers, label)
} else { } else {
headers.IgnoredFieldsIndices = append(headers.IgnoredFieldsIndices, i) headers.IgnoredFieldsIndexes = append(headers.IgnoredFieldsIndexes, i)
} }
} }
return headers return headers, nil
} }
func WriteHeaders(sheetName string, entity any, file *excelize.File) (TableHeaders, error) { func WriteHeaders(sheetName string, entity any, file *excelize.File) (TableHeaders, error) {
headers := ExportHeaders(entity) headers, err := ExportHeaders(entity)
if err != nil {
return headers, err
}
for i, header := range headers.Headers { for i, header := range headers.Headers {
cellName, err := GetHeaderCellNameByIndex(i) cellName, err := GetHeaderCellNameByIndex(i)
if err != nil { if err != nil {
@@ -152,17 +205,19 @@ func WriteHeaders(sheetName string, entity any, file *excelize.File) (TableHeade
return headers, err return headers, err
} }
} }
err := ApplyStyleHeaders(file, sheetName, headers)
err = ApplyStyleHeaders(file, sheetName, headers)
if err != nil { if err != nil {
return headers, err return headers, err
} }
return headers, nil return headers, nil
} }
func GetStyleId(f *excelize.File, style *excelize.Style) (int, error) { func GetStyleId(f *excelize.File, style *excelize.Style) (int, error) {
styleId, err := f.NewStyle(style) styleId, err := f.NewStyle(style)
if err != nil { if err != nil {
return 0, fmt.Errorf("ошибка при создании стиля: %w", err) return 0, fmt.Errorf("error occured while creating a style: %w", err)
} }
return styleId, nil return styleId, nil

View File

@@ -0,0 +1,35 @@
package excel
type ExporterInterface interface {
GetSheetName() string
GetEntity() any
GetProvider() func() ([]any, error)
}
type Exporter[T any] struct {
SheetName string
Entity T
Provider func() ([]*T, error)
}
func (e Exporter[T]) GetSheetName() string {
return e.SheetName
}
func (e Exporter[T]) GetEntity() any {
return e.Entity
}
func (e Exporter[T]) GetProvider() func() ([]any, error) {
return func() ([]any, error) {
entities, err := e.Provider()
if err != nil {
return nil, err
}
result := make([]any, len(entities))
for i, entity := range entities {
result[i] = entity
}
return result, nil
}
}

View File

@@ -1,10 +1,4 @@
package excel package excel
const ( const excelNameTag string = "excel"
CellTypeTag = "cellType" const timestampTag string = "datetime"
)
const (
TimestampTag = "timestamp"
DurationTag = "duration"
)

View File

@@ -10,14 +10,14 @@ type PostType struct {
} }
type Post struct { type Post struct {
Id uint `gorm:"primaryKey" ui:"hidden"` Id uint `gorm:"primaryKey" ui:"hidden;label:\"Номер поста\""`
Text string `ui:"label:Текст"` Text string `ui:"label:Текст"`
Deadline int64 `ui:"label:Дедлайн;datatype:datetime;"` Deadline int64 `ui:"label:Дедлайн;datatype:datetime;"`
CreatedAt int64 `gorm:"autoCreateTime" ui:"label:Время создания;readonly;datatype:datetime;"` CreatedAt int64 `gorm:"autoCreateTime" ui:"label:Время создания;readonly;datatype:datetime;"`
AuthorId uint `ui:"hidden" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` AuthorId uint `ui:"hidden" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
Author Author `ui:"label:Автор; field:Name;"` Author Author `ui:"label:Автор; field:Name;"`
PostTypeId uint `ui:"hidden"` PostTypeId uint `ui:"hidden; excel:Номер типа поста;"`
PostType PostType `ui:"label:Тип; field:Name;" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` PostType PostType `ui:"label:Тип поста; field:Name;" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
Comments []Comment `ui:"label:Комментарии; field:Text;" gorm:"many2many:comments_post;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` Comments []Comment `ui:"label:Комментарии; field:Text;" gorm:"many2many:comments_post;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
} }

View File

@@ -29,6 +29,7 @@ func (service *CommentService) GetAll() ([]*Comment, error) {
comments, err := dal.Comment.Preload(field.Associations).Find() comments, err := dal.Comment.Preload(field.Associations).Find()
return comments, err return comments, err
} }
func (service *CommentService) GetById(id uint) (*Comment, error) { func (service *CommentService) GetById(id uint) (*Comment, error) {
item, err := dal.Comment.Preload(field.Associations).Where(dal.Comment.Id.Eq(id)).First() item, err := dal.Comment.Preload(field.Associations).Where(dal.Comment.Id.Eq(id)).First()
if err != nil { if err != nil {
@@ -40,6 +41,7 @@ func (service *CommentService) GetById(id uint) (*Comment, error) {
} }
return item, nil return item, nil
} }
func (service *CommentService) Update(item Comment) (Comment, error) { func (service *CommentService) Update(item Comment) (Comment, error) {
ReplaceEmptySlicesWithNil(&item) ReplaceEmptySlicesWithNil(&item)
err := dal.Comment.Preload(field.Associations).Save(&item) err := dal.Comment.Preload(field.Associations).Save(&item)
@@ -55,6 +57,7 @@ func (service *CommentService) Update(item Comment) (Comment, error) {
return item, err return item, err
} }
func (service *CommentService) Delete(id uint) error { func (service *CommentService) Delete(id uint) error {
_, err := dal.Comment.Unscoped().Where(dal.Comment.Id.Eq(id)).Delete() _, err := dal.Comment.Unscoped().Where(dal.Comment.Id.Eq(id)).Delete()
return err return err

View File

@@ -4,7 +4,7 @@ import (
"app/internal/dal" "app/internal/dal"
"app/internal/database" "app/internal/database"
"app/internal/dialogs" "app/internal/dialogs"
excel "app/internal/extras/excel" "app/internal/extras/excel"
"app/internal/models" "app/internal/models"
"errors" "errors"
"fmt" "fmt"
@@ -64,7 +64,12 @@ func (service *PostService) Count() (int64, error) {
return amount, err return amount, err
} }
func (service *PostService) ExportToExcel() { func (service *PostService) ExportToExcel() {
err := excel.ExportEntityToSpreadsheet("report.xlsx", "Посты", Post{}, service.GetAll) exporter := excel.Exporter[Post]{
SheetName: "Посты",
Entity: Post{},
Provider: service.GetAll,
}
err := excel.ExportEntitiesToSpreadsheet("report.xlsx", exporter)
if err != nil { if err != nil {
dialogs.ErrorDialog("Ошибка экспорта", fmt.Sprintf("Ошибка при экспорте данных: %s", err)) dialogs.ErrorDialog("Ошибка экспорта", fmt.Sprintf("Ошибка при экспорте данных: %s", err))
} }

View File

@@ -7,12 +7,9 @@ import (
"app/internal/extras/excel" "app/internal/extras/excel"
"app/internal/models" "app/internal/models"
"errors" "errors"
"os/signal"
"strconv"
"syscall"
"gorm.io/gen/field" "gorm.io/gen/field"
"gorm.io/gorm" "gorm.io/gorm"
"strconv"
) )
type PostTypeService struct { type PostTypeService struct {
@@ -66,7 +63,6 @@ func (service *PostTypeService) Count() (int64, error) {
} }
func (service *PostTypeService) ImportFromExcel() error { func (service *PostTypeService) ImportFromExcel() error {
signal.Ignore(syscall.SIGSEGV)
filepath, err := dialogs.OpenFileDialog("Импорт данных") filepath, err := dialogs.OpenFileDialog("Импорт данных")
if err != nil { if err != nil {
return err return err
@@ -79,10 +75,13 @@ func (service *PostTypeService) ImportFromExcel() error {
return err return err
} }
service.Create(PostType{ _, err = service.Create(PostType{
Id: uint(id), Id: uint(id),
Name: row[1], Name: row[1],
}) })
if err != nil {
return err
}
return nil return nil
}, },
}) })