package pdf import ( "bytes" "embed" "fmt" "strings" "time" "github.com/phpdave11/gofpdf" "git.dumerain.org/alban/paycheck/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 }