Adapter rapports de rencontre au modele papier

This commit is contained in:
Daniel Allaire 2026-06-02 00:06:11 -04:00
parent e1b80793ee
commit 968079c671
3 changed files with 112 additions and 74 deletions

View file

@ -101,66 +101,91 @@ async def _html_rapport_rencontre(reunion: Reunion, membre, db: AsyncSession) ->
)
annonces = reunion.annonces or []
annonces_html = "".join(f"<li>{escape(a)}</li>" for a in annonces) or "<li>Aucune annonce.</li>"
annonces_texte = "\n".join(annonces)
collecte_texte = "Aucune collecte saisie."
if collecte:
detenteur = await db.get(Membre, collecte.detenteur_id) if collecte.detenteur_id else None
collecte_texte = (
f"{float(collecte.montant):.2f} $"
f"\nDétenteur: {detenteur.prenom if detenteur else '—'}"
f"\nStatut: {collecte.statut}"
)
ventes_lit_texte = "\n".join(
row.replace("<tr><td>", "")
.replace("</td><td>", " · ")
.replace("</td><td class=\"montant\">", " · ")
.replace("</td></tr>", "")
for row in ventes_lit_rows
)
jetons_texte = "\n".join(
row.replace("<tr><td>", "")
.replace("</td><td>", " · ")
.replace("</td></tr>", "")
for row in jetons_rows
)
ventes_jetons_texte = "\n".join(
row.replace("<tr><td>", "")
.replace("</td><td>", " · ")
.replace("</td><td class=\"montant\">", " · ")
.replace("</td></tr>", "")
for row in ventes_jetons_rows
)
notes_texte = "\n\n".join([texte for texte in [reunion.note, annonces_texte] if texte])
def bloc(label: str, contenu: str = "", cls: str = "") -> str:
return (
f"<div class=\"field {cls}\"><div class=\"box\">"
f"{escape(contenu or '')}</div><div class=\"label\">{escape(label)}</div></div>"
)
css = """
body { font-family: 'DejaVu Sans', sans-serif; font-size: 10.5pt; color: #27323a; margin: 1.6cm; }
.header { border-bottom: 3px solid #1a5276; padding-bottom: 12pt; margin-bottom: 18pt; }
.groupe { color: #1a5276; font-size: 20pt; font-weight: 800; }
.coords { color: #667; font-size: 9pt; margin-top: 3pt; }
h1 { font-size: 16pt; color: #1a5276; margin: 0 0 12pt; }
h2 { font-size: 12pt; color: #1a5276; margin: 16pt 0 6pt; border-bottom: 1px solid #d8e2ea; padding-bottom: 3pt; }
.meta { display: grid; grid-template-columns: 1fr 1fr; gap: 8pt; }
.box { border: 1px solid #d8e2ea; border-radius: 6pt; padding: 8pt; background: #f8fafc; }
.label { font-size: 8pt; color: #667; text-transform: uppercase; font-weight: 700; }
.value { font-size: 11pt; font-weight: 700; margin-top: 2pt; }
table { width: 100%; border-collapse: collapse; margin-top: 5pt; }
th { text-align: left; background: #1a5276; color: white; padding: 5pt; font-size: 8.5pt; }
td { border-bottom: 1px solid #e5e9ef; padding: 5pt; vertical-align: top; }
.montant { text-align: right; white-space: nowrap; }
.note { white-space: pre-wrap; }
.page-break { break-before: page; page-break-before: always; }
.footer { margin-top: 20pt; color: #889; font-size: 8pt; text-align: center; }
@page { size: letter; margin: 0.55in 0.65in; }
body { font-family: Arial, 'DejaVu Sans', sans-serif; color: #111; font-size: 10pt; }
.header { text-align: center; margin: 12pt 0 40pt; }
.groupe { font-size: 21pt; font-weight: 400; margin-bottom: 16pt; }
.district { font-size: 16pt; margin-bottom: 18pt; }
h1 { font-size: 14pt; font-weight: 500; letter-spacing: 0; margin: 0; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18pt 18pt; }
.field { break-inside: avoid; }
.field.full { grid-column: 1 / -1; }
.box {
border: 1pt solid #222;
min-height: 27pt;
padding: 6pt 8pt;
white-space: pre-wrap;
line-height: 1.25;
overflow: hidden;
}
.label { font-style: italic; font-size: 8.5pt; margin: 4pt 0 0 1pt; }
.short .box { min-height: 24pt; }
.theme .box { min-height: 58pt; }
.medium .box { min-height: 58pt; }
.deep .box { min-height: 95pt; }
.notes .box { min-height: 150pt; }
.footer { margin-top: 10pt; color: #777; font-size: 7.5pt; text-align: center; }
"""
groupe_nom = groupe.nom if groupe and groupe.nom else "Groupe"
coords = " · ".join([
item for item in [
groupe.adresse_local if groupe else None,
f"District {groupe.district}" if groupe and groupe.district else None,
] if item
])
ventes_lit_html = (
"<table><tr><th>Titre</th><th>Qté</th><th>Montant</th><th>Vendu par</th><th>Statut</th></tr>"
+ "".join(ventes_lit_rows) + "</table>"
if ventes_lit_rows else "<p>Aucune vente de littérature.</p>"
)
ventes_jetons_html = (
"<table><tr><th>Type</th><th>Qté</th><th>Montant</th><th>Vendu par</th><th>Statut</th></tr>"
+ "".join(ventes_jetons_rows) + "</table>"
if ventes_jetons_rows else "<p>Aucune vente de jetons.</p>"
)
jetons_html = (
"<table><tr><th>Type</th><th>Membre</th><th>Détail</th></tr>" + "".join(jetons_rows) + "</table>"
if jetons_rows else "<p>Aucun jeton/gâteau comptabilisé.</p>"
)
district = f"District {groupe.district}" if groupe and groupe.district else "District"
html = f"""<!doctype html><html><head><meta charset="utf-8"><style>{css}</style></head><body>
<div class="header">
<div class="groupe">{escape(groupe_nom)}</div>
<div class="coords">{escape(coords)}</div>
<div class="district">{escape(district)}</div>
<h1>RÉUNION HEBDOMADAIRE</h1>
</div>
<h1>Rapport de rencontre</h1>
<div class="meta">
<div class="box"><div class="label">Date</div><div class="value">{reunion.date_reunion.date().isoformat()}</div></div>
<div class="box"><div class="label">Heure</div><div class="value">{reunion.date_reunion.strftime('%H:%M')}</div></div>
<div class="box"><div class="label">Sujet</div><div class="value">{escape(reunion.theme or 'Rencontre hebdomadaire')}</div></div>
<div class="box"><div class="label">Animé par</div><div class="value">{escape(animateur.prenom if animateur else '—')}</div></div>
<div class="grid">
{bloc("DATE ( AAAA / MM / JJ)", reunion.date_reunion.date().isoformat(), "short")}
{bloc("PRÉSENCES", "", "short")}
{bloc("SALLE & CAFÉ", groupe.adresse_local if groupe else "", "short")}
{bloc("ANIMATION", animateur.prenom if animateur else "", "short")}
{bloc("THÈME DE LA DISCUSSION", reunion.theme or "", "full theme")}
{bloc("NOUVEAU(X)", "", "medium")}
{bloc("ANNIVERSAIRES / JETONS", jetons_texte, "medium")}
{bloc("COLLECTE", collecte_texte, "short")}
{bloc("DÉPENSES", "", "deep")}
{bloc("VENTES (Littérature)", ventes_lit_texte, "short")}
{bloc("VENTES (Jetons)", ventes_jetons_texte, "short")}
{bloc("NOTES", notes_texte, "full notes")}
</div>
<h2>Annonces</h2><ul>{annonces_html}</ul>
<h2>Notes</h2><div class="note">{escape(reunion.note or 'Aucune note.')}</div>
<h2>Collecte</h2>{collecte_html}
<div class="page-break"></div>
<h2>Ventes de littérature</h2>{ventes_lit_html}
<h2>Ventes de jetons</h2>{ventes_jetons_html}
<h2>Jetons/Gâteaux</h2>{jetons_html}
<div class="footer">Rapport généré automatiquement · {datetime.now().strftime('%Y-%m-%d %H:%M')}</div>
</body></html>"""
return html, jour

Binary file not shown.

View file

@ -20,36 +20,48 @@ const formulaires = [
},
];
const lignes = (n) => Array.from({ length: n }, (_, i) => `<div class="line">${i + 1}.</div>`).join('');
function htmlFormulaire(type, groupe) {
const nomGroupe = groupe?.nom || 'Groupe';
const commun = `
const district = groupe?.district ? `District ${groupe.district}` : 'District';
const lieu = groupe?.adresse_local || '';
const bloc = (label, contenu = '', cls = '') => `
<div class="field ${cls}">
<div class="box">${contenu}</div>
<div class="label">${label}</div>
</div>`;
const lignes = (n) => Array.from({ length: n }, (_, i) => `<div class="line">${i + 1}.</div>`).join('');
const entete = (titre) => `
<div class="head">
<h1>${nomGroupe}</h1>
<p>Formulaire papier de reprise en main</p>
</div>
<div class="meta">
<div>Date: ____________________</div>
<div>Responsable: ____________________</div>
<p>${district}</p>
<h2>${titre}</h2>
</div>`;
const sections = {
rencontre: `
<h2>Rencontre hebdomadaire</h2>
<label>Sujet / thème</label><div class="box"></div>
<label>Collecte</label><div class="grid"><div>Montant: __________</div><div>Détenteur: __________</div></div>
<label>Ventes littérature / jetons / gâteau</label>${lignes(6)}
<label>Remises de jetons / anniversaires</label>${lignes(6)}
<label>Notes</label><div class="big"></div>`,
${entete('RÉUNION HEBDOMADAIRE')}
<div class="form-grid">
${bloc('DATE ( AAAA / MM / JJ)', '', 'short')}
${bloc('PRÉSENCES', '', 'short')}
${bloc('SALLE & CAFÉ', lieu, 'short')}
${bloc('ANIMATION', '', 'short')}
${bloc('THÈME DE LA DISCUSSION', '', 'full theme')}
${bloc('NOUVEAU(X)', '', 'medium')}
${bloc('ANNIVERSAIRES / JETONS', '', 'medium')}
${bloc('COLLECTE', '', 'short')}
${bloc('DÉPENSES', '', 'deep')}
${bloc('VENTES (Littérature)', '', 'short')}
${bloc('VENTES (Jetons)', '', 'short')}
${bloc('NOTES', '', 'full notes')}
</div>`,
assemblee: `
<h2>Assemblée d'affaires</h2>
${entete("ASSEMBLÉE D'AFFAIRES")}
<label>Présences</label>${lignes(12)}
<label>Rapport du trésorier</label><div class="big"></div>
<label>Propositions / décisions</label>${lignes(10)}
<label>Postes à pourvoir</label>${lignes(6)}
<label>Suites à donner</label><div class="big"></div>`,
tresorerie: `
<h2>Trésorerie</h2>
${entete('TRÉSORERIE')}
<label>Encaisse / banque / réserves</label>${lignes(8)}
<label>Collectes et ventes reçues</label>${lignes(8)}
<label>Dépenses et contributions</label>${lignes(8)}
@ -57,13 +69,14 @@ function htmlFormulaire(type, groupe) {
};
return `<!doctype html><html lang="fr"><head><meta charset="utf-8"><title>${nomGroupe} - ${type}</title>
<style>
@page{size:letter;margin:0.6in} body{font-family:Arial,sans-serif;color:#111;line-height:1.35}
.head{border-bottom:2px solid #111;padding-bottom:10px;margin-bottom:14px}.head h1{margin:0;font-size:22px}.head p{margin:4px 0 0;color:#555}
h2{font-size:18px;margin:14px 0 10px}.meta{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
label{display:block;font-weight:bold;margin-top:14px}.box{height:42px;border:1px solid #999;margin-top:6px}.big{height:120px;border:1px solid #999;margin-top:6px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;border:1px solid #999;padding:10px;margin-top:6px}
@page{size:letter;margin:0.55in 0.65in} body{font-family:Arial,sans-serif;color:#111;line-height:1.35}
.head{text-align:center;margin:12pt 0 40pt}.head h1{margin:0 0 16pt;font-size:21pt;font-weight:400}.head p{margin:0 0 18pt;font-size:16pt}.head h2{font-size:14pt;font-weight:500;margin:0}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:18pt}
.field{break-inside:avoid}.field.full{grid-column:1 / -1}.box{min-height:27pt;border:1pt solid #222;padding:6pt 8pt;white-space:pre-wrap}.label{font-style:italic;font-size:8.5pt;margin:4pt 0 0 1pt}
.short .box{min-height:24pt}.theme .box{min-height:58pt}.medium .box{min-height:58pt}.deep .box{min-height:95pt}.notes .box{min-height:150pt}
label{display:block;font-weight:bold;margin-top:14px}.big{height:120px;border:1px solid #999;margin-top:6px}
.line{height:28px;border-bottom:1px solid #bbb;padding-top:6px}
</style></head><body>${commun}${sections[type]}<script>window.print()</script></body></html>`;
</style></head><body>${sections[type]}<script>window.print()</script></body></html>`;
}
export default function ModePapier() {