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  }