Refonte mise en page des exports PDF (en-tête, pied de page, tables)
En-tête (mensuel + annuel) : - Titre « RAPPORT DE TRAJETS » en majuscules - Logo agrandi (120 pt) à gauche, bloc titre centré sur l'espace restant, bloc d'infos placé dessous, trait séparateur sur toute la largeur Pied de page (mensuel + annuel) : - Mention légale (déclaration sur l'honneur, calcul indicatif, non opposable aux administrations) en 5 pt justifiée, au-dessus de la pagination Rapport mensuel : - Adresse d'arrivée à la ligne sous le départ, précédée de « → » - Colonne dédiée « A/R » (marqueur « X ») avant les Kms ; largeurs ajustées (A/R minimale, Kms jusqu'à « 9 999 », adresses maximisées) Rapport annuel : - Espace après « Véhicule » ; barème applicable et total km sur deux colonnes - Titre du tableau barème complété « — Kms parcourus × Coef + Base » - Espace avant la régularisation, régularisation centrée Steering : nouvelle règle workflow (valider l'approche avant de coder, l'utilisateur lance le dev). Specs requirements.md / design-requirements.md / design.md synchronisées (Exigence 7 : critères + maquettes PDF + glossaire Mention_Legale).
This commit is contained in:
@@ -319,13 +319,7 @@ public class PdfService : IPdfService
|
||||
|
||||
page.Header().Element(c => ComposerEntete(c, modele, precision));
|
||||
page.Content().PaddingTop(10).Element(c => ComposerTableTrajets(c, modele, precision));
|
||||
page.Footer().AlignCenter().Text(txt =>
|
||||
{
|
||||
txt.Span("Page ");
|
||||
txt.CurrentPageNumber();
|
||||
txt.Span(" / ");
|
||||
txt.TotalPages();
|
||||
});
|
||||
page.Footer().Element(ComposerPiedDePage);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -351,24 +345,50 @@ public class PdfService : IPdfService
|
||||
$"{modele.Vehicule.Plaque} — {modele.Vehicule.PuissanceFiscale} CV " +
|
||||
$"(Coef. : {FormatDecimal(modele.Coefficient, 3)} / Base : {FormatDecimal(modele.Base, 0)})";
|
||||
|
||||
container.Row(ligne =>
|
||||
container.Column(entete =>
|
||||
{
|
||||
// Logo carré en haut à gauche.
|
||||
if (LogoCarre.Length > 0)
|
||||
// Logo à gauche ; à droite, bloc titre (centré) puis bloc d'infos en dessous.
|
||||
entete.Item().Row(bandeau =>
|
||||
{
|
||||
ligne.ConstantItem(45).Image(LogoCarre).FitWidth();
|
||||
}
|
||||
if (LogoCarre.Length > 0)
|
||||
{
|
||||
bandeau.ConstantItem(120).AlignTop().Image(LogoCarre).FitWidth();
|
||||
}
|
||||
|
||||
ligne.RelativeItem().PaddingLeft(LogoCarre.Length > 0 ? 10 : 0).Column(col =>
|
||||
bandeau.RelativeItem().PaddingLeft(LogoCarre.Length > 0 ? 12 : 0).Column(droite =>
|
||||
{
|
||||
// Bloc titre, centré sur l'espace restant après le logo.
|
||||
droite.Item().Column(titre =>
|
||||
{
|
||||
titre.Spacing(2);
|
||||
titre.Item().AlignCenter().Text("RAPPORT DE TRAJETS").FontSize(16).Bold();
|
||||
titre.Item().AlignCenter().Text($"Feuille d'indemnité kilométrique {periode}").FontSize(11).SemiBold();
|
||||
});
|
||||
|
||||
// Bloc d'infos, sous le bloc titre (espace pour les séparer).
|
||||
ComposerInfosMensuel(droite.Item().PaddingTop(10), modele, identite, matricule, domicile, vehiculeLibelle, precision);
|
||||
});
|
||||
});
|
||||
|
||||
// Trait séparateur sur toute la largeur de la page.
|
||||
entete.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Medium);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compose le bloc d'informations du rapport mensuel (identité, domicile, véhicule, totaux),
|
||||
/// affiché à droite du logo sous le bloc titre.
|
||||
/// </summary>
|
||||
private static void ComposerInfosMensuel(
|
||||
IContainer container, ModeleRapportMensuel modele, string identite, string matricule,
|
||||
string domicile, string vehiculeLibelle, int precision)
|
||||
{
|
||||
container.Column(col =>
|
||||
{
|
||||
col.Spacing(2);
|
||||
|
||||
// Titre fixe imposé par l'Exigence 7.6, période intégrée (remplace la ligne « Période »).
|
||||
col.Item().Text("Rapport de trajets").FontSize(16).Bold();
|
||||
col.Item().Text($"Feuille d'indemnité kilométrique {periode}").FontSize(11).SemiBold();
|
||||
|
||||
// Identité (nom, prénom, matricule) — Exigence 7.8.
|
||||
col.Item().PaddingTop(4).Text(txt =>
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Utilisateur : ").SemiBold();
|
||||
txt.Span($"{identite} — Matricule : {matricule}");
|
||||
@@ -418,10 +438,7 @@ public class PdfService : IPdfService
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
col.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Medium);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -436,8 +453,9 @@ public class PdfService : IPdfService
|
||||
{
|
||||
columns.ConstantColumn(70); // Date
|
||||
columns.RelativeColumn(2); // Motif (commentaire)
|
||||
columns.RelativeColumn(3); // Adresse départ → arrivée
|
||||
columns.ConstantColumn(55); // Kms
|
||||
columns.RelativeColumn(4); // Adresse départ → arrivée (maximisée pour tenir sur 2 lignes)
|
||||
columns.ConstantColumn(22); // A/R (minimale)
|
||||
columns.ConstantColumn(44); // Kms (jusqu'à « 9 999 » avec séparateur de milliers)
|
||||
});
|
||||
|
||||
// En-tête de table.
|
||||
@@ -446,33 +464,39 @@ public class PdfService : IPdfService
|
||||
header.Cell().Element(CelluleEntete).Text("Date");
|
||||
header.Cell().Element(CelluleEntete).Text("Motifs");
|
||||
header.Cell().Element(CelluleEntete).Text("Départ → Arrivée");
|
||||
header.Cell().Element(CelluleEntete).AlignCenter().Text("A/R").FontSize(8);
|
||||
header.Cell().Element(CelluleEntete).AlignRight().Text("Kms");
|
||||
});
|
||||
|
||||
if (modele.Trajets.Count == 0)
|
||||
{
|
||||
table.Cell().ColumnSpan(4).Element(Cellule).Text("Aucun trajet pour ce mois.").Italic();
|
||||
table.Cell().ColumnSpan(5).Element(Cellule).Text("Aucun trajet pour ce mois.").Italic();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var trajet in modele.Trajets)
|
||||
{
|
||||
var motif = string.IsNullOrWhiteSpace(trajet.Commentaire) ? "—" : trajet.Commentaire!;
|
||||
// Suffixe « (A/R) » pour les trajets aller/retour ; la distance affichée est
|
||||
// déjà la distance effective (doublée) portée par le TrajetDto.
|
||||
var itineraire = trajet.AllerRetour
|
||||
? $"{trajet.AdresseDepart} → {trajet.AdresseArrivee} (A/R)"
|
||||
: $"{trajet.AdresseDepart} → {trajet.AdresseArrivee}";
|
||||
var km = trajet.Distance ?? 0m;
|
||||
|
||||
table.Cell().Element(Cellule).Text(trajet.Date.ToString("dd/MM/yyyy", FrCulture));
|
||||
table.Cell().Element(Cellule).Text(motif);
|
||||
table.Cell().Element(Cellule).Text(itineraire);
|
||||
// Adresse de départ, puis adresse d'arrivée sur une nouvelle ligne, précédée de « → ».
|
||||
// L'information aller/retour est désormais portée par la colonne A/R dédiée.
|
||||
table.Cell().Element(Cellule).Text(txt =>
|
||||
{
|
||||
txt.Line(trajet.AdresseDepart);
|
||||
txt.Span($"→ {trajet.AdresseArrivee}");
|
||||
});
|
||||
// Colonne A/R : « X » lorsque le trajet est un aller/retour, sinon vide
|
||||
// (« X » plutôt qu'une coche Unicode, non rendue par la police par défaut Lato).
|
||||
// La distance affichée est déjà la distance effective (doublée) portée par le TrajetDto.
|
||||
table.Cell().Element(Cellule).AlignCenter().Text(trajet.AllerRetour ? "X" : string.Empty).SemiBold();
|
||||
table.Cell().Element(Cellule).AlignRight().Text(FormatKm(km, precision));
|
||||
}
|
||||
|
||||
// Ligne de total du mois.
|
||||
table.Cell().ColumnSpan(3).Element(CelluleTotal).AlignRight().Text("Total du mois :").SemiBold();
|
||||
table.Cell().ColumnSpan(4).Element(CelluleTotal).AlignRight().Text("Total du mois :").SemiBold();
|
||||
table.Cell().Element(CelluleTotal).AlignRight().Text($"{FormatKm(modele.TotalKmMois, precision)}").SemiBold();
|
||||
}
|
||||
});
|
||||
@@ -511,13 +535,7 @@ public class PdfService : IPdfService
|
||||
col.Item().Element(c => ComposerTableSyntheseAnnuelle(c, modele, precision));
|
||||
col.Item().Element(c => ComposerTotauxAnnuels(c, modele, precision));
|
||||
});
|
||||
page.Footer().AlignCenter().Text(txt =>
|
||||
{
|
||||
txt.Span("Page ");
|
||||
txt.CurrentPageNumber();
|
||||
txt.Span(" / ");
|
||||
txt.TotalPages();
|
||||
});
|
||||
page.Footer().Element(ComposerPiedDePage);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -531,7 +549,6 @@ public class PdfService : IPdfService
|
||||
/// </summary>
|
||||
private static void ComposerEnteteAnnuel(IContainer container, ModeleRapportAnnuel modele, int precision)
|
||||
{
|
||||
var reg = modele.Regularisation;
|
||||
var identite = FormatIdentite(modele.Profil);
|
||||
var matricule = string.IsNullOrWhiteSpace(modele.Profil.Matricule)
|
||||
? "—"
|
||||
@@ -540,24 +557,52 @@ public class PdfService : IPdfService
|
||||
? "—"
|
||||
: modele.Profil.AdresseDomicile!;
|
||||
|
||||
container.Row(ligne =>
|
||||
container.Column(entete =>
|
||||
{
|
||||
// Logo carré en haut à gauche.
|
||||
if (LogoCarre.Length > 0)
|
||||
// Logo à gauche ; à droite, bloc titre (centré) puis bloc d'infos en dessous.
|
||||
entete.Item().Row(bandeau =>
|
||||
{
|
||||
ligne.ConstantItem(45).Image(LogoCarre).FitWidth();
|
||||
}
|
||||
if (LogoCarre.Length > 0)
|
||||
{
|
||||
bandeau.ConstantItem(120).AlignTop().Image(LogoCarre).FitWidth();
|
||||
}
|
||||
|
||||
ligne.RelativeItem().PaddingLeft(LogoCarre.Length > 0 ? 10 : 0).Column(col =>
|
||||
bandeau.RelativeItem().PaddingLeft(LogoCarre.Length > 0 ? 12 : 0).Column(droite =>
|
||||
{
|
||||
// Bloc titre, centré sur l'espace restant après le logo.
|
||||
droite.Item().Column(titre =>
|
||||
{
|
||||
titre.Spacing(2);
|
||||
titre.Item().AlignCenter().Text("RAPPORT DE TRAJETS").FontSize(16).Bold();
|
||||
titre.Item().AlignCenter().Text($"Synthèse {modele.Annee} d'indemnité kilométrique").FontSize(11).SemiBold();
|
||||
});
|
||||
|
||||
// Bloc d'infos, sous le bloc titre (espace pour les séparer).
|
||||
ComposerInfosAnnuel(droite.Item().PaddingTop(10), modele, identite, matricule, domicile, precision);
|
||||
});
|
||||
});
|
||||
|
||||
// Trait séparateur sur toute la largeur de la page.
|
||||
entete.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Medium);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compose le bloc d'informations de la synthèse annuelle (identité, domicile, véhicule,
|
||||
/// tranche applicable, total annuel), affiché à droite du logo sous le bloc titre.
|
||||
/// </summary>
|
||||
private static void ComposerInfosAnnuel(
|
||||
IContainer container, ModeleRapportAnnuel modele, string identite, string matricule,
|
||||
string domicile, int precision)
|
||||
{
|
||||
var reg = modele.Regularisation;
|
||||
|
||||
container.Column(col =>
|
||||
{
|
||||
col.Spacing(2);
|
||||
|
||||
// Titre fixe imposé par l'Exigence 7.6.
|
||||
col.Item().Text("Rapport de trajets").FontSize(16).Bold();
|
||||
col.Item().Text($"Synthèse {modele.Annee} d'indemnité kilométrique").FontSize(11).SemiBold();
|
||||
|
||||
// Identité (nom, prénom, matricule) — Exigence 7.8.
|
||||
col.Item().PaddingTop(4).Text(txt =>
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Utilisateur : ").SemiBold();
|
||||
txt.Span($"{identite} — Matricule : {matricule}");
|
||||
@@ -577,22 +622,22 @@ public class PdfService : IPdfService
|
||||
txt.Span("Véhicule : ").SemiBold();
|
||||
txt.Span($"{reg.Plaque} — {reg.PuissanceFiscale} CV");
|
||||
});
|
||||
col.Item().Text(txt =>
|
||||
// Barème applicable et total kilométrique sur deux colonnes (gain d'une ligne,
|
||||
// même agencement que le bloc totaux du rapport mensuel) — Exigences 7.6/7.7/7.12.
|
||||
col.Item().PaddingTop(6).Row(row =>
|
||||
{
|
||||
txt.Span("Barème applicable : ").SemiBold();
|
||||
txt.Span(reg.TrancheLibelle);
|
||||
row.RelativeItem().Text(txt =>
|
||||
{
|
||||
txt.Span("Barème applicable : ").SemiBold();
|
||||
txt.Span(reg.TrancheLibelle);
|
||||
});
|
||||
row.RelativeItem().Text(txt =>
|
||||
{
|
||||
txt.Span("Total kilométrique de l'année : ").SemiBold();
|
||||
txt.Span($"{FormatKm(reg.TotalKmAnnuel, precision)} km");
|
||||
});
|
||||
});
|
||||
|
||||
// Total kilométrique de la période (Exigence 7.6).
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Total kilométrique de l'année : ").SemiBold();
|
||||
txt.Span($"{FormatKm(reg.TotalKmAnnuel, precision)} km");
|
||||
});
|
||||
|
||||
col.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Medium);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -607,7 +652,7 @@ public class PdfService : IPdfService
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Span("Barème applicable pour l'année ").SemiBold();
|
||||
txt.Span($"({modele.Regularisation.PuissanceFiscale} CV)").SemiBold();
|
||||
txt.Span($"({modele.Regularisation.PuissanceFiscale} CV) — Kms parcourus × Coef + Base").SemiBold();
|
||||
});
|
||||
|
||||
if (modele.TranchesBareme.Count == 0)
|
||||
@@ -725,7 +770,8 @@ public class PdfService : IPdfService
|
||||
txt.Span("Formule réelle : ").SemiBold();
|
||||
txt.Span(formule);
|
||||
});
|
||||
col.Item().Text(txt =>
|
||||
// Espace entre la formule réelle et la régularisation, puis régularisation centrée.
|
||||
col.Item().PaddingTop(8).AlignCenter().Text(txt =>
|
||||
{
|
||||
txt.Span("Régularisation (réel − payé) : ").SemiBold();
|
||||
txt.Span($"{FormatMontant(reg.Regularisation)} €");
|
||||
@@ -733,6 +779,41 @@ public class PdfService : IPdfService
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pied de page commun aux deux rapports : mention indicative en petite taille, puis la
|
||||
/// pagination en dessous.
|
||||
/// </summary>
|
||||
private static void ComposerPiedDePage(IContainer container)
|
||||
{
|
||||
container.Column(col =>
|
||||
{
|
||||
col.Spacing(2);
|
||||
|
||||
// Mention légale indicative, en très petit caractère pour tenir sur 2-3 lignes.
|
||||
col.Item().Text(txt =>
|
||||
{
|
||||
txt.Justify();
|
||||
txt.DefaultTextStyle(x => x.FontSize(5).FontColor(Colors.Grey.Darken1));
|
||||
txt.Span(
|
||||
"Les kilomètres restitués dans le présent document sont déclarés par l'utilisateur sur l'honneur " +
|
||||
"et sous sa seule responsabilité. Le calcul de l'indemnité kilométrique est fourni à titre " +
|
||||
"purement indicatif, sur la base du barème kilométrique officiel en vigueur. SchotzIAD ne saurait " +
|
||||
"être tenue responsable d'une quelconque erreur ou omission dans les données saisies ni du montant " +
|
||||
"résultant du calcul. Le présent document ne constitue pas un justificatif opposable " +
|
||||
"aux administrations.");
|
||||
});
|
||||
|
||||
col.Item().AlignCenter().Text(txt =>
|
||||
{
|
||||
txt.DefaultTextStyle(x => x.FontSize(8));
|
||||
txt.Span("Page ");
|
||||
txt.CurrentPageNumber();
|
||||
txt.Span(" / ");
|
||||
txt.TotalPages();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static string FormatIdentite(ProfilDto profil)
|
||||
{
|
||||
var prenom = (profil.Prenom ?? string.Empty).Trim();
|
||||
|
||||
Reference in New Issue
Block a user