Import initail de calcul-astreintes v0.9.4 pour passage en paycheck 1.0
This commit is contained in:
BIN
internal/pdf/assets/paychek-logo.png
Normal file
BIN
internal/pdf/assets/paychek-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 341 KiB |
BIN
internal/pdf/fonts/DejaVuSans-Bold.ttf
Normal file
BIN
internal/pdf/fonts/DejaVuSans-Bold.ttf
Normal file
Binary file not shown.
BIN
internal/pdf/fonts/DejaVuSans.ttf
Normal file
BIN
internal/pdf/fonts/DejaVuSans.ttf
Normal file
Binary file not shown.
237
internal/pdf/pdf.go
Normal file
237
internal/pdf/pdf.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/phpdave11/gofpdf"
|
||||
|
||||
"git.dumerain.org/alban/calcul-astreintes/internal/models"
|
||||
)
|
||||
|
||||
//go:embed fonts/DejaVuSans.ttf fonts/DejaVuSans-Bold.ttf assets/paychek-logo.png
|
||||
var assetFS embed.FS
|
||||
|
||||
func BuildPDF(
|
||||
meta models.ExportPDFMeta,
|
||||
profileLabel string,
|
||||
req models.CalculateRequest,
|
||||
res models.CalculateResponse,
|
||||
) ([]byte, error) {
|
||||
|
||||
regularFont, err := assetFS.ReadFile("fonts/DejaVuSans.ttf")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boldFont, err := assetFS.ReadFile("fonts/DejaVuSans-Bold.ttf")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logoBytes, logoErr := assetFS.ReadFile("assets/paychek-logo.png")
|
||||
hasLogo := logoErr == nil && len(logoBytes) > 0
|
||||
|
||||
pdf := gofpdf.New("P", "mm", "A4", "")
|
||||
if hasLogo {
|
||||
pdf.RegisterImageOptionsReader(
|
||||
"paychek-logo",
|
||||
gofpdf.ImageOptions{ImageType: "PNG"},
|
||||
bytes.NewReader(logoBytes),
|
||||
)
|
||||
}
|
||||
// ✅ IMPORTANT : on désactive l'auto page-break pour éviter une 2e page
|
||||
pdf.SetMargins(12, 14, 12)
|
||||
pdf.SetAutoPageBreak(false, 0)
|
||||
pdf.AddPage()
|
||||
|
||||
pdf.AddUTF8FontFromBytes("DejaVu", "", regularFont)
|
||||
pdf.AddUTF8FontFromBytes("DejaVu", "B", boldFont)
|
||||
|
||||
// A4 en mm
|
||||
pageW := 210.0
|
||||
contentW := 170.0
|
||||
left := (pageW - contentW) / 2.0
|
||||
|
||||
created := time.Now().Format("02/01/2006 15:04")
|
||||
|
||||
fullName := strings.TrimSpace(strings.Join([]string{
|
||||
meta.Nom,
|
||||
meta.Prenom,
|
||||
}, " "))
|
||||
if fullName == "" {
|
||||
fullName = "(nom non renseigné)"
|
||||
}
|
||||
|
||||
// ---------------- TITRE ----------------
|
||||
titleY := pdf.GetY()
|
||||
logoSize := 10.0 // mm
|
||||
gap := 2.5 // espace texte ↔ logo
|
||||
|
||||
pdf.SetFont("DejaVu", "B", 18)
|
||||
|
||||
// Largeur du texte
|
||||
title := "Paychek"
|
||||
textW := pdf.GetStringWidth(title)
|
||||
|
||||
// Largeur totale du bloc (texte + logo)
|
||||
blockW := textW
|
||||
if hasLogo {
|
||||
blockW += gap + logoSize
|
||||
}
|
||||
|
||||
// Position X pour centrer le bloc
|
||||
startX := left + (contentW-blockW)/2
|
||||
|
||||
// Texte
|
||||
pdf.SetXY(startX, titleY)
|
||||
pdf.CellFormat(textW, 10, title, "", 0, "L", false, 0, "")
|
||||
|
||||
// Logo à droite du texte
|
||||
if hasLogo {
|
||||
pdf.ImageOptions(
|
||||
"paychek-logo",
|
||||
startX+textW+gap,
|
||||
titleY,
|
||||
logoSize,
|
||||
logoSize,
|
||||
false,
|
||||
gofpdf.ImageOptions{ImageType: "PNG"},
|
||||
0,
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
pdf.Ln(10)
|
||||
|
||||
pdf.SetFont("DejaVu", "", 11)
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(contentW, 6, "Give me my fuc** money", "", 1, "C", false, 0, "")
|
||||
|
||||
// ✅ Séparation franche sous le titre
|
||||
pdf.Ln(6)
|
||||
|
||||
// ✅ CENTRAGE VERTICAL : on place le "bloc principal" à une hauteur stable
|
||||
// (A4, contenu fixe : 2 colonnes + tableau 5 lignes + légende)
|
||||
// -> évite un rendu collé en haut.
|
||||
startY := 48.0
|
||||
pdf.SetY(startY)
|
||||
|
||||
// ---------------- IDENTITÉ / VALEURS ----------------
|
||||
pdf.SetFont("DejaVu", "B", 12)
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(contentW/2-5, 6, "Identité", "", 0, "L", false, 0, "")
|
||||
pdf.CellFormat(contentW/2-5, 6, "Valeurs importées", "", 1, "L", false, 0, "")
|
||||
|
||||
pdf.SetFont("DejaVu", "", 11)
|
||||
|
||||
y := pdf.GetY()
|
||||
pdf.SetXY(left, y)
|
||||
pdf.CellFormat(contentW/2-5, 6, "Mois : "+meta.YearMonth, "", 1, "L", false, 0, "")
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(contentW/2-5, 6, "Agent : "+fullName, "", 1, "L", false, 0, "")
|
||||
if strings.TrimSpace(meta.Matricule) != "" {
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(contentW/2-5, 6, "Matricule : "+meta.Matricule, "", 1, "L", false, 0, "")
|
||||
} else {
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(contentW/2-5, 6, "Matricule : (non renseigné)", "", 1, "L", false, 0, "")
|
||||
}
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(contentW/2-5, 6, "Profil : "+profileLabel, "", 1, "L", false, 0, "")
|
||||
|
||||
rightX := left + contentW/2 + 5
|
||||
pdf.SetXY(rightX, y)
|
||||
pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("456 : %.2f h", req.Q456), "", 1, "L", false, 0, "")
|
||||
pdf.SetX(rightX)
|
||||
pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("458 : %.2f h", req.Q458), "", 1, "L", false, 0, "")
|
||||
pdf.SetX(rightX)
|
||||
pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("459 : %.2f h", req.Q459), "", 1, "L", false, 0, "")
|
||||
pdf.SetX(rightX)
|
||||
pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("471 : %.2f h", req.Q471), "", 1, "L", false, 0, "")
|
||||
pdf.SetX(rightX)
|
||||
pdf.CellFormat(contentW/2-5, 6, fmt.Sprintf("Dim/JF : %d j", req.NbDimFerie), "", 1, "L", false, 0, "")
|
||||
|
||||
pdf.Ln(8)
|
||||
|
||||
// ---------------- TABLEAU ----------------
|
||||
pdf.SetFont("DejaVu", "B", 12)
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(contentW, 7, "Résultat", "", 1, "L", false, 0, "")
|
||||
|
||||
// Largeurs adaptées (total = 170)
|
||||
colW := []float64{24, 52, 38, 22, 34}
|
||||
headerColor := []int{197, 227, 242}
|
||||
|
||||
pdf.SetFont("DejaVu", "B", 9)
|
||||
pdf.SetFillColor(headerColor[0], headerColor[1], headerColor[2])
|
||||
pdf.SetX(left)
|
||||
headers := []string{"Code paie", "Libellé", "Nombre", "Base", "À payer"}
|
||||
for i, h := range headers {
|
||||
align := "L"
|
||||
if i >= 2 {
|
||||
align = "R"
|
||||
}
|
||||
pdf.CellFormat(colW[i], 7, h, "1", 0, align, true, 0, "")
|
||||
}
|
||||
pdf.Ln(-1)
|
||||
|
||||
pdf.SetFont("DejaVu", "", 9)
|
||||
for _, l := range res.Lines {
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(colW[0], 7, l.Code, "1", 0, "L", false, 0, "")
|
||||
pdf.CellFormat(colW[1], 7, l.Label, "1", 0, "L", false, 0, "")
|
||||
pdf.CellFormat(colW[2], 7, fmt.Sprintf("%.2f %s", l.Quantity, l.Unit), "1", 0, "R", false, 0, "")
|
||||
pdf.CellFormat(colW[3], 7, fmt.Sprintf("%.2f", l.Rate), "1", 0, "R", false, 0, "")
|
||||
pdf.CellFormat(colW[4], 7, fmt.Sprintf("%.2f", l.Amount), "1", 0, "R", false, 0, "")
|
||||
pdf.Ln(-1)
|
||||
}
|
||||
|
||||
// TOTAL BRUT (fond jaune clair)
|
||||
pdf.SetFont("DejaVu", "B", 10)
|
||||
pdf.SetFillColor(255, 249, 196)
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(colW[0]+colW[1]+colW[2]+colW[3], 7, "TOTAL BRUT", "1", 0, "L", true, 0, "")
|
||||
pdf.CellFormat(colW[4], 7, fmt.Sprintf("%.2f", res.Total), "1", 1, "R", true, 0, "")
|
||||
|
||||
pdf.Ln(6)
|
||||
|
||||
// ---------------- LÉGENDE ----------------
|
||||
pdf.SetFont("DejaVu", "B", 9)
|
||||
pdf.SetX(left)
|
||||
pdf.CellFormat(contentW, 5, "Correspondance code paie / fichier Excel", "", 1, "L", false, 0, "")
|
||||
|
||||
pdf.SetFont("DejaVu", "", 8)
|
||||
legend := "" +
|
||||
"• 456 → colonne L (Interv.astreinte Jour)\n" +
|
||||
"• 458 → colonne N (Interv.astreinte Dim & JF)\n" +
|
||||
"• 459 → colonne M (Interv.astreinte Nuit)\n" +
|
||||
"• 471 → colonne G (Astreinte = total d'heures du mois)\n" +
|
||||
"• 480 → déduit des lignes \"P_DIM-JF\" (colonne C) → compteur Dim/JF\n"
|
||||
pdf.SetX(left)
|
||||
pdf.MultiCell(contentW, 4.2, legend, "", "L", false)
|
||||
|
||||
// ---------------- FOOTER (toujours sur la page 1) ----------------
|
||||
// ✅ Position absolue proche du bas (A4=297mm) + marges
|
||||
pdf.SetFont("DejaVu", "", 8)
|
||||
pdf.SetTextColor(120, 120, 120)
|
||||
pdf.SetXY(12, 284)
|
||||
pdf.CellFormat(
|
||||
186,
|
||||
6,
|
||||
fmt.Sprintf("Paychek - Made with \u2665 in Go by Flooze Corp \u00B7 A Niarmud Nablax Company - Exporté le %s", created),
|
||||
"",
|
||||
0,
|
||||
"C",
|
||||
false,
|
||||
0,
|
||||
"",
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pdf.Output(&buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
50
internal/pdf/sanitize.go
Normal file
50
internal/pdf/sanitize.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// SanitizeFilename transforme une chaîne Unicode en nom de fichier sûr :
|
||||
// - supprime les accents (Clément → Clement)
|
||||
// - remplace les espaces par des tirets
|
||||
// - supprime les caractères interdits (Windows / Unix)
|
||||
// - évite les noms vides
|
||||
func SanitizeFilename(input string) string {
|
||||
if input == "" {
|
||||
return "inconnu"
|
||||
}
|
||||
|
||||
// 1) Normalisation Unicode (NFD) pour séparer lettres + accents
|
||||
t := norm.NFD.String(input)
|
||||
|
||||
// 2) Suppression des marques diacritiques (accents)
|
||||
sb := strings.Builder{}
|
||||
for _, r := range t {
|
||||
if unicode.Is(unicode.Mn, r) {
|
||||
continue
|
||||
}
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
out := sb.String()
|
||||
|
||||
// 3) Remplacements simples
|
||||
out = strings.ReplaceAll(out, " ", "-")
|
||||
out = strings.ReplaceAll(out, "_", "-")
|
||||
|
||||
// 4) Suppression des caractères interdits dans les noms de fichiers
|
||||
// Windows + Unix
|
||||
re := regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`)
|
||||
out = re.ReplaceAllString(out, "")
|
||||
|
||||
// 5) Nettoyage final
|
||||
out = strings.Trim(out, "-.")
|
||||
if out == "" {
|
||||
return "inconnu"
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user