github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/core/utils/table/excel.go (about) 1 package table 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "path/filepath" 8 "strconv" 9 10 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 11 12 "github.com/360EntSecGroup-Skylar/excelize" 13 ) 14 15 // attributs à appliquer sur une case 16 // doit pouvoir être une clé de dictionnaire 17 type style struct { 18 color string 19 bold, italic, withLeftBorder bool 20 euros bool 21 } 22 23 type fill struct { 24 Type string `json:"type"` 25 Color [1]string `json:"color"` 26 Pattern int `json:"pattern"` 27 } 28 type font struct { 29 Bold bool `json:"bold"` 30 Italic bool `json:"italic"` 31 } 32 type border [1]struct { 33 Type string `json:"type"` 34 Color string `json:"color"` 35 Style int `json:"style"` 36 } 37 38 // enregistre le style sur excelize 39 // ne devrait être appelé qu'une seule fois par style 40 func (s style) register(b builder) (int, error) { 41 styles := map[string]interface{}{} 42 if s.color != "" { 43 styles["fill"] = fill{Type: "pattern", Color: [1]string{s.color}, Pattern: 1} 44 } 45 styles["font"] = font{Bold: s.bold, Italic: s.italic} 46 if s.withLeftBorder { 47 styles["border"] = border{{Type: "left", Color: "000000", Style: 6}} 48 } 49 if s.euros { 50 styles["number_format"] = 219 51 } 52 format, err := json.Marshal(styles) 53 if err != nil { 54 return 0, err 55 } 56 return b.NewStyle(string(format)) 57 } 58 59 func stringWidth(s string, bold bool) float64 { 60 c := 1.1 61 if bold { 62 c = 1.2 63 } 64 return 2 + c*float64(len([]rune(s))) 65 } 66 67 func findColWidth(header rd.Header, liste rd.Table) float64 { 68 maxWidth := stringWidth(header.Label, true) 69 for _, row := range liste { 70 if width := stringWidth(row.Fields.Data(header.Field).String(), false); width > maxWidth { 71 maxWidth = width 72 } 73 } 74 return maxWidth 75 } 76 77 // dirige la création du fichier 78 type builder struct { 79 *excelize.File 80 81 styles map[[2]int]style // (row, col) -> style 82 } 83 84 func newBuilder() builder { 85 return builder{File: excelize.NewFile(), styles: map[[2]int]style{}} 86 } 87 88 func (b builder) setCell(row, col int, value string) error { 89 cell, err := excelize.CoordinatesToCellName(col, row) 90 if err != nil { 91 return err 92 } 93 return b.SetCellStr("Sheet1", cell, value) 94 } 95 96 // enregistre le style pour la case donnée 97 // le style est effectivement appliquer via la méthode `applyStyles` 98 func (b builder) setStyle(color rd.Color, bold, italic, withLeftBorder, euros bool, row, col int) { 99 s := style{bold: bold, italic: italic, withLeftBorder: withLeftBorder, euros: euros} 100 if color != nil { 101 s.color = color.Hex() 102 } 103 b.styles[[2]int{row, col}] = s 104 } 105 106 func (b builder) applyStyles() error { 107 // aggrège les styles 108 m := map[style]int{} 109 for _, v := range b.styles { 110 m[v] = 0 111 } 112 // récupère l'ID excelize 113 for s := range m { 114 id, err := s.register(b) 115 if err != nil { 116 return err 117 } 118 m[s] = id 119 } 120 // parcourt le fichier pour appliquer les styles 121 for cell, style := range b.styles { 122 row, col := cell[0], cell[1] 123 cellName, err := excelize.CoordinatesToCellName(col, row) 124 if err != nil { 125 return err 126 } 127 if err = b.SetCellStyle("Sheet1", cellName, cellName, m[style]); err != nil { 128 return err 129 } 130 } 131 return nil 132 } 133 134 type Total struct { 135 label string 136 value string 137 } 138 139 func (b builder) drawItems(headers []rd.Header, liste rd.Table, startingRow int, showLineNumbers, showColors bool, colLeftBorder int) error { 140 var colOffset int // pour les numéros de lignes 141 if showLineNumbers { 142 colOffset = 1 143 } 144 145 for row, data := range liste { 146 currentRow := row + startingRow 147 if showLineNumbers { 148 if err := b.setCell(currentRow, 1, strconv.Itoa(row+1)); err != nil { 149 return err 150 } 151 } 152 for col, field := range headers { 153 currentCol := col + 1 + colOffset 154 var color rd.Color 155 if showColors { 156 color = data.BackgroundColor(field.Field) 157 } 158 159 cellData := data.Fields.Data(field.Field) 160 _, isEuros := cellData.(rd.Euros) // applique le format euros 161 b.setStyle(color, data.Bolds[field.Field], false, 162 colLeftBorder == currentCol, isEuros, currentRow, currentCol) 163 164 if err := b.setCell(currentRow, currentCol, cellData.String()); err != nil { 165 return err 166 } 167 } 168 } 169 return nil 170 } 171 172 // if `colLeftBorder == -1`, aucune ligne verticale supplémentaire n'est tracée 173 func renderListe(headers []rd.Header, liste rd.Table, totals []Total, showLineNumbers, showColors bool, colLeftBorder int) (*excelize.File, error) { 174 b := newBuilder() 175 var colOffset int // pour les numéros de lignes 176 if showLineNumbers { 177 colOffset = 1 178 } 179 180 // headers 181 for col, field := range headers { 182 if err := b.setCell(1, col+1+colOffset, field.Label); err != nil { 183 return nil, err 184 } 185 b.setStyle(nil, true, false, false, false, 1, col+1+colOffset) 186 colLetter, err := excelize.ColumnNumberToName(col + 1 + colOffset) 187 if err != nil { 188 return nil, err 189 } 190 colWidth := findColWidth(field, liste) 191 if err := b.SetColWidth("Sheet1", colLetter, colLetter, colWidth); err != nil { 192 return nil, err 193 } 194 } 195 196 // datas 197 if err := b.drawItems(headers, liste, 2, showLineNumbers, showColors, colLeftBorder); err != nil { 198 return nil, err 199 } 200 201 // pour une ligne de totaux 202 totalRow := len(liste) + 3 203 for index, total := range totals { 204 if err := b.setCell(totalRow, 2*index+1+colOffset, total.label); err != nil { 205 return nil, err 206 } 207 b.setStyle(nil, false, true, false, false, totalRow, 2*index+1+colOffset) 208 if err := b.setCell(totalRow, 2*index+2+colOffset, total.value); err != nil { 209 return nil, err 210 } 211 b.setStyle(nil, true, false, false, false, totalRow, 2*index+2+colOffset) 212 } 213 214 // applique les styles 215 if err := b.applyStyles(); err != nil { 216 return nil, err 217 } 218 219 return b.File, nil 220 } 221 222 func save(f *excelize.File, directory, filename string) (string, error) { 223 path, err := filepath.Abs(filepath.Join(directory, filename)) 224 if err != nil { 225 return "", err 226 } 227 err = f.SaveAs(path) 228 if err != nil { 229 return "", err 230 } 231 return path, nil 232 } 233 234 // ExportExcel enregistre un fichier excel basique. 235 func ExportExcel(headers []rd.Header, liste rd.Table, directory string) (string, error) { 236 f, err := renderListe(headers, liste, nil, false, true, -1) 237 if err != nil { 238 return "", err 239 } 240 return save(f, directory, "export.xlsx") 241 } 242 243 func GenereListeAides(headers []rd.Header, liste rd.Table, directory string) (string, error) { 244 f, err := renderListe(headers, liste, nil, false, true, -1) 245 if err != nil { 246 return "", err 247 } 248 return save(f, directory, "liste_aides.xlsx") 249 } 250 251 func GenereListeEquipe(headers []rd.Header, liste rd.Table, showColors bool) (*bytes.Buffer, error) { 252 f, err := renderListe(headers, liste, nil, false, showColors, -1) 253 if err != nil { 254 return nil, err 255 } 256 return f.WriteToBuffer() 257 } 258 259 func EnregistreListePaiements(headers []rd.Header, liste rd.Table, total rd.Euros, directory string) (string, error) { 260 totals := []Total{ 261 {"Total :", total.String()}, 262 } 263 f, err := renderListe(headers, liste, totals, true, true, -1) 264 if err != nil { 265 return "", err 266 } 267 return save(f, directory, "liste_paiements.xlsx") 268 } 269 270 // EnregistreSuiviFinancierCamp enregistre un tableau des participants avec l'état de leur facture 271 func EnregistreSuiviFinancierCamp(headers []rd.Header, liste rd.Table, totalDemande, 272 totalAides rd.Euros, nomCamp, directory string) (string, error) { 273 totals := []Total{ 274 {"Total demandé:", totalDemande.String()}, 275 {"Total aides:", totalAides.String()}, 276 } 277 f, err := renderListe(headers, liste, totals, false, true, -1) 278 if err != nil { 279 return "", err 280 } 281 return save(f, directory, fmt.Sprintf("suivi_financier_%s.xlsx", nomCamp)) 282 } 283 284 // GenereSuiviFinancierCamp renvoie un tableau des participants avec l'état de leur facture 285 func GenereSuiviFinancierCamp(headers []rd.Header, liste rd.Table, totalDemande, 286 totalAides rd.Euros) (*bytes.Buffer, error) { 287 totals := []Total{ 288 {"Total demandé:", totalDemande.String()}, 289 {"Total aides:", totalAides.String()}, 290 } 291 f, err := renderListe(headers, liste, totals, false, true, -1) 292 if err != nil { 293 return nil, err 294 } 295 return f.WriteToBuffer() 296 } 297 298 func genereListeParticipants(headersParticipant, headersResponsable []rd.Header, inscrits, attente rd.Table, showColors bool) (*excelize.File, error) { 299 headers := append(headersParticipant, headersResponsable...) 300 colLine := len(headersParticipant) + 1 301 302 liste := append(append(inscrits, rd.Item{}, rd.Item{}), attente...) // saut de lignes entre inscrits et attente 303 304 f, err := renderListe(headers, liste, nil, false, showColors, colLine) 305 if err != nil { 306 return nil, err 307 } 308 return f, nil 309 } 310 311 func EnregistreListeParticipants(headersParticipant, headersResponsable []rd.Header, inscrits, attente rd.Table, showColors bool, 312 nomCamp, directory string) (filepath string, err error) { 313 f, err := genereListeParticipants(headersParticipant, headersResponsable, inscrits, attente, showColors) 314 if err != nil { 315 return "", err 316 } 317 return save(f, directory, fmt.Sprintf("liste_%s.xlsx", nomCamp)) 318 } 319 320 func GenereListeParticipants(headersParticipant, headersResponsable []rd.Header, inscrits, attente rd.Table, showColors bool) (*bytes.Buffer, error) { 321 f, err := genereListeParticipants(headersParticipant, headersResponsable, inscrits, attente, showColors) 322 if err != nil { 323 return nil, err 324 } 325 return f.WriteToBuffer() 326 } 327 328 func EnregistreBilanCamps(headers []rd.Header, camps rd.Table, directory string) (string, error) { 329 f, err := renderListe(headers, camps, nil, false, true, -1) 330 if err != nil { 331 return "", err 332 } 333 return save(f, directory, "bilan_camps.xlsx") 334 } 335 336 func EnregistreDons(headers []rd.Header, dons rd.Table, total rd.Euros, directory string) (string, error) { 337 totals := []Total{ 338 {label: "Total", value: total.String()}, 339 } 340 f, err := renderListe(headers, dons, totals, true, false, -1) 341 if err != nil { 342 return "", err 343 } 344 return save(f, directory, "dons.xlsx") 345 }