r"""Génère le manuel d'utilisation de l'application de la Ressourcerie.

Lancer depuis la racine du projet :
    C:\laragon\bin\python\python-3.13\python.exe docs/generer-manuel.py

Le contenu vit dans SECTIONS, plus bas. Modifier le texte là, puis relancer.
"""

from pathlib import Path

from reportlab.lib import colors
from reportlab.lib.enums import TA_JUSTIFY
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import (
    BaseDocTemplate,
    Frame,
    Image,
    KeepTogether,
    NextPageTemplate,
    PageBreak,
    PageTemplate,
    Paragraph,
    Spacer,
    Table,
    TableStyle,
)

RACINE = Path(__file__).resolve().parent.parent
SORTIE = RACINE / "docs" / "Manuel-utilisation.pdf"
LOGO = RACINE / "public" / "images" / "logo-ressourcerie.jpg"

# Couleurs du logo de la Ressourcerie, et l'ardoise d'If-Net pour la couverture.
BLEU = colors.HexColor("#007CBC")
BLEU_FONCE = colors.HexColor("#015C8C")
VERT = colors.HexColor("#76BE02")
ENCRE = colors.HexColor("#0F172A")
GRIS = colors.HexColor("#52607A")
TRAIT = colors.HexColor("#DFE3EE")
AMBRE_FOND = colors.HexColor("#FDF3E3")
AMBRE = colors.HexColor("#92400E")
NUIT = colors.HexColor("#0F172A")
CIEL = colors.HexColor("#38BDF8")

MARGE = 20 * mm


def styles():
    feuille = getSampleStyleSheet()
    base = feuille["Normal"]

    return {
        "corps": ParagraphStyle(
            "corps", parent=base, fontName="Helvetica", fontSize=10, leading=15.5,
            textColor=ENCRE, alignment=TA_JUSTIFY, spaceAfter=7,
        ),
        "intro": ParagraphStyle(
            "intro", parent=base, fontName="Helvetica", fontSize=10.5, leading=16,
            textColor=GRIS, alignment=TA_JUSTIFY, spaceAfter=10,
        ),
        "titre1": ParagraphStyle(
            "titre1", parent=base, fontName="Helvetica-Bold", fontSize=17, leading=21,
            textColor=BLEU_FONCE, spaceBefore=4, spaceAfter=10,
        ),
        "titre2": ParagraphStyle(
            "titre2", parent=base, fontName="Helvetica-Bold", fontSize=12, leading=16,
            textColor=ENCRE, spaceBefore=12, spaceAfter=5,
        ),
        "puce": ParagraphStyle(
            "puce", parent=base, fontName="Helvetica", fontSize=10, leading=15,
            textColor=ENCRE, leftIndent=11, bulletIndent=2, spaceAfter=4,
        ),
        "encadre": ParagraphStyle(
            "encadre", parent=base, fontName="Helvetica", fontSize=9.5, leading=14.5,
            textColor=AMBRE, alignment=TA_JUSTIFY,
        ),
        "couverture_titre": ParagraphStyle(
            "couverture_titre", parent=base, fontName="Helvetica-Bold", fontSize=26,
            leading=31, textColor=colors.white,
        ),
        "couverture_sous": ParagraphStyle(
            "couverture_sous", parent=base, fontName="Helvetica", fontSize=12,
            leading=18, textColor=colors.HexColor("#94A3B8"),
        ),
        "legende": ParagraphStyle(
            "legende", parent=base, fontName="Helvetica", fontSize=8.5, leading=12,
            textColor=GRIS,
        ),
    }


def couverture(canevas, doc):
    """Bandeau ardoise d'If-Net, logo de la Ressourcerie, titre."""
    largeur, hauteur = A4
    canevas.saveState()

    canevas.setFillColor(NUIT)
    canevas.rect(0, 0, largeur, hauteur, stroke=0, fill=1)

    # Le filet reprend les couleurs du site d'If-Net.
    canevas.setFillColor(colors.HexColor("#6366F1"))
    canevas.rect(0, hauteur - 116 * mm, largeur / 3, 2, stroke=0, fill=1)
    canevas.setFillColor(colors.HexColor("#A855F7"))
    canevas.rect(largeur / 3, hauteur - 116 * mm, largeur / 3, 2, stroke=0, fill=1)
    canevas.setFillColor(CIEL)
    canevas.rect(2 * largeur / 3, hauteur - 116 * mm, largeur / 3, 2, stroke=0, fill=1)

    canevas.setFont("Helvetica-Bold", 13)
    canevas.setFillColor(colors.white)
    canevas.drawString(MARGE, hauteur - 25 * mm, "IF-NET")
    # Le point d'accent se pose après le mot, dont on mesure la largeur plutôt
    # que de la deviner.
    canevas.setFillColor(CIEL)
    canevas.drawString(
        MARGE + canevas.stringWidth("IF-NET", "Helvetica-Bold", 13),
        hauteur - 25 * mm,
        ".",
    )

    canevas.setFont("Helvetica", 8.5)
    canevas.setFillColor(colors.HexColor("#94A3B8"))
    canevas.drawRightString(largeur - MARGE, hauteur - 25 * mm, "MANUEL D'UTILISATION")

    if LOGO.exists():
        canevas.drawImage(
            str(LOGO), MARGE, hauteur - 100 * mm,
            width=largeur - 2 * MARGE, height=(largeur - 2 * MARGE) * 744 / 2125,
            preserveAspectRatio=True, anchor="sw", mask=None,
        )

    canevas.setFont("Helvetica-Bold", 25)
    canevas.setFillColor(colors.white)
    canevas.drawString(MARGE, hauteur - 140 * mm, "Manuel d'utilisation")

    canevas.setFont("Helvetica", 13)
    canevas.setFillColor(colors.HexColor("#CBD5E1"))
    canevas.drawString(MARGE, hauteur - 150 * mm, "Application de gestion de La Ressourcerie du Calaisis")

    canevas.setFont("Helvetica", 10)
    canevas.setFillColor(colors.HexColor("#94A3B8"))
    canevas.drawString(MARGE, 32 * mm, "Collectes, livraisons, meubles à emporter et tournées")
    canevas.drawString(MARGE, 26 * mm, "Version du 10 septembre 2026")
    canevas.drawString(MARGE, 20 * mm, "Réalisé par If-Net pour FACE VALO")

    canevas.restoreState()


def page_courante(canevas, doc):
    """Bandeau de tete discret et numero de page."""
    largeur, hauteur = A4
    canevas.saveState()

    canevas.setStrokeColor(TRAIT)
    canevas.setLineWidth(0.6)
    canevas.line(MARGE, hauteur - 15 * mm, largeur - MARGE, hauteur - 15 * mm)

    canevas.setFont("Helvetica", 8)
    canevas.setFillColor(GRIS)
    canevas.drawString(MARGE, hauteur - 13 * mm, "La Ressourcerie du Calaisis — Manuel d'utilisation")
    canevas.drawRightString(largeur - MARGE, hauteur - 13 * mm, "If-Net")

    canevas.setFillColor(BLEU)
    canevas.rect(MARGE, 16 * mm, 10 * mm, 1.5, stroke=0, fill=1)

    canevas.setFont("Helvetica", 8.5)
    canevas.setFillColor(GRIS)
    canevas.drawRightString(largeur - MARGE, 15 * mm, str(canevas.getPageNumber() - 1))

    canevas.restoreState()


# --- Contenu du manuel ---------------------------------------------------
# Chaque section : (titre, [blocs]). Un bloc est un tuple dont le premier
# élément dit sa nature : 'p' paragraphe, 'i' introduction, 'st' sous-titre,
# 'l' liste à puces, 'e' encadré, 'r' tableau.

SECTIONS = [
    ("Avant de commencer", [
        ("i", "Ce manuel s'adresse aux trois façons de se servir de l'application : "
              "depuis le magasin, depuis le camion, et depuis le bureau. Chacun ne voit "
              "que ce qui le concerne, et c'est voulu."),
        ("st", "Se connecter"),
        ("p", "L'adresse de l'application ouvre une page d'accueil publique : un "
              "donateur y trouve le formulaire de don, une personne qui veut aider celui "
              "des bénévoles, et les salariés le bouton <b>Espace salariés</b>. C'est "
              "cette adresse qu'il faut diffuser au public."),
        ("p", "L'application s'ouvre dans un navigateur, sur ordinateur comme sur "
              "téléphone. Rien à installer. Chacun entre son adresse e-mail et son mot "
              "de passe. Sur un téléphone personnel, cocher <b>Rester connecté sur cet "
              "appareil</b> évite de retaper le mot de passe à chaque fois."),
        ("st", "Qui voit quoi"),
        ("r", [
            ["Rôle", "Ce qu'il voit"],
            ["Magasin", "Les meubles à emporter en cours, et le formulaire de nouvelle "
                        "livraison. Après une vente, la confirmation de cette vente-là "
                        "uniquement."],
            ["Logistique", "La tournée du jour et ses arrêts. Ni prix, ni fichier clients. "
                           "Le lendemain, les arrêts de la veille ne s'ouvrent plus."],
            ["Manager", "Tout le suivi : demandes, planning, dossiers, archives, envois, "
                        "et les réglages de l'application."],
            ["Administrateur", "Tout, y compris les pôles d'activité et la création des "
                               "comptes."],
        ]),
        ("e", "Ces limites ne sont pas de simples menus cachés. Un lien direct conservé "
              "sur un téléphone ne rouvre rien : le refus vient du serveur."),
        ("st", "Se repérer"),
        ("p", "Pour un manager, la barre du haut suit les domaines de la Ressourcerie : "
              "<b>Aujourd'hui</b>, <b>Magasin</b>, <b>Logistique</b>, <b>Planning</b> et "
              "<b>Bénévoles</b>. Elle ne montre que les domaines de vos pôles."),
        ("p", "<b>Aujourd'hui</b> rassemble ce qui demande un geste, les tournées du jour, "
              "puis tous les écrans en tuiles, suivi et administration compris."),
        ("st", "Comptes partagés"),
        ("p", "Les comptes magasin et logistique sont partagés par plusieurs salariés. "
              "C'est pourquoi l'application demande votre prénom au moment des gestes qui "
              "comptent : un retrait, un résultat de tournée. Il n'est jamais obligatoire, "
              "mais il permet de savoir qui a fait quoi."),
        ("p", "Au départ d'un salarié, un manager coupe les connexions ouvertes sur le "
              "compte depuis <b>Administration</b>, puis <b>Comptes</b>. Les téléphones "
              "concernés redemandent alors une connexion."),
    ]),

    ("Le magasin", [
        ("i", "Deux gestes quotidiens : enregistrer une livraison au moment de la vente, "
              "et remettre au client un meuble qu'il vient chercher."),
        ("st", "Enregistrer une livraison"),
        ("p", "Menu <b>Nouvelle livraison</b>. Le formulaire suit l'ordre de la "
              "conversation avec le client : ses coordonnées, l'adresse, la prestation, "
              "l'accès au logement, le chargement, les meubles, puis le créneau."),
        ("l", [
            "<b>Le secteur se choisit à la main</b>, d'après la commune du client. "
            "L'application ne le devine pas à partir d'une distance.",
            "<b>Le montant s'affiche pendant la saisie</b>, pour que vous puissiez "
            "l'annoncer. Il se recalcule dès que vous changez le secteur, la prestation "
            "ou le nombre d'armoires.",
            "<b>Le remontage n'est jamais compté d'office.</b> Ne renseignez le nombre "
            "d'armoires ou de penderies que si le client le demande.",
            "<b>Une photo par meuble</b> aide l'équipe à charger le bon meuble. Le "
            "téléphone propose directement l'appareil photo.",
        ]),
        ("p", "Si l'adresse ne rentre dans aucun secteur, cochez <b>Adresse hors "
              "grille</b>. Le dossier part alors sans montant, n'encaissez rien, et un "
              "manager fixera le prix."),
        ("st", "Le créneau"),
        ("p", "Seules les tournées confirmées par un manager sont proposées. Choisissez "
              "celle qui convient au client et annoncez-lui la date. Si deux personnes "
              "prennent la dernière place au même instant, l'une des deux reçoit un "
              "message et rien n'est enregistré à moitié : il suffit de choisir un autre "
              "créneau."),
        ("p", "Quand aucun créneau n'est ouvert, indiquez la date souhaitée par le "
              "client. Un manager la placera au planning."),
        ("e", "Le règlement se fait à l'achat, en totalité. Rien n'est jamais encaissé "
              "le jour de la livraison, même si le client a demandé un service en plus."),
        ("st", "Après la vente"),
        ("p", "L'écran de confirmation reprend le numéro de dossier, les meubles et le "
              "montant. Communiquez le numéro au client. Cet écran ne se rouvre pas "
              "ensuite depuis le compte magasin : c'est un manager qui retrouve le "
              "dossier si besoin."),
    ]),

    ("Les meubles à emporter", [
        ("i", "Un client achète un meuble et revient le chercher. La fiche de retrait "
              "tient le compte de ce qui reste à remettre, et calcule la date limite."),
        ("st", "Créer une fiche"),
        ("p", "Menu <b>Meubles à emporter</b>, puis <b>Nouvelle fiche</b>. Le haut de "
              "l'écran annonce la date limite que donnerait un achat du jour : vous "
              "pouvez l'annoncer au client avant même d'avoir fini la saisie."),
        ("p", "Ajoutez un article par meuble, avec son emplacement en réserve et une "
              "photo. Si le client n'a pas d'adresse e-mail, décochez l'envoi du rappel : "
              "le suivi restera manuel, et aucun message ne partira dans le vide."),
        ("st", "La date limite"),
        ("p", "Elle se calcule en jours d'ouverture, pas en jours de calendrier. Trois "
              "jours d'ouverture après le jour de l'achat, le jour de l'achat non compté, "
              "et l'échéance tombe à la fermeture du troisième."),
        ("r", [
            ["Achat le mardi", "Le magasin ouvre mardi, mercredi, vendredi, samedi"],
            ["Mercredi", "jour 1"],
            ["Jeudi", "fermé, ne compte pas"],
            ["Vendredi", "jour 2"],
            ["Samedi", "jour 3, échéance à 17h30"],
        ]),
        ("e", "Une fermeture exceptionnelle décale l'échéance d'autant. Il n'y a donc "
              "pas de règle simple du type « plus six jours » : laissez l'application "
              "calculer."),
        ("st", "Remettre un meuble"),
        ("p", "Ouvrez la fiche, cochez les articles que le client emporte, indiquez "
              "votre prénom, puis enregistrez. Si le client ne prend qu'une partie, "
              "cochez seulement ce qu'il emporte : le dossier reste ouvert pour le "
              "reste, et le rappel ne réclamera que ce qui manque."),
        ("p", "Quand tout est remis, le dossier sort de votre liste. Il reste consultable "
              "par les managers."),
        ("st", "Retards"),
        ("p", "Une fiche dont la date est dépassée s'affiche en orange. Rien n'est remis "
              "en vente et rien n'est supprimé automatiquement : la décision revient aux "
              "managers, qui reçoivent une alerte."),
    ]),

    ("L'équipe sur le terrain", [
        ("i", "Tout se passe sur le téléphone, sans bon papier. L'écran ne montre que la "
              "journée en cours."),
        ("st", "La tournée du jour"),
        ("p", "À la connexion, la liste des arrêts s'affiche dans l'ordre, avec le "
              "véhicule, l'équipe et les consignes du manager. Touchez un arrêt pour "
              "l'ouvrir."),
        ("st", "Avant de partir"),
        ("p", "Pour une livraison, la <b>checklist de chargement</b> se coche au dépôt, "
              "meuble par meuble. L'écran indique combien il en reste à charger. C'est ce "
              "qui évite de repartir sans un meuble et de refaire le trajet."),
        ("st", "Sur place"),
        ("l", [
            "<b>Appeler</b> et <b>Itinéraire</b> ouvrent directement le téléphone et la "
            "carte.",
            "Les contraintes d'accès sont rappelées : étage, ascenseur, escaliers, "
            "passages étroits, stationnement.",
            "Pour une collecte, seuls les objets retenus par le manager figurent. Si un "
            "objet ne correspond pas à ce qui était annoncé, notez-le dans le "
            "commentaire.",
        ]),
        ("st", "Enregistrer le résultat"),
        ("p", "Indiquez les quantités réellement chargées ou livrées, puis le résultat : "
              "réalisée, partiellement réalisée, client absent, refus sur place, non "
              "réalisée. Ce que vous écrivez dans <b>Problème rencontré</b> remonte aux "
              "managers."),
        ("st", "Faire signer"),
        ("p", "Présentez le récapitulatif au client, puis faites signer au doigt dans le "
              "cadre. La signature clôt l'arrêt. Le bon est alors figé tel qu'il a été "
              "présenté et ne se réécrit plus."),
        ("p", "Si le client refuse de signer ou n'est pas là, dépliez <b>La signature est "
              "impossible ou refusée</b> et indiquez le motif. L'arrêt se clôt quand même, "
              "et les managers en sont informés."),
        ("e", "Aucun prix n'apparaît sur ces écrans, et rien n'est à encaisser. Si un "
              "client vous parle d'argent, renvoyez-le vers le magasin."),
    ]),

    ("Le planning des effectifs", [
        ("i", "Qui travaille, quand, dans chaque pôle. Il change souvent : le tenir ici évite "
              "de le réimprimer à chaque retouche."),
        ("st", "Tous les salariés, pôle par pôle"),
        ("p", "Le planning couvre la logistique comme le magasin. Chaque salarié appartient "
              "à un pôle, et chaque manager ne voit et ne remplit que les salariés des "
              "siens. Un manager de plusieurs pôles voit ses équipes séparées."),
        ("p", "Il ne valide aucune intervention : dire qui travaille un jour ne compose "
              "aucune tournée. Les salariés se gèrent depuis <b>Planning</b>, puis "
              "<b>Salariés</b>."),
        ("st", "Deux vues, deux onglets"),
        ("p", "Le planning se tient au mois. La vue <b>semaine</b> montre la même chose en "
              "plus grand, pour l'afficher ou le lire d'un coup d'œil."),
        ("p", "Un salarié qui a son accès dispose de deux onglets : <b>l'équipe</b>, pour "
              "voir qui est là dans son pôle, et <b>mon planning</b>, pour ne voir que le sien."),
        ("st", "Les journées et leurs horaires"),
        ("p", "Une journée est d'un type, et le type porte ses horaires. Chaque pôle a les "
              "siens : on ne propose pas au magasin les horaires du camion."),
        ("l", [
            "<b>Logistique</b> : journée de travail 9 h - 12 h et 13 h - 16 h 30 ; "
            "encombrants 8 h - 12 h et 13 h - 15 h 30 ; encombrants en journée continue "
            "8 h - 15 h.",
            "<b>Magasin</b> : journée au magasin, dont les horaires restent à renseigner.",
            "<b>Pour tous</b> : repos, congés, formation, absence, rendez-vous "
            "socio-professionnel.",
        ]),
        ("p", "Les horaires se règlent depuis <b>Planning</b>, puis <b>Journées et "
              "horaires</b>. Laissez l'après-midi vide pour une journée continue. Un manager "
              "règle les journées de ses pôles ; une journée commune à tous ne se modifie que "
              "par l'administrateur."),
        ("st", "Ce que les collègues voient"),
        ("p", "Certains motifs ne regardent pas l'équipe. Un congé, une absence ou un "
              "rendez-vous avec la référente socio-professionnelle s'affichent "
              "<b>Absent</b> pour les collègues. Seuls la personne concernée et les "
              "managers en lisent le motif."),
        ("st", "Modifier"),
        ("p", "Un manager choisit le type dans la case, pour toute la période affichée, et "
              "enregistre en une fois. Une case laissée vide efface la journée."),
        ("e", "Le salarié de la logistique voit sa journée du jour rappelée en tête de sa "
              "tournée, avec ses horaires."),
    ]),

    ("Le bureau : les collectes", [
        ("i", "Un donateur remplit un formulaire depuis chez lui, ou avec vous au "
              "téléphone. Vous décidez objet par objet d'après les photos, et vous "
              "donnez la date dans le même geste."),
        ("st", "La demande"),
        ("p", "Le formulaire est accessible sans compte, par un lien ou un QR code. Le "
              "donateur décrit ses objets, joint des photos et propose jusqu'à trois "
              "dates. Seul l'essentiel est affiché ; les détails d'accès et de dimensions "
              "se déplient avec <b>Préciser</b>, quand il y a quelque chose à dire."),
        ("e", "Le formulaire reste fermé au public tant que le texte des conditions de "
              "collecte n'est pas saisi dans les réglages. Faire cocher des conditions "
              "vides n'aurait aucune valeur."),
        ("st", "Examiner les objets"),
        ("p", "Ouvrez une demande depuis <b>Collectes</b>. Chaque objet a ses photos et "
              "son propre choix, retenu ou refusé, avec un motif si vous refusez."),
        ("st", "Valider et dater"),
        ("p", "Le bouton <b>Valider la demande</b> reste inactif tant qu'un objet n'a pas "
              "été tranché. La décision d'ensemble s'en déduit : tout retenu, une partie, "
              "ou rien du tout."),
        ("p", "Au-dessus du bouton, renseignez le <b>jour</b> et la <b>demi-journée</b>. "
              "Les dates proposées par le donateur sont rappelées juste en dessous. Aucune "
              "tournée ni aucun véhicule n'est nécessaire : la date est fixée, et le "
              "donateur reçoit aussitôt son récapitulatif."),
        ("p", "La date reste facultative. Vous pouvez valider maintenant et dater plus "
              "tard, depuis le bloc <b>Rendez-vous</b> de la même fiche."),
        ("st", "Ce que le donateur reçoit"),
        ("r", [
            ["Votre geste", "Son message"],
            ["Il manque des informations", "La liste de ce qui manque, telle que vous "
                                           "l'avez écrite. Elle est obligatoire."],
            ["Aucun objet retenu", "Un refus, avec le motif que vous avez donné."],
            ["Acceptée, sans date", "Les objets qui seront repris, et l'annonce d'une date à venir."],
            ["Date fixée", "Le récapitulatif : jour, demi-journée, plage horaire, objets."],
        ]),
        ("e", "Si la date arrive avant que l'annonce d'acceptation soit partie, seule la "
              "date est envoyée : deux messages pour une nouvelle, c'est un de trop."),
        ("st", "Et la logistique ?"),
        ("p", "Une collecte datée apparaît parmi les dossiers à placer, à sa date. La "
              "tournée qui la reçoit doit tomber ce jour-là, à cette demi-journée : "
              "l'application refuse une autre tournée, pour ne pas faire mentir le "
              "récapitulatif du donateur."),
        ("p", "<b>Aujourd'hui</b> signale les collectes datées qui ne sont sur aucune "
              "tournée, en rouge à deux jours de l'échéance."),
    ]),

    ("La logistique : les tournées", [
        ("i", "Collectes, livraisons et encombrants au même endroit, pour lire la charge "
              "d'un coup d'œil. Les collectes arrivent déjà datées depuis leur fiche."),
        ("st", "Créer une tournée"),
        ("p", "Sur la semaine affichée, cliquez <b>Ajouter une tournée</b> au jour voulu. "
              "Choisissez la demi-journée et l'activité. Le véhicule et les équipiers "
              "peuvent attendre : seuls les véhicules libres sont proposés, et seuls les "
              "salariés de la logistique non affectés ailleurs."),
        ("p", "Le <b>nombre de places</b> est facultatif. Laissé vide, la tournée accepte "
              "autant d'arrêts que vous y mettez. Renseigné, il plafonne les réservations "
              "prises depuis le magasin."),
        ("e", "Une tournée en brouillon n'est pas proposée au magasin. Passez-la en "
              "<b>Confirmée</b> quand elle est prête à recevoir des réservations."),
        ("st", "Composer les arrêts"),
        ("p", "Sur la fiche de la tournée, ajoutez les dossiers en attente, puis "
              "numérotez les arrêts dans l'ordre où l'équipe les fera. Retirer un dossier "
              "ne le perd pas : il repasse en attente de place."),
        ("p", "Déplacer une tournée entraîne avec elle tous les dossiers placés, et "
              "chaque client déjà confirmé reçoit un récapitulatif à jour."),
        ("st", "Après la tournée"),
        ("p", "Les arrêts clôturés partent dans <b>Archives</b>, avec le bon signé. Une "
              "intervention inachevée remonte sur l'accueil, dans <b>Interventions à "
              "reprogrammer</b>, jusqu'à ce qu'un nouveau passage soit placé."),
    ]),

    ("Le bureau : archives et envois", [
        ("st", "Les bons signés"),
        ("p", "Menu <b>Archives</b>. Chaque arrêt clôturé garde le bon tel qu'il a été "
              "présenté au client. Si le dossier change après coup, le bon ne bouge pas."),
        ("p", "Pour corriger une erreur constatée plus tard, ajoutez une correction en bas "
              "du bon. Elle s'inscrit à côté, avec votre nom et la date, sans réécrire ce "
              "que le client a signé."),
        ("e", "Le bon signé reste interne. Il n'est jamais envoyé au client, qui reçoit "
              "seulement son récapitulatif."),
        ("st", "Suivre les envois"),
        ("p", "Menu <b>Envois</b>. Chaque message garde son état : en attente, envoyé, "
              "annulé, en échec. Un message annulé indique pourquoi, par exemple parce que "
              "le client est venu chercher son meuble avant le rappel."),
        ("p", "Un échec conserve sa raison. Après cinq tentatives, l'application cesse de "
              "réessayer et vous attend, plutôt que d'expédier une salve de messages "
              "identiques. Le bouton <b>Réessayer maintenant</b> relance à la main."),
        ("st", "Satisfaction"),
        ("p", "Les réponses au questionnaire arrivent dans <b>Administration</b>, puis "
              "<b>Satisfaction</b>, avec la moyenne et les commentaires."),
    ]),

    ("Les bénévoles", [
        ("i", "Une personne propose de donner de son temps. Le premier appel compte : "
              "rappelée trois semaines plus tard, elle est souvent partie ailleurs."),
        ("st", "Le formulaire public"),
        ("p", "Il se remplit sans compte, à l'adresse que donne le bouton <b>Voir le "
              "formulaire public</b>. La personne indique qui elle est, les demi-journées "
              "où elle est libre et l'activité qui lui plairait. Elle doit accepter qu'on "
              "garde ses coordonnées pour la recontacter."),
        ("p", "Elle reçoit un accusé de réception si elle a laissé une adresse e-mail, et "
              "les managers du pôle choisi une alerte sous la cloche."),
        ("st", "Le suivi"),
        ("p", "Menu <b>Bénévoles</b>. La liste montre par défaut ce qui reste à faire. "
              "Sur la fiche, faites avancer la candidature :"),
        ("l", [
            "<b>Nouvelle</b>, puis <b>À contacter</b> et <b>Rencontre prévue</b> ;",
            "<b>Bénévole accueilli</b>, <b>Non retenue</b> ou <b>Désistement</b>, qui "
            "ferment le dossier.",
        ]),
        ("p", "Les notes internes ne sont jamais communiquées à la personne. Si la "
              "candidature relève d'une autre équipe, changez son pôle : seuls les managers "
              "de ce pôle la verront."),
        ("e", "C'est une première version. Les champs du formulaire évolueront avec vos "
              "retours."),
    ]),

    ("Les encombrants", [
        ("i", "Enlèvement pour la collectivité, à ne pas confondre avec une collecte chez "
              "un donateur : rien n'est examiné sur photo, rien n'est facturé, et le "
              "ramassage se fait sur le trottoir. Cette activité relève du pôle Logistique."),
        ("st", "Importer une feuille de route"),
        ("p", "Menu <b>Encombrants</b>, puis <b>Importer une feuille</b>. Déposez le fichier "
              "envoyé par le service de la ville, enregistré au format CSV depuis Excel. "
              "Rien n'est créé à l'envoi."),
        ("p", "Le lecteur accepte ce qu'un export français produit vraiment : le "
              "point-virgule comme séparateur, les accents d'Excel, un code postal dont le "
              "zéro initial a sauté, et des intitulés de colonnes approximatifs. Seule "
              "l'adresse est indispensable."),
        ("p", "La feuille au format PDF, telle que le service de la ville l'envoie, est "
              "lue aussi. Son tableau est relu colonne par colonne, et non à plat, pour "
              "qu'une adresse ne se retrouve pas collée à une date. Le titre et la ligne "
              "d'en-tête qui reviennent en haut de chaque page sont écartés : une tournée "
              "d'encombrants tient rarement sur une seule page."),
        ("e", "Un PDF scanné ne contient pas de texte : il est refusé, avec l'explication. "
              "Demandez alors le fichier d'origine au service qui l'envoie."),
        ("st", "Vérifier avant d'importer"),
        ("p", "Chaque adresse est cherchée dans la Base Adresse Nationale. Celles qui ne "
              "sont pas retrouvées, ou mal, apparaissent en orange : c'est presque toujours "
              "une faute de frappe de la collectivité. Corrigez-les à l'écran plutôt que de "
              "faire tourner un camion pour rien."),
        ("l", [
            "Les lignes <b>déjà connues</b> sont décochées d'office, pour éviter un second "
            "passage au même endroit.",
            "Décochez ce qui ne doit pas entrer, corrigez ce qui doit l'être, puis importez.",
            "Tant que vous n'avez pas confirmé, rien n'est enregistré.",
        ]),
        ("e", "Vous pouvez aussi saisir une demande reçue au coup par coup, sans passer par "
              "un fichier."),
        ("st", "Placer les enlèvements"),
        ("p", "Créez au planning une tournée de type <b>Encombrants</b>, puis ajoutez-y les "
              "demandes en attente. Elles y figurent avec leur commune, et celles dont "
              "l'adresse reste douteuse sont signalées jusque dans la liste de choix."),
        ("st", "Ordonner la tournée"),
        ("p", "L'ordre des adresses sur la feuille ne veut rien dire : la collectivité les "
              "saisit au fil des appels. Le bouton <b>Ordonner sur la carte</b>, sur la "
              "tournée, range les arrêts pour limiter la conduite, et annonce le temps et "
              "le kilométrage estimés. L'import d'une feuille le fait déjà tout seul."),
        ("l", [
            "Un arrêt <b>déjà terminé</b> garde sa place en tête : il est derrière l'équipe.",
            "Une adresse <b>non localisée</b> part en fin de tournée, signalée en orange : "
            "corrigez-la, puis relancez le calcul.",
            "L'ordre proposé n'est qu'une proposition : vous le remaniez juste en dessous, "
            "arrêt par arrêt.",
        ]),
        ("e", "Le point de départ est l'adresse du dépôt, à renseigner une fois dans "
              "<b>Administration &rsaquo; Réglages &rsaquo; Tournées</b>. Sans elle, le "
              "calcul part du premier arrêt."),
        ("st", "Sur le terrain"),
        ("p", "L'équipe retrouve ces arrêts dans sa tournée du jour, au même endroit que les "
              "collectes et les livraisons. Trois choses y changent : les résultats "
              "proposés, dont <b>Rien sur place</b>, l'absence de checklist de chargement, "
              "et l'absence de liste d'objets à cocher."),
        ("e", "Sur voirie, il n'y a souvent personne à faire signer. L'équipe utilise alors "
              "« la signature est impossible » : l'arrêt se clôt quand même, avec son motif."),
        ("st", "Quand le camion est plein"),
        ("p", "Le camion se remplit avant la fin de la liste. L'équipe appuie sur "
              "<b>Camion plein — on part vider</b>, va aux bennes, puis sur "
              "<b>C'est reparti</b> en revenant. Autant de fois que nécessaire dans la "
              "journée."),
        ("p", "Ces allers-retours apparaissent sur la tournée, côté manager, avec leur "
              "heure et leur durée. Sans eux, une matinée à trois rotations ressemble à "
              "une matinée lente."),
        ("e", "L'adresse des bennes s'affiche sous le bouton, si elle est renseignée dans "
              "les réglages."),
    ]),

    ("L'administration", [
        ("i", "Tout ce qui suit se change sans appeler personne. C'est le principe : "
              "aucun texte ni aucun délai n'est écrit en dur dans l'application."),
        ("st", "Réglages"),
        ("p", "Les délais annoncés, les plages horaires, les textes affichés aux clients "
              "et le contenu de chaque message automatique. Une valeur non renseignée est "
              "signalée en orange."),
        ("p", "Dans les modèles de messages, les mots entre accolades sont remplacés à "
              "l'envoi. La liste des mots disponibles figure sous chaque champ. Un mot "
              "inconnu reste tel quel, ce qui rend une faute de frappe visible."),
        ("st", "Calendrier"),
        ("p", "Les jours et horaires d'ouverture, et les fermetures exceptionnelles. "
              "L'écran affiche la date limite que donnerait un achat du jour, pour que "
              "vous voyiez tout de suite l'effet de ce que vous changez."),
        ("e", "Modifier le calendrier ne recalcule aucune date déjà annoncée à un client. "
              "Pour en déplacer une, passez par la prolongation depuis la fiche de "
              "retrait : elle garde une trace de l'échéance d'origine."),
        ("st", "Secteurs et tarifs"),
        ("p", "Un tarif ne se corrige pas : vous en publiez un nouveau, avec sa date "
              "d'effet. Les dossiers déjà chiffrés gardent leur prix, et vous saurez plus "
              "tard quel tarif s'appliquait à quelle date."),
        ("p", "Rattachez les communes à leur secteur. C'est ce qui évite au magasin de se "
              "tromper de couronne."),
        ("st", "Véhicules et équipiers"),
        ("p", "Un véhicule déclaré indisponible n'est plus proposé sur ces dates. Un "
              "véhicule sorti du parc, ou un équipier qui quitte les équipes, n'est pas "
              "supprimé : les tournées passées doivent continuer à dire avec quoi et avec "
              "qui elles ont été faites."),
        ("st", "Comptes"),
        ("p", "Un compte par manager et un compte par salarié, plus un compte partagé par "
              "branche d'activité, qui sert aux nouveaux arrivants le temps qu'ils aient "
              "le leur."),
        ("p", "Créez, désactivez, changez un mot de passe. <b>Couper les connexions</b> "
              "est distinct de la désactivation : c'est le geste au départ d'un salarié "
              "sur un compte partagé. Changer un mot de passe les coupe aussi."),
        ("p", "Le dernier manager actif ne peut pas être désactivé, et personne ne peut "
              "se désactiver soi-même."),
    ]),

    ("Les alertes", [
        ("i", "Ce qu'un manager doit savoir sans avoir à le chercher, sur deux canaux : "
              "un courriel, et une alerte dans l'application."),
        ("st", "La cloche"),
        ("p", "En haut à droite de chaque écran. La pastille rouge compte ce que vous "
              "n'avez pas encore lu, et se met à jour toute seule, sans recharger la "
              "page."),
        ("st", "Ce qui déclenche une alerte"),
        ("l", [
            "Un <b>bon signé</b> sur le terrain.",
            "Un <b>retrait dont la date limite est dépassée</b>. Rien n'est remis en vente "
            "pour autant.",
            "Une <b>livraison hors grille</b> : le dossier est parti sans montant, rien "
            "n'a été encaissé, et il attend son prix.",
            "Un <b>problème signalé</b> par l'équipe sur un arrêt.",
            "Un <b>message qui n'a pas pu partir</b> après plusieurs tentatives.",
        ]),
        ("st", "Le son"),
        ("p", "Un signal sonore accompagne chaque alerte nouvelle, avec une courte "
              "vibration sur les téléphones qui la gèrent. Le bouton <b>Son activé</b>, "
              "sur l'écran des alertes, le coupe pour cet appareil seulement : votre "
              "téléphone en tournée et le poste du bureau se règlent séparément."),
        ("e", "Le son ne peut retentir qu'après votre premier appui sur l'écran : c'est "
              "une règle des navigateurs mobiles, qu'aucune application web ne contourne."),
        ("st", "Qui voit quoi"),
        ("p", "Une alerte reste dans son pôle, et le courriel aussi. Chacun la lit pour "
              "son propre compte : la marquer lue ne l'efface pas chez vos collègues."),
        ("e", "Recevoir une notification quand l'application est fermée demande une "
              "installation sur l'écran d'accueil du téléphone. C'est possible, et cela "
              "reste à décider ensemble."),
    ]),

    ("Les pôles d'activité", [
        ("i", "La Ressourcerie ne se limite pas au magasin. Le pôle est la cloison qui "
              "permet à chaque secteur d'activité de travailler sans voir les dossiers "
              "des autres."),
        ("st", "Comment cela fonctionne"),
        ("p", "Chaque dossier appartient à un pôle. Chaque compte est rattaché à un ou "
              "plusieurs pôles, et ne voit que ceux-là. Un manager du magasin ne voit donc "
              "pas les dossiers de la boutique du centre commercial, et inversement."),
        ("p", "Seul l'administrateur voit tous les pôles, y compris ceux créés plus tard. "
              "C'est le compte qui garde la main quand une équipe se trompe de cloison."),
        ("st", "Créer un pôle"),
        ("p", "Menu <b>Administration</b>, puis <b>Pôles</b>, visible du seul "
              "administrateur. Un pôle a un nom, un code qui ne change plus, et une "
              "description."),
        ("e", "Un pôle qui porte des dossiers ne se désactive pas : ils deviendraient "
              "introuvables. Déplacez-les d'abord."),
        ("st", "Rattacher les comptes"),
        ("p", "Depuis <b>Comptes</b>, cochez les pôles de chaque compte. Un manager ne "
              "peut donner que les pôles dont il fait lui-même partie : il ne crée pas "
              "d'accès à une équipe qu'il ne suit pas. Un compte créé sans choix hérite "
              "des pôles de celui qui le crée."),
        ("st", "Pour la suite"),
        ("p", "Les pôles Vue d'en face, Encombrants, Aide au tech et Chiffons sont déjà "
              "posés, sans dossiers. Quand une nouvelle brique sera développée pour l'une "
              "de ces équipes, elle se rattachera à son pôle et sera invisible des autres, "
              "sans toucher à ce qui existe."),
    ]),

    ("Si quelque chose ne va pas", [
        ("r", [
            ["Ce que vous voyez", "Ce que cela veut dire"],
            ["Page non accessible avec ce compte",
             "Ce dossier appartient à un autre rôle ou à un autre pôle. Un manager ou "
             "l'administrateur peut le retrouver."],
            ["Session expirée",
             "La page est restée ouverte trop longtemps. Reconnectez-vous et recommencez "
             "la saisie."],
            ["Ce véhicule est déjà pris sur cette demi-journée",
             "Une autre tournée l'utilise, ou il est déclaré indisponible."],
            ["La tournée est complète",
             "Le nombre de places est atteint. Choisissez une autre date, ou augmentez le "
             "nombre de places sur la tournée."],
            ["Aucun tarif n'est défini pour ce secteur",
             "La grille est incomplète. À compléter dans Administration, puis Secteurs et "
             "tarifs."],
            ["Le formulaire n'est pas encore ouvert",
             "Le texte des conditions de collecte n'est pas saisi dans les réglages."],
            ["Un message reste en attente",
             "La tâche planifiée du serveur ne tourne peut-être plus. À signaler à "
             "If-Net."],
        ]),
        ("st", "Une saisie faite deux fois"),
        ("p", "L'application s'en protège. Un envoi lancé deux fois de suite ne crée pas "
              "deux dossiers, et un même message ne part jamais en double."),
        ("st", "Un doute sur un bon signé"),
        ("p", "Le bon conserve ce qui a été présenté au client. Cherchez-le dans "
              "<b>Archives</b> plutôt que dans le dossier, qui a pu évoluer depuis."),
    ]),
]


def tableau(lignes, s):
    """Tableau à deux colonnes, la première servant d'intitulé."""
    donnees = [
        [Paragraph(f"<b>{cellule}</b>" if rang == 0 else cellule, s["corps"]) for cellule in ligne]
        for rang, ligne in enumerate(lignes)
    ]

    largeur = A4[0] - 2 * MARGE
    table = Table(donnees, colWidths=[largeur * 0.32, largeur * 0.68], hAlign="LEFT")
    table.setStyle(TableStyle([
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ("LEFTPADDING", (0, 0), (-1, -1), 0),
        ("RIGHTPADDING", (0, 0), (-1, -1), 10),
        ("LINEBELOW", (0, 0), (-1, -2), 0.5, TRAIT),
        ("LINEBELOW", (0, 0), (-1, 0), 1, BLEU),
    ]))

    return table


def encadre(texte, s):
    """Encadré orange : ce qu'il ne faut pas rater."""
    largeur = A4[0] - 2 * MARGE
    table = Table([[Paragraph(texte, s["encadre"])]], colWidths=[largeur], hAlign="LEFT")
    table.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), AMBRE_FOND),
        ("LINEBEFORE", (0, 0), (0, -1), 3, AMBRE),
        ("TOPPADDING", (0, 0), (-1, -1), 9),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 9),
        ("LEFTPADDING", (0, 0), (-1, -1), 11),
        ("RIGHTPADDING", (0, 0), (-1, -1), 11),
    ]))

    return table


def construire():
    s = styles()
    histoire = []

    # La couverture occupe sa page, dessinée par le canevas. Le changement de
    # gabarit doit être demandé, sinon elle se répéterait sur tout le document.
    histoire.append(Spacer(1, 1))
    histoire.append(NextPageTemplate("courante"))
    histoire.append(PageBreak())

    # Sommaire.
    histoire.append(Paragraph("Sommaire", s["titre1"]))
    sommaire = [
        [str(numero), titre]
        for numero, (titre, _) in enumerate(SECTIONS, start=1)
    ]
    largeur = A4[0] - 2 * MARGE
    table = Table(
        [[Paragraph(f"<b>{n}</b>", s["corps"]), Paragraph(t, s["corps"])] for n, t in sommaire],
        colWidths=[12 * mm, largeur - 12 * mm],
        hAlign="LEFT",
    )
    table.setStyle(TableStyle([
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 4),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
        ("LEFTPADDING", (0, 0), (-1, -1), 0),
        ("LINEBELOW", (0, 0), (-1, -2), 0.4, TRAIT),
    ]))
    histoire.append(table)
    histoire.append(PageBreak())

    for numero, (titre, blocs) in enumerate(SECTIONS, start=1):
        histoire.append(Paragraph(f"{numero}. {titre}", s["titre1"]))

        for bloc in blocs:
            nature, contenu = bloc[0], bloc[1]

            if nature == "p":
                histoire.append(Paragraph(contenu, s["corps"]))
            elif nature == "i":
                histoire.append(Paragraph(contenu, s["intro"]))
            elif nature == "st":
                histoire.append(Paragraph(contenu, s["titre2"]))
            elif nature == "l":
                for point in contenu:
                    histoire.append(Paragraph(point, s["puce"], bulletText="•"))
                histoire.append(Spacer(1, 4))
            elif nature == "e":
                histoire.append(Spacer(1, 4))
                histoire.append(KeepTogether(encadre(contenu, s)))
                histoire.append(Spacer(1, 9))
            elif nature == "r":
                histoire.append(Spacer(1, 3))
                histoire.append(tableau(contenu, s))
                histoire.append(Spacer(1, 9))

        if numero < len(SECTIONS):
            histoire.append(PageBreak())

    doc = BaseDocTemplate(
        str(SORTIE),
        pagesize=A4,
        title="Manuel d'utilisation - La Ressourcerie du Calaisis",
        author="If-Net",
        subject="Application de gestion des collectes, livraisons et meubles à emporter",
        leftMargin=MARGE, rightMargin=MARGE,
        topMargin=25 * mm, bottomMargin=22 * mm,
    )

    cadre = Frame(MARGE, 22 * mm, A4[0] - 2 * MARGE, A4[1] - 47 * mm, id="corps")
    doc.addPageTemplates([
        PageTemplate(id="couverture", frames=[cadre], onPage=couverture),
        PageTemplate(id="courante", frames=[cadre], onPage=page_courante),
    ])

    doc.build(histoire)

    return SORTIE


if __name__ == "__main__":
    chemin = construire()
    print(f"{chemin} ({chemin.stat().st_size // 1024} Ko)")
