From 1ed106f167dfe81f342f78db7b0ee8b72dea1943 Mon Sep 17 00:00:00 2001 From: gogacoder Date: Tue, 11 Mar 2025 16:10:18 +0700 Subject: [PATCH] feat: excel export for primitive fields --- go.mod | 3 +- go.sum | 2 + internal/extras/excel/export.go | 123 +++++++++++++++++++++--------- internal/extras/excel/exporter.go | 35 +++++++++ internal/extras/excel/tags.go | 10 +-- internal/models/models.go | 8 +- internal/services/comment.go | 3 + internal/services/post.go | 9 ++- internal/services/posttype.go | 11 ++- 9 files changed, 149 insertions(+), 55 deletions(-) create mode 100644 internal/extras/excel/exporter.go diff --git a/go.mod b/go.mod index 84d4e75..07fa07b 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ module app -go 1.22.4 +go 1.22.12 toolchain go1.23.4 require ( + github.com/kuzgoga/fogg v0.1.2 github.com/wailsapp/wails/v3 v3.0.0-alpha.9 github.com/xuri/excelize/v2 v2.9.0 gorm.io/driver/sqlite v1.5.7 diff --git a/go.sum b/go.sum index 693b030..6177f6b 100644 --- a/go.sum +++ b/go.sum @@ -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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI= diff --git a/internal/extras/excel/export.go b/internal/extras/excel/export.go index d6f8c49..33c9e01 100644 --- a/internal/extras/excel/export.go +++ b/internal/extras/excel/export.go @@ -2,7 +2,9 @@ package excel import ( "app/internal/dialogs" + "errors" "fmt" + "github.com/kuzgoga/fogg" "github.com/xuri/excelize/v2" "log/slog" "reflect" @@ -13,10 +15,10 @@ import ( type TableHeaders struct { Headers []string - IgnoredFieldsIndices []int + IgnoredFieldsIndexes []int } -func isPrimitive(valueType reflect.Type) bool { +func isPrimitiveType(valueType reflect.Type) bool { switch valueType.Kind() { case reflect.Bool, 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() defer func() { 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 + } + } + + if err := DeleteDefaultSheet(file); err != nil { return err } - if err := file.DeleteSheet("Sheet1"); err != nil { + + 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 } @@ -59,33 +91,38 @@ func ExportEntityToSpreadsheet[T any](filename, sheetName string, entity T, prov for i, item := range items { structValue := reflect.ValueOf(item).Elem() + columnOffset := 0 for j := 0; j < structValue.NumField(); j++ { - if slices.Contains(headers.IgnoredFieldsIndices, j) { + if slices.Contains(headers.IgnoredFieldsIndexes, j) { + columnOffset-- continue } + 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() - cellName, err := GetCellNameByIndices(j, i+1) + cellName, err := GetCellNameByIndices(j+columnOffset, i+1) if err != nil { return err } - cellType := structValue.Type().Field(j).Tag.Get(CellTypeTag) + datatype := tag.GetTag("ui").GetParamOr("datatype", "") - var cellValue any - - switch cellType { - case TimestampTag: - cellValue = time.Unix(field.Int(), 0) - case DurationTag: - cellValue = time.Duration(field.Int()) - default: - cellValue = fieldValue + if datatype == timestampTag { + err = file.SetCellValue(sheetName, cellName, time.Unix(fieldValue.(int64), 0)) + } else { + err = file.SetCellValue(sheetName, cellName, fieldValue) } - slog.Debug("Field %s value: %v, %s\n", cellName, cellValue, cellType) - err = file.SetCellValue(sheetName, cellName, cellValue) + slog.Info(fmt.Sprintf("Field %s value: %v, %s\n", cellName, fieldValue, datatype)) + if err != nil { 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 } @@ -124,23 +157,43 @@ func GetCellNameByIndices(column int, row int) (string, error) { return cellName, nil } -func ExportHeaders(entity any) TableHeaders { +func ExportHeaders(entity any) (TableHeaders, error) { headers := TableHeaders{} v := reflect.TypeOf(entity) for i := range v.NumField() { - field := v.Field(i) - displayName := field.Tag.Get("displayName") - if displayName != "" { - headers.Headers = append(headers.Headers, displayName) + tag, err := fogg.Parse(string(v.Field(i).Tag)) + if err != nil { + return headers, errors.New(fmt.Sprintf("Error occured while tag parsing `%s`: %s", err, string(v.Field(i).Tag))) + } + + 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 { - 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) { - headers := ExportHeaders(entity) + headers, err := ExportHeaders(entity) + if err != nil { + return headers, err + } + for i, header := range headers.Headers { cellName, err := GetHeaderCellNameByIndex(i) if err != nil { @@ -152,17 +205,19 @@ func WriteHeaders(sheetName string, entity any, file *excelize.File) (TableHeade return headers, err } } - err := ApplyStyleHeaders(file, sheetName, headers) + + err = ApplyStyleHeaders(file, sheetName, headers) if err != nil { return headers, err } + return headers, nil } func GetStyleId(f *excelize.File, style *excelize.Style) (int, error) { styleId, err := f.NewStyle(style) if err != nil { - return 0, fmt.Errorf("ошибка при создании стиля: %w", err) + return 0, fmt.Errorf("error occured while creating a style: %w", err) } return styleId, nil diff --git a/internal/extras/excel/exporter.go b/internal/extras/excel/exporter.go new file mode 100644 index 0000000..1fe9987 --- /dev/null +++ b/internal/extras/excel/exporter.go @@ -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 + } +} diff --git a/internal/extras/excel/tags.go b/internal/extras/excel/tags.go index 736b219..1cceedb 100644 --- a/internal/extras/excel/tags.go +++ b/internal/extras/excel/tags.go @@ -1,10 +1,4 @@ package excel -const ( - CellTypeTag = "cellType" -) - -const ( - TimestampTag = "timestamp" - DurationTag = "duration" -) +const excelNameTag string = "excel" +const timestampTag string = "datetime" diff --git a/internal/models/models.go b/internal/models/models.go index 1d804cf..6f66c72 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -10,14 +10,14 @@ type PostType struct { } type Post struct { - Id uint `gorm:"primaryKey" ui:"hidden"` + Id uint `gorm:"primaryKey" ui:"hidden;label:\"Номер поста\""` Text string `ui:"label:Текст"` 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;"` Author Author `ui:"label:Автор; field:Name;"` - PostTypeId uint `ui:"hidden"` - PostType PostType `ui:"label:Тип; field:Name;" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` + PostTypeId uint `ui:"hidden; excel:Номер типа поста;"` + 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;"` } diff --git a/internal/services/comment.go b/internal/services/comment.go index a2a7573..5039867 100644 --- a/internal/services/comment.go +++ b/internal/services/comment.go @@ -29,6 +29,7 @@ func (service *CommentService) GetAll() ([]*Comment, error) { comments, err := dal.Comment.Preload(field.Associations).Find() return comments, err } + func (service *CommentService) GetById(id uint) (*Comment, error) { item, err := dal.Comment.Preload(field.Associations).Where(dal.Comment.Id.Eq(id)).First() if err != nil { @@ -40,6 +41,7 @@ func (service *CommentService) GetById(id uint) (*Comment, error) { } return item, nil } + func (service *CommentService) Update(item Comment) (Comment, error) { ReplaceEmptySlicesWithNil(&item) err := dal.Comment.Preload(field.Associations).Save(&item) @@ -55,6 +57,7 @@ func (service *CommentService) Update(item Comment) (Comment, error) { return item, err } + func (service *CommentService) Delete(id uint) error { _, err := dal.Comment.Unscoped().Where(dal.Comment.Id.Eq(id)).Delete() return err diff --git a/internal/services/post.go b/internal/services/post.go index 9820779..a90936d 100644 --- a/internal/services/post.go +++ b/internal/services/post.go @@ -4,7 +4,7 @@ import ( "app/internal/dal" "app/internal/database" "app/internal/dialogs" - excel "app/internal/extras/excel" + "app/internal/extras/excel" "app/internal/models" "errors" "fmt" @@ -64,7 +64,12 @@ func (service *PostService) Count() (int64, error) { return amount, err } 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 { dialogs.ErrorDialog("Ошибка экспорта", fmt.Sprintf("Ошибка при экспорте данных: %s", err)) } diff --git a/internal/services/posttype.go b/internal/services/posttype.go index 5b196c6..6054de6 100644 --- a/internal/services/posttype.go +++ b/internal/services/posttype.go @@ -7,12 +7,9 @@ import ( "app/internal/extras/excel" "app/internal/models" "errors" - "os/signal" - "strconv" - "syscall" - "gorm.io/gen/field" "gorm.io/gorm" + "strconv" ) type PostTypeService struct { @@ -66,7 +63,6 @@ func (service *PostTypeService) Count() (int64, error) { } func (service *PostTypeService) ImportFromExcel() error { - signal.Ignore(syscall.SIGSEGV) filepath, err := dialogs.OpenFileDialog("Импорт данных") if err != nil { return err @@ -79,10 +75,13 @@ func (service *PostTypeService) ImportFromExcel() error { return err } - service.Create(PostType{ + _, err = service.Create(PostType{ Id: uint(id), Name: row[1], }) + if err != nil { + return err + } return nil }, })