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:
2026-10-10 10:33:23 +02:00
parent 6bdd67fd00
commit e9f34e59e8
5 changed files with 238 additions and 95 deletions
@@ -104,6 +104,12 @@ Le document est organisé **en miroir** des exigences de `requirements.md` : cha
#### Acceptance Criteria #### Acceptance Criteria
1. IF le Backend ne parvient pas à générer le fichier PDF dans un délai de 15 secondes, THEN THE Backend SHALL retourner une erreur et THE Frontend SHALL afficher un message indiquant que la génération du PDF a échoué. 1. IF le Backend ne parvient pas à générer le fichier PDF dans un délai de 15 secondes, THEN THE Backend SHALL retourner une erreur et THE Frontend SHALL afficher un message indiquant que la génération du PDF a échoué.
2. WHEN le Backend compose l'en-tête d'un Export_PDF (mensuel ou annuel), THE Backend SHALL disposer l'en-tête sur une rangée comportant le logo carré aligné en haut à gauche à une largeur de 120 pt, puis à droite du logo un bloc titre (titre "RAPPORT DE TRAJETS" en 16 pt gras et sous-titre spécifique en 11 pt) centré sur l'espace restant, le bloc d'infos étant placé sous le bloc titre, et SHALL tracer un trait séparateur horizontal sur toute la largeur de la page sous l'en-tête.
3. WHEN le Backend compose le pied de page d'un Export_PDF (mensuel ou annuel), THE Backend SHALL afficher la Mention_Legale en police de 5 pt, en gris, justifiée, positionnée au-dessus de la pagination « Page X / Y ».
4. WHEN le Backend compose la colonne "A/R" de la table des Trajets d'un Export_PDF mensuel, THE Backend SHALL afficher le marqueur « X » (et non une coche Unicode, non rendue par la police par défaut Lato) pour un Trajet Aller_Retour.
5. WHEN le Backend définit les largeurs de colonnes de la table des Trajets d'un Export_PDF mensuel, THE Backend SHALL dimensionner la colonne "A/R" à sa largeur minimale, la colonne "Kms" pour afficher jusqu'à « 9 999 » (avec séparateur de milliers), et la colonne des adresses de façon maximisée pour que l'adresse de départ et l'adresse d'arrivée tiennent majoritairement sur deux lignes.
6. WHEN le Backend compose le bloc d'infos de l'en-tête d'un Export_PDF annuel, THE Backend SHALL insérer un espace après la ligne « Véhicule » et présenter « Barème applicable » et « Total kilométrique de l'année » sur deux colonnes côte à côte.
7. WHEN le Backend compose la fin d'un Export_PDF annuel, THE Backend SHALL conserver la ligne « Formule réelle » alignée à gauche, insérer un espace, puis centrer la ligne « Régularisation (réel − payé) » sur la largeur de la page.
--- ---
+61 -29
View File
@@ -1549,42 +1549,50 @@ QuestPDF est une bibliothèque .NET open-source (licence MIT pour usage communau
``` ```
┌────────────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────────────┐
│ [Logo] Rapport de trajets │ │ ┌───────┐ RAPPORT DE TRAJETS (bloc titre centré │
│ Feuille d'indemnité kilométrique JANVIER 2025 │ │ │ │ Feuille d'indemnité kilométrique à droite du logo) │
│ Utilisateur : Cécile DOUCET — Matricule : AB12CD34 │ │ │ Logo │ JANVIER 2025 │
│ Domicile : 29 route des Cascades, 23400 Saint-Pardoux-Morterolles │ │ │ 120pt │ Utilisateur : Cécile DOUCET — Matricule : AB12CD34 │
│ Véhicule : GOLF (ES-036-YF) — 6 CV (Coef. : 0,447 / Base : 0) │ │ │ │ Domicile : 29 route des Cascades, 23400 Saint-Pardoux-Morter. │
│ Total parcouru sur le mois : 931 Kms TOTAL IK du mois : 416,16 € │ │ └───────┘ Véhicule : GOLF (ES-036-YF) — 6 CV (Coef. : 0,447 / Base : 0) │
│ Total parcouru sur l'année : 931 Kms Réglé : 500,00 € │ │ Total parcouru sur le mois : 931 Kms TOTAL IK du mois : 416 € │
├────────────┬──────────────────────────┬──────────────────────────┬─────────┤ │ Total parcouru sur l'année : 931 Kms Réglé : 500,00 € │
│ Date │ Motifs (commentaire) │ Adresse départ → arrivée │ Kms │ ├──────────────────────────────────────────────────────────────────────────── (trait pleine largeur)
├────────────┼──────────────────────────┼──────────────────────────┼─────────┤ ├────────────┬────────────────┬───────────────────────────────┬─────┬────────┤
│ 03/01/2025 │ Rdv chez Marie │ ... → Aureil │ 94 │ │ Date │ Motifs │ Départ → Arrivée │ A/R │ Kms │
│ ... │ ... │ ... │ ... │ ├────────────┼────────────────┼───────────────────────────────┼─────┼────────┤
└────────────┴──────────────────────────┴──────────────────────────┴─────────┘ │ 03/01/2025 │ Rdv chez Marie │ 29 route des Cascades... │ X │ 94 │
│ │ │ → Aureil │ │ │
│ ... │ ... │ ... │ │ ... │
├────────────┴────────────────┴───────────────────────────────┼─────┼────────┤
│ Total du mois : │ │ 931 │
└──────────────────────────────────────────────────────────────┴─────┴────────┘
(pied de page : Mention_Legale 5 pt justifiée, puis « Page X / Y »)
``` ```
> Le titre du document intègre désormais le mois et l'année (« Feuille d'indemnité kilométrique JANVIER 2025 ») ; il n'y a plus de ligne « Période » distincte dans l'en-tête. Le libellé « Adresse de départ » devient « Domicile » et est positionné juste après la ligne Utilisateur/Matricule (au lieu d'être après la ligne Véhicule). Le « TOTAL IK du mois » = kilomètres du mois × coefficient de la tranche annuelle applicable. Le « Réglé » = Montant_Regle saisi pour le mois. Le motif affiché est le commentaire du Trajet. Les kilomètres (mois, année, table des trajets) sont formatés selon Precision_Distance. > Le titre « RAPPORT DE TRAJETS » est désormais en majuscules. L'en-tête est disposé sur une rangée : logo carré agrandi (120 pt) aligné en haut à gauche ; à droite du logo, un bloc titre (titre + sous-titre « Feuille d'indemnité kilométrique {MOIS} {ANNEE} ») centré sur l'espace restant ; sous le bloc titre, le bloc d'infos (Utilisateur/Matricule, Domicile, Véhicule, totaux). Un trait séparateur horizontal est tracé sur toute la largeur de la page sous l'en-tête. Le libellé « Adresse de départ » devient « Domicile », positionné juste après la ligne Utilisateur/Matricule. Dans la table des Trajets, l'adresse d'arrivée passe à la ligne sous l'adresse de départ, précédée de « → » ; une colonne dédiée **« A/R »** (juste avant « Kms ») affiche un marqueur « X » pour un Aller_Retour (le suffixe « (A/R) » dans la cellule itinéraire est supprimé). Le « TOTAL IK du mois » = kilomètres du mois × coefficient de la tranche annuelle applicable. Le « Réglé » = Montant_Regle saisi pour le mois. Le motif affiché est le commentaire du Trajet. Les kilomètres (mois, année, table des trajets) sont formatés selon Precision_Distance.
#### PDF Annuel (`AAAA_Trajets_MATRICULE_PLAQUE.pdf`) #### PDF Annuel (`AAAA_Trajets_MATRICULE_PLAQUE.pdf`)
``` ```
┌──────────────────────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────────────────────┐
│ [Logo] Rapport de trajets │ │ ┌───────┐ RAPPORT DE TRAJETS (bloc titre centré │
│ Synthèse 2025 d'indemnité kilométrique │ │ │ │ Synthèse 2025 d'indemnité km à droite du logo) │
│ Utilisateur : Cécile DOUCET — Matricule : AB12CD34 │ │ │ Logo │ │
│ Domicile : 29 route des Cascades, 23400 Saint-Pardoux-Morterolles │ │ │ 120pt │ Utilisateur : Cécile DOUCET — Matricule : AB12CD34 │
│ Véhicule : GOLF (ES-036-YF) — 6 CV │ │ │ │ Domicile : 29 route des Cascades, 23400 Saint-Pardoux-Morter. │
│ Barème applicable : 5001–20000 │ │ └───────┘ Véhicule : GOLF (ES-036-YF) — 6 CV │
│ Total kilométrique de l'année : 12 241 km │ │ (espace après Véhicule)│
├──────────────────────────────────────────────────────────────────────────────┤ │ Barème applicable : 5001–20000 Total kilométrique de │
│ Barème applicable pour l'année (6 CV) : │ │ l'année : 12 241 km │
├──────────────────────────────────────────────────────────────────────────── (trait pleine largeur)
│ Barème applicable pour l'année (6 CV) — Kms parcourus × Coef + Base │
│ Tranche (km) │ Coefficient │ Base (€) │ │ Tranche (km) │ Coefficient │ Base (€) │
│ 0 à 5000 │ 0,665 │ 0 │ │ 0 à 5000 │ 0,665 │ 0 │
│ 5001 à 20000 │ 0,374 │ 1457 │ │ 5001 à 20000 │ 0,374 │ 1457 │
│ 20000 et plus │ 0,447 │ 0 │ │ 20000 et plus │ 0,447 │ 0 │
├────────────┬───────────────┬───────────────────┬────────────────────────────┤ ├────────────┬───────────────┬───────────────────┬────────────────────────────┤
│ Mois │ Kms parcourus │ Frais payé (réglé)│ Frais réel │ │ Mois │ Kms parcourus │ Frais payé │ Frais réel │
├────────────┼───────────────┼───────────────────┼────────────────────────────┤ ├────────────┼───────────────┼───────────────────┼────────────────────────────┤
│ Janvier │ 931 │ 500,00 € │ 459,01 € │ │ Janvier │ 931 │ 500,00 € │ 459,01 € │
│ ... │ ... │ ... │ ... │ │ ... │ ... │ ... │ ... │
@@ -1593,24 +1601,48 @@ QuestPDF est une bibliothèque .NET open-source (licence MIT pour usage communau
│ TOTAL │ 12 241 │ 6 000,00 € │ 6 035,13 € │ │ TOTAL │ 12 241 │ 6 000,00 € │ 6 035,13 € │
├────────────┴───────────────┴───────────────────┴────────────────────────────┤ ├────────────┴───────────────┴───────────────────┴────────────────────────────┤
│ Formule réelle : 12241 km × 0,374 + 1457 = 6 035,13 € (tranche 5001–20000, 6 CV) │ │ Formule réelle : 12241 km × 0,374 + 1457 = 6 035,13 € (tranche 5001–20000, 6 CV) │
│ Régularisation (réel − payé) : 35,13 € │ │ │
│ Régularisation (réel − payé) : 35,13 € (ligne centrée) │
└──────────────────────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────────────────────┘
(pied de page : Mention_Legale 5 pt justifiée, puis « Page X / Y »)
``` ```
> La ligne « Année : AAAA », auparavant présente dans l'en-tête, est retirée : elle est redondante avec le titre « Synthèse {Année} d'indemnité kilométrique ». Le « Frais réel » mensuel = kilomètres du mois × coefficient de la tranche annuelle. Le « Frais payé » = Montant_Regle saisi. La minoration 80% n'apparaît pas dans la synthèse : elle sert uniquement d'indication de versement en cours d'année. Les kilomètres (total annuel, table des mois, formule réelle) sont formatés selon Precision_Distance. > Le titre « RAPPORT DE TRAJETS » est en majuscules et son bloc titre (titre + sous-titre « Synthèse {Année} d'indemnité kilométrique ») est centré à droite du logo 120 pt ; le bloc d'infos est placé dessous. La ligne « Année : AAAA » reste retirée (redondante avec le titre). Un espace est ajouté après la ligne « Véhicule », puis « Barème applicable » et « Total kilométrique de l'année » sont présentés sur **deux colonnes côte à côte** (même agencement que le bloc totaux du rapport mensuel). Le titre du tableau barème rappelle le mode de calcul en toutes lettres : « Barème applicable pour l'année (N CV) — Kms parcourus × Coef + Base » (les valeurs coef/base effectivement appliquées apparaissent, elles, dans la « Formule réelle » en fin de document). En fin de document, un espace sépare la ligne « Formule réelle » (alignée à gauche) de la ligne « Régularisation (réel − payé) », désormais **centrée** sur la largeur de la page. Le « Frais réel » mensuel = kilomètres du mois × coefficient de la tranche annuelle. Le « Frais payé » = Montant_Regle saisi. La minoration 80% n'apparaît pas dans la synthèse : elle sert uniquement d'indication de versement en cours d'année. Les kilomètres (total annuel, table des mois, formule réelle) sont formatés selon Precision_Distance.
> **Rapports distincts par véhicule** : si la période demandée (mois ou année) contient des Trajets associés à plusieurs Véhicules, le backend génère un fichier PDF distinct par Véhicule. Chaque PDF ne contient que les Trajets et montants du Véhicule concerné. > **Rapports distincts par véhicule** : si la période demandée (mois ou année) contient des Trajets associés à plusieurs Véhicules, le backend génère un fichier PDF distinct par Véhicule. Chaque PDF ne contient que les Trajets et montants du Véhicule concerné.
### En-tête obligatoire (Exigence 7.6) ### En-tête obligatoire (Exigence 7.6)
Tous les PDF incluent en en-tête : Tous les PDF incluent en en-tête :
1. Le **logo carré** de l'Application, en haut à gauche (voir « Logo dans l'en-tête » ci-dessous) 1. Le **logo carré** de l'Application, aligné en haut à gauche (voir « Logo dans l'en-tête » ci-dessous)
2. Titre fixe : **"Rapport de trajets"**, suivi du titre spécifique intégrant la période (`Feuille d'indemnité kilométrique {Mois} {AAAA}` pour le mensuel, `Synthèse {AAAA} d'indemnité kilométrique` pour l'annuel) 2. Titre fixe : **"RAPPORT DE TRAJETS"** (en majuscules), suivi du titre spécifique intégrant la période (`Feuille d'indemnité kilométrique {Mois} {AAAA}` pour le mensuel, `Synthèse {AAAA} d'indemnité kilométrique` pour l'annuel) — titre et sous-titre formant un bloc titre **centré** sur l'espace restant à droite du logo
3. Total kilométrique de la période, formaté selon Precision_Distance 3. Total kilométrique de la période, formaté selon Precision_Distance
### Logo dans l'en-tête (Exigences 7.13) ### Logo dans l'en-tête (Exigences 7.13)
Le logo carré (`logo-carre.png`, cf. section Charte graphique) est **embarqué comme ressource** dans l'assembly du backend (`Assets/logo-carre.png`, copié depuis les assets du frontend au moment du build) plutôt que lu depuis le système de fichiers, pour rester indépendant du répertoire de travail au runtime. `PdfService` charge ces octets une seule fois via `Assembly.GetManifestResourceStream`, en cache statique, et les insère via `Image().FitWidth()` dans une colonne fixe (45 pt) à gauche de la colonne de texte de l'en-tête, pour les deux rapports (mensuel et annuel). Si la ressource est introuvable (tableau vide), le logo est simplement omis sans faire échouer la génération du PDF. Le logo carré (`logo-carre.png`, cf. section Charte graphique) est **embarqué comme ressource** dans l'assembly du backend (`Assets/logo-carre.png`, copié depuis les assets du frontend au moment du build) plutôt que lu depuis le système de fichiers, pour rester indépendant du répertoire de travail au runtime. `PdfService` charge ces octets une seule fois via `Assembly.GetManifestResourceStream`, en cache statique, et les insère via `Image().FitWidth()` dans une colonne fixe de **120 pt** (au lieu de 45 pt), alignée en haut (`AlignTop`) à gauche de l'en-tête, pour les deux rapports (mensuel et annuel). Si la ressource est introuvable (tableau vide), le logo est simplement omis sans faire échouer la génération du PDF.
L'en-tête est désormais disposé **en rangée** (`Row`) : le logo à gauche, puis à sa droite une colonne contenant d'abord le **bloc titre centré** (titre « RAPPORT DE TRAJETS » 16 pt gras + sous-titre spécifique 11 pt, chacun en `AlignCenter`), puis, séparé par un espace (`PaddingTop`), le **bloc d'infos** (Utilisateur/Matricule, Domicile, Véhicule, totaux). Un **trait séparateur horizontal** (`LineHorizontal`) est tracé sur toute la largeur de la page sous l'en-tête. Cette disposition remplace l'ancienne superposition `Layers` où le titre était centré sur toute la largeur de la page avec un logo de 45 pt.
### Pied de page (Exigences 7.14)
Le pied de page, commun aux deux rapports, est composé par `ComposerPiedDePage` et comporte deux éléments empilés :
1. La **Mention_Legale** indicative : « 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. » — rendue en **police 5 pt**, couleur **grise**, **justifiée** (`Justify`), de façon à tenir sur deux à trois lignes.
2. La **pagination** « Page X / Y » (`CurrentPageNumber` / `TotalPages`), centrée en 8 pt, placée **sous** la Mention_Legale.
### Table des Trajets du rapport mensuel (Exigences 7.10, 7.15, 7.16)
La table des Trajets du rapport mensuel (`ComposerTableTrajets`) comporte cinq colonnes : Date (70 pt), Motifs (relative 2), **Départ → Arrivée** (relative 4, maximisée pour que départ et arrivée tiennent majoritairement sur deux lignes), **A/R** (constante 22 pt, largeur minimale) et **Kms** (constante 44 pt, dimensionnée pour afficher jusqu'à « 9 999 » avec séparateur de milliers).
- **Itinéraire** : l'adresse de départ est rendue en première ligne, puis l'adresse d'arrivée **passe à la ligne** en dessous, précédée du symbole « → » (`txt.Line(départ)` suivi de `txt.Span("→ " + arrivée)`).
- **Colonne A/R** : affiche le marqueur « **X** » lorsque le Trajet est un Aller_Retour, et reste vide sinon. Le marqueur « X » est préféré à une coche Unicode (« ✓ ») car la police par défaut Lato ne la rend pas. L'information Aller_Retour n'est plus signalée par le suffixe « (A/R) » dans la cellule itinéraire ; la distance affichée reste la Distance_Effective (déjà doublée) portée par le `TrajetDto`.
- La ligne de total du mois s'étend sur les quatre premières colonnes (`ColumnSpan(4)`) et affiche le total dans la colonne Kms.
### En-tête et fin du rapport annuel (Exigences 7.11, 7.14)
- **Bloc d'infos de l'en-tête** (`ComposerInfosAnnuel`) : un espace (`PaddingTop`) est ajouté après la ligne « Véhicule », puis « Barème applicable » et « Total kilométrique de l'année » sont présentés sur **deux colonnes côte à côte** (`Row` de deux `RelativeItem`), même agencement que le bloc totaux du rapport mensuel.
- **Titre du tableau barème** (`ComposerTableBaremeAnnuel`) : complété pour rappeler le mode de calcul en toutes lettres — « Barème applicable pour l'année (N CV) — Kms parcourus × Coef + Base ». Les valeurs coef/base effectivement appliquées apparaissent, elles, dans la « Formule réelle » en fin de document.
- **Fin du document** (`ComposerTotauxAnnuels`) : la ligne « Formule réelle : … » reste alignée à gauche ; un espace (`PaddingTop`) la sépare de la ligne « Régularisation (réel − payé) : … € », désormais **centrée** (`AlignCenter`) sur la largeur de la page.
### Precision_Distance dans les PDF (Exigence 7.12) ### Precision_Distance dans les PDF (Exigence 7.12)
@@ -24,6 +24,7 @@ L'application permet de gérer un profil (identité, matricule, adresse de domic
- **Calculateur_Distance** : Le composant backend responsable du calcul de la Distance entre deux points géographiques. - **Calculateur_Distance** : Le composant backend responsable du calcul de la Distance entre deux points géographiques.
- **API** : L'interface de programmation exposée par le Backend pour communiquer avec le Frontend. - **API** : L'interface de programmation exposée par le Backend pour communiquer avec le Frontend.
- **Export_PDF** : La fonctionnalité permettant de générer un fichier PDF contenant les données d'un Résumé_Mensuel ou d'un Résumé_Annuel. - **Export_PDF** : La fonctionnalité permettant de générer un fichier PDF contenant les données d'un Résumé_Mensuel ou d'un Résumé_Annuel.
- **Mention_Legale** : L'avertissement légal obligatoire figurant en pied de page de chaque Export_PDF, rappelant que les kilomètres sont déclarés sur l'honneur par l'utilisateur, que le calcul de l'indemnité kilométrique est purement indicatif, que SchotzIAD décline toute responsabilité sur les données saisies et le montant calculé, et que le document ne constitue pas un justificatif opposable aux administrations.
- **Carte** : La visualisation cartographique interactive affichant les itinéraires des Trajets d'une journée. - **Carte** : La visualisation cartographique interactive affichant les itinéraires des Trajets d'une journée.
- **Cache_Adresses** : Le stockage des couples (adresse textuelle normalisée, coordonnées géographiques) des adresses déjà saisies, afin d'éviter des recherches répétées pour des adresses déjà connues. - **Cache_Adresses** : Le stockage des couples (adresse textuelle normalisée, coordonnées géographiques) des adresses déjà saisies, afin d'éviter des recherches répétées pour des adresses déjà connues.
- **Carnet_Adresses** : Le répertoire des Adresses_Nommées gérées par l'utilisateur via un menu dédié, permettant de créer, lister, modifier et supprimer des adresses réutilisables identifiées par une Dénomination, afin de simplifier la saisie des Trajets. - **Carnet_Adresses** : Le répertoire des Adresses_Nommées gérées par l'utilisateur via un menu dédié, permettant de créer, lister, modifier et supprimer des adresses réutilisables identifiées par une Dénomination, afin de simplifier la saisie des Trajets.
@@ -208,7 +209,7 @@ L'application permet de gérer un profil (identité, matricule, adresse de domic
2. WHEN l'utilisateur demande l'export PDF d'un Résumé_Annuel, THE Backend SHALL générer un fichier PDF contenant le tableau des douze mois (mois, total kilométrique) ainsi que le total kilométrique annuel. 2. WHEN l'utilisateur demande l'export PDF d'un Résumé_Annuel, THE Backend SHALL générer un fichier PDF contenant le tableau des douze mois (mois, total kilométrique) ainsi que le total kilométrique annuel.
3. WHEN le Backend génère un Export_PDF avec succès, THE Frontend SHALL déclencher le téléchargement automatique du fichier PDF dans le navigateur de l'utilisateur. IF le mécanisme de téléchargement échoue après une génération réussie, THEN THE Frontend SHALL afficher un message d'erreur indiquant que le téléchargement a échoué. 3. WHEN le Backend génère un Export_PDF avec succès, THE Frontend SHALL déclencher le téléchargement automatique du fichier PDF dans le navigateur de l'utilisateur. IF le mécanisme de téléchargement échoue après une génération réussie, THEN THE Frontend SHALL afficher un message d'erreur indiquant que le téléchargement a échoué.
4. THE Backend SHALL nommer le fichier PDF exporté selon le format suivant : `AAAAMM_Trajets_MATRICULE_PLAQUE.pdf` pour un export mensuel et `AAAA_Trajets_MATRICULE_PLAQUE.pdf` pour un export annuel, où PLAQUE désigne la plaque d'immatriculation du Véhicule nettoyée de ses espaces et de ses tirets ; les segments MATRICULE et PLAQUE, ainsi que le séparateur `_` qui les précède, SHALL être omis lorsque la donnée correspondante (matricule du Profil ou plaque du Véhicule) est vide ou absente. 4. THE Backend SHALL nommer le fichier PDF exporté selon le format suivant : `AAAAMM_Trajets_MATRICULE_PLAQUE.pdf` pour un export mensuel et `AAAA_Trajets_MATRICULE_PLAQUE.pdf` pour un export annuel, où PLAQUE désigne la plaque d'immatriculation du Véhicule nettoyée de ses espaces et de ses tirets ; les segments MATRICULE et PLAQUE, ainsi que le séparateur `_` qui les précède, SHALL être omis lorsque la donnée correspondante (matricule du Profil ou plaque du Véhicule) est vide ou absente.
5. WHEN le Backend génère un Export_PDF, THE Backend SHALL inclure dans le PDF : le titre "Rapport de trajets", la période couverte (mois/année ou année), et le total kilométrique de la période en en-tête du document. 5. WHEN le Backend génère un Export_PDF, THE Backend SHALL inclure dans le PDF : le titre "RAPPORT DE TRAJETS" (en majuscules), la période couverte (mois/année ou année), et le total kilométrique de la période en en-tête du document.
6. WHEN l'utilisateur demande un export PDF (mensuel ou annuel), THE Backend SHALL n'inclure que les Trajets associés au Véhicule sélectionné et SHALL indiquer dans l'en-tête du PDF la plaque d'immatriculation et la Puissance_Fiscale du Véhicule concerné. 6. WHEN l'utilisateur demande un export PDF (mensuel ou annuel), THE Backend SHALL n'inclure que les Trajets associés au Véhicule sélectionné et SHALL indiquer dans l'en-tête du PDF la plaque d'immatriculation et la Puissance_Fiscale du Véhicule concerné.
7. WHEN le Backend génère un Export_PDF (mensuel ou annuel), THE Backend SHALL inclure dans l'en-tête du document : le nom, le prénom, le matricule de l'utilisateur, ainsi que la plaque d'immatriculation et la Puissance_Fiscale du Véhicule concerné. 7. WHEN le Backend génère un Export_PDF (mensuel ou annuel), THE Backend SHALL inclure dans l'en-tête du document : le nom, le prénom, le matricule de l'utilisateur, ainsi que la plaque d'immatriculation et la Puissance_Fiscale du Véhicule concerné.
8. IF des Trajets de plusieurs Véhicules existent pour la période (mois ou année) demandée, THEN THE Backend SHALL générer un rapport PDF distinct par Véhicule. 8. IF des Trajets de plusieurs Véhicules existent pour la période (mois ou année) demandée, THEN THE Backend SHALL générer un rapport PDF distinct par Véhicule.
@@ -217,6 +218,9 @@ L'application permet de gérer un profil (identité, matricule, adresse de domic
11. WHEN le Backend génère un Export_PDF annuel (synthèse), THE Backend SHALL faire figurer : le nom et le prénom de l'utilisateur, l'adresse de domicile, le Bareme_Kilometrique applicable pour l'année, le Véhicule et sa puissance fiscale, la tranche applicable ; puis pour chacun des douze mois : les kilomètres parcourus, le Montant_Regle (frais payé) et le Frais_Reel_Mensuel (frais réel) ; et enfin les totaux : total kilométrique annuel, total réglé, Montant_Reel, et Regularisation (Montant_Reel − total réglé) ; le titre du document SHALL indiquer l'année couverte (ex. "Synthèse 2025 d'indemnité kilométrique"), et l'en-tête SHALL ne pas comporter de ligne distincte "Année" dès lors que l'année figure déjà dans ce titre. 11. WHEN le Backend génère un Export_PDF annuel (synthèse), THE Backend SHALL faire figurer : le nom et le prénom de l'utilisateur, l'adresse de domicile, le Bareme_Kilometrique applicable pour l'année, le Véhicule et sa puissance fiscale, la tranche applicable ; puis pour chacun des douze mois : les kilomètres parcourus, le Montant_Regle (frais payé) et le Frais_Reel_Mensuel (frais réel) ; et enfin les totaux : total kilométrique annuel, total réglé, Montant_Reel, et Regularisation (Montant_Reel − total réglé) ; le titre du document SHALL indiquer l'année couverte (ex. "Synthèse 2025 d'indemnité kilométrique"), et l'en-tête SHALL ne pas comporter de ligne distincte "Année" dès lors que l'année figure déjà dans ce titre.
12. WHEN le Backend génère un Export_PDF (mensuel ou annuel), THE Backend SHALL exprimer tous les kilomètres affichés dans le document (totaux d'en-tête, table des Trajets, table de synthèse annuelle, formule réelle) avec le nombre de décimales défini par Precision_Distance (voir spec `precision-distances-km`, Requirements 1, 3, 4, 5), au lieu d'une précision fixe à deux décimales. 12. WHEN le Backend génère un Export_PDF (mensuel ou annuel), THE Backend SHALL exprimer tous les kilomètres affichés dans le document (totaux d'en-tête, table des Trajets, table de synthèse annuelle, formule réelle) avec le nombre de décimales défini par Precision_Distance (voir spec `precision-distances-km`, Requirements 1, 3, 4, 5), au lieu d'une précision fixe à deux décimales.
13. WHEN le Backend génère un Export_PDF (mensuel ou annuel), THE Backend SHALL afficher le logo carré de l'Application en haut à gauche de l'en-tête du document. 13. WHEN le Backend génère un Export_PDF (mensuel ou annuel), THE Backend SHALL afficher le logo carré de l'Application en haut à gauche de l'en-tête du document.
14. WHEN le Backend génère un Export_PDF (mensuel ou annuel), THE Backend SHALL faire figurer en pied de page la Mention_Legale suivante : « 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. »
15. WHEN le Backend génère un Export_PDF mensuel, THE Backend SHALL faire figurer dans la liste des Trajets une colonne dédiée "A/R" affichant un marqueur lorsque le Trajet est un Aller_Retour et restant vide sinon, l'indicateur Aller_Retour n'étant plus signalé dans la cellule de l'itinéraire.
16. WHEN le Backend génère un Export_PDF mensuel, THE Backend SHALL présenter, pour chaque Trajet de la liste, l'adresse d'arrivée sur une ligne distincte sous l'adresse de départ, précédée du symbole "→".
--- ---
+20
View File
@@ -0,0 +1,20 @@
# Méthode de collaboration
## Valider l'approche avant de coder
Ne jamais commencer à écrire ou modifier du code applicatif tant que l'approche n'a pas été exposée et explicitement validée par l'utilisateur.
Pour toute évolution ou correction fonctionnelle/technique :
1. D'abord décrire l'orientation envisagée (structure, impacts, choix et alternatives), de façon concise.
2. Attendre la validation explicite de l'utilisateur. Le signal convenu est le mot « code » : tant qu'il n'est pas donné, rester en discussion et ne pas éditer de fichier de code.
3. Une fois « code » reçu, implémenter puis vérifier (build/tests).
## Exceptions (ne nécessitent pas le signal « code »)
- Les actions de configuration ou de documentation explicitement demandées par l'utilisateur (ex. ajout/mise à jour d'un fichier de steering, d'une spec, d'un README) quand la demande est sans ambiguïté.
- Les opérations de lecture, recherche, build, test ou diagnostic qui ne modifient pas le code applicatif.
## Lancement de l'environnement de développement
C'est l'utilisateur qui lance le serveur de développement (`./run-dev.sh`). Ne pas le démarrer automatiquement ; se contenter de signaler quand un redémarrage est nécessaire (ex. changement backend).
@@ -319,13 +319,7 @@ public class PdfService : IPdfService
page.Header().Element(c => ComposerEntete(c, modele, precision)); page.Header().Element(c => ComposerEntete(c, modele, precision));
page.Content().PaddingTop(10).Element(c => ComposerTableTrajets(c, modele, precision)); page.Content().PaddingTop(10).Element(c => ComposerTableTrajets(c, modele, precision));
page.Footer().AlignCenter().Text(txt => page.Footer().Element(ComposerPiedDePage);
{
txt.Span("Page ");
txt.CurrentPageNumber();
txt.Span(" / ");
txt.TotalPages();
});
}); });
}); });
@@ -351,24 +345,50 @@ public class PdfService : IPdfService
$"{modele.Vehicule.Plaque} — {modele.Vehicule.PuissanceFiscale} CV " + $"{modele.Vehicule.Plaque} — {modele.Vehicule.PuissanceFiscale} CV " +
$"(Coef. : {FormatDecimal(modele.Coefficient, 3)} / Base : {FormatDecimal(modele.Base, 0)})"; $"(Coef. : {FormatDecimal(modele.Coefficient, 3)} / Base : {FormatDecimal(modele.Base, 0)})";
container.Row(ligne => container.Column(entete =>
{ {
// Logo carré en haut à gauche. // Logo à gauche ; à droite, bloc titre (centré) puis bloc d'infos en dessous.
if (LogoCarre.Length > 0) 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); 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. // Identité (nom, prénom, matricule) — Exigence 7.8.
col.Item().PaddingTop(4).Text(txt => col.Item().Text(txt =>
{ {
txt.Span("Utilisateur : ").SemiBold(); txt.Span("Utilisateur : ").SemiBold();
txt.Span($"{identite} — Matricule : {matricule}"); txt.Span($"{identite} — Matricule : {matricule}");
@@ -418,10 +438,7 @@ public class PdfService : IPdfService
}); });
}); });
}); });
col.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Medium);
}); });
});
} }
/// <summary> /// <summary>
@@ -436,8 +453,9 @@ public class PdfService : IPdfService
{ {
columns.ConstantColumn(70); // Date columns.ConstantColumn(70); // Date
columns.RelativeColumn(2); // Motif (commentaire) columns.RelativeColumn(2); // Motif (commentaire)
columns.RelativeColumn(3); // Adresse départ → arrivée columns.RelativeColumn(4); // Adresse départ → arrivée (maximisée pour tenir sur 2 lignes)
columns.ConstantColumn(55); // Kms columns.ConstantColumn(22); // A/R (minimale)
columns.ConstantColumn(44); // Kms (jusqu'à « 9 999 » avec séparateur de milliers)
}); });
// En-tête de table. // En-tête de table.
@@ -446,33 +464,39 @@ public class PdfService : IPdfService
header.Cell().Element(CelluleEntete).Text("Date"); header.Cell().Element(CelluleEntete).Text("Date");
header.Cell().Element(CelluleEntete).Text("Motifs"); header.Cell().Element(CelluleEntete).Text("Motifs");
header.Cell().Element(CelluleEntete).Text("Départ → Arrivée"); 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"); header.Cell().Element(CelluleEntete).AlignRight().Text("Kms");
}); });
if (modele.Trajets.Count == 0) 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 else
{ {
foreach (var trajet in modele.Trajets) foreach (var trajet in modele.Trajets)
{ {
var motif = string.IsNullOrWhiteSpace(trajet.Commentaire) ? "—" : trajet.Commentaire!; 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; var km = trajet.Distance ?? 0m;
table.Cell().Element(Cellule).Text(trajet.Date.ToString("dd/MM/yyyy", FrCulture)); table.Cell().Element(Cellule).Text(trajet.Date.ToString("dd/MM/yyyy", FrCulture));
table.Cell().Element(Cellule).Text(motif); 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)); table.Cell().Element(Cellule).AlignRight().Text(FormatKm(km, precision));
} }
// Ligne de total du mois. // 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(); 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 => ComposerTableSyntheseAnnuelle(c, modele, precision));
col.Item().Element(c => ComposerTotauxAnnuels(c, modele, precision)); col.Item().Element(c => ComposerTotauxAnnuels(c, modele, precision));
}); });
page.Footer().AlignCenter().Text(txt => page.Footer().Element(ComposerPiedDePage);
{
txt.Span("Page ");
txt.CurrentPageNumber();
txt.Span(" / ");
txt.TotalPages();
});
}); });
}); });
@@ -531,7 +549,6 @@ public class PdfService : IPdfService
/// </summary> /// </summary>
private static void ComposerEnteteAnnuel(IContainer container, ModeleRapportAnnuel modele, int precision) private static void ComposerEnteteAnnuel(IContainer container, ModeleRapportAnnuel modele, int precision)
{ {
var reg = modele.Regularisation;
var identite = FormatIdentite(modele.Profil); var identite = FormatIdentite(modele.Profil);
var matricule = string.IsNullOrWhiteSpace(modele.Profil.Matricule) var matricule = string.IsNullOrWhiteSpace(modele.Profil.Matricule)
? "—" ? "—"
@@ -540,24 +557,52 @@ public class PdfService : IPdfService
? "—" ? "—"
: modele.Profil.AdresseDomicile!; : modele.Profil.AdresseDomicile!;
container.Row(ligne => container.Column(entete =>
{ {
// Logo carré en haut à gauche. // Logo à gauche ; à droite, bloc titre (centré) puis bloc d'infos en dessous.
if (LogoCarre.Length > 0) 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); 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. // Identité (nom, prénom, matricule) — Exigence 7.8.
col.Item().PaddingTop(4).Text(txt => col.Item().Text(txt =>
{ {
txt.Span("Utilisateur : ").SemiBold(); txt.Span("Utilisateur : ").SemiBold();
txt.Span($"{identite} — Matricule : {matricule}"); txt.Span($"{identite} — Matricule : {matricule}");
@@ -577,22 +622,22 @@ public class PdfService : IPdfService
txt.Span("Véhicule : ").SemiBold(); txt.Span("Véhicule : ").SemiBold();
txt.Span($"{reg.Plaque} — {reg.PuissanceFiscale} CV"); 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(); row.RelativeItem().Text(txt =>
txt.Span(reg.TrancheLibelle); {
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> /// <summary>
@@ -607,7 +652,7 @@ public class PdfService : IPdfService
col.Item().Text(txt => col.Item().Text(txt =>
{ {
txt.Span("Barème applicable pour l'année ").SemiBold(); 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) if (modele.TranchesBareme.Count == 0)
@@ -725,7 +770,8 @@ public class PdfService : IPdfService
txt.Span("Formule réelle : ").SemiBold(); txt.Span("Formule réelle : ").SemiBold();
txt.Span(formule); 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("Régularisation (réel − payé) : ").SemiBold();
txt.Span($"{FormatMontant(reg.Regularisation)} €"); 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) private static string FormatIdentite(ProfilDto profil)
{ {
var prenom = (profil.Prenom ?? string.Empty).Trim(); var prenom = (profil.Prenom ?? string.Empty).Trim();