feat: excel export for primitive fields
This commit is contained in:
3
go.mod
3
go.mod
@@ -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
2
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.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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
35
internal/extras/excel/exporter.go
Normal file
35
internal/extras/excel/exporter.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
package excel
|
package excel
|
||||||
|
|
||||||
const (
|
const excelNameTag string = "excel"
|
||||||
CellTypeTag = "cellType"
|
const timestampTag string = "datetime"
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
TimestampTag = "timestamp"
|
|
||||||
DurationTag = "duration"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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;"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user