Ta clé de secoursÀ imprimer maintenant — elle ne sera plus jamais affichée
C'est la seule façon de rouvrir tes offres si le mot de passe est perdu.
Sans elle et sans mot de passe, les données sont définitivement illisibles — personne ne peut les récupérer,
ni ton service informatique, ni moi. Range-la ailleurs que sur ce PC.
Navigateur trop ancienLe coffre ne peut pas s'ouvrir ici
Ce navigateur ne sait pas déchiffrer le catalogue (fonction
crypto.subtle absente). Ouvre le fichier avec Google Chrome ou Microsoft Edge à jour —
c'est ce que fait le raccourci « Calculateur ASCV » du bureau.
Le configurateur de réseau tourne
dans sa propre fenêtre, à l’intérieur de l’application : il garde ses réglages, ses
exports (CSV, OBJ 3D, DXF 2D/3D) et sa nomenclature, sans jamais toucher aux tarifs ni aux
offres de l’ERP. Tout est embarqué, rien ne sort sur Internet — la bibliothèque 3D est dans
le fichier. Cette base est encore en chantier : ce qui y est calculé reste une
estimation, la calculation qui fait foi pour une offre est celle des pages
Calculation et Offres.
Offres
Détails
Rabais et suivi
Les rabais par groupe (QUA, STD, RON, FLX, BRD) restent dans la carte Tarifs ;
le rabais global ci-dessus s'applique par-dessus, comme dans l'ERP. Tout ce qui est saisi ici part
dans l'offre enregistrée : la ligne de la grille se remplit toute seule pendant que tu écris.
#
Gr.
Désignation
Quantité
Unité
Prix à l'unité
Total HT
Rabais %
Rabais
Total HT 2
Actions
Même lecture que l'onglet « Lignes de l'offre » de l'ERP : le rabais du groupe est
appliqué ligne par ligne (colonnes orange) et la ligne Somme ferme le tableau. Les boutons
+ Gaine, + Pce quadratique, + Référence catalogue et + Article libre
ouvrent une petite fenêtre sans quitter la page : les références du catalogue à gauche, la
calculation à droite. Tu peux enchaîner plusieurs positions puis fermer. + Import PDF / liste
ouvre la même sorte de fenêtre pour lire une demande d'offre : l'écran de vérification s'affiche
dedans et les positions retenues tombent directement dans le tableau ci-dessus, sans changer de
page — l'onglet Import, juste à côté, fait la même chose en pleine largeur.
Chaque ligne se modifie (✎), se duplique (⧉), se déplace (↑ ↓) et se
supprime (✕) directement ici ; coche deux lignes ou plus pour en faire un assemblage.
Le prix unitaire n'est ouvert qu'à un administrateur : c'est la règle des tarifs.
Une majoration d'assemblage se recalcule toute seule, et l'assemblage se dissout s'il
descend sous deux pièces. La même liste, avec les poids, reste dans la carte Positions.
Groupe d'articles
Code
Lignes
Quantité
Poids (kg)
Les kilos viennent du calcul ASCV, exactement comme sur le bon de préparation :
gaines droites et pièces de forme sont comptées séparément, c'est ce qu'il faut
pour commander la tôle. Le tableau donne le nombre d'articles par groupe du catalogue
(rond, BRDi, standard, flexibles…) — les postes chiffrés au poids comptent en kilos, pas en pièces.
Libellé
Prix à l'unité
Quantité
Unité
Total HT
Rabais
Total HT 2
Compteur
Les positions identiques (même libellé et même prix unitaire) sont regroupées, le
compteur dit combien de lignes de l'offre ont été réunies : pratique pour vérifier d'un
coup d'œil qu'une pièce n'a pas été chiffrée deux fois.
Import — PDF ou liste de pièces
Les demandes d'offre dessinées (ProAir App) sont lues optiquement, sans texte : sections, longueurs, angles et surfaces sont extraits des plans puis recoupés avec la surface totale du document. L'app lit le fichier, reconnaît les pièces quadratiques (calcul ASCV) et les articles du catalogue (prix ERP), puis te montre le résultat à vérifier avant de l'ajouter aux positions.
Assistant IA — lecture des demandes d'offre (facultatif)
Sans clé, l'application reste entièrement hors ligne : aucun appel n'est fait, rien ne sort de ton PC, et tout fonctionne comme avant. Avec une clé, l'IA sert d'interprète pour les lignes que les règles n'ont pas su lire et pour les demandes dessinées. Elle ne donne jamais de prix ni de référence : elle rend seulement famille, cotes, matière, classe, angle, longueur et quantité — le calcul ASCV et ton catalogue chiffrent comme d'habitude, au centime près.
À savoir, honnêtement : la clé est enregistrée dans ce navigateur, sur ce PC. Quiconque a accès à ta session Windows peut la relire — mets donc un plafond de dépense dans la console Anthropic (Billing → Limits) et change la clé si tu prêtes la machine. Elle n'est jamais écrite dans le fichier de réglages que tu exportes. Chaque ligne relue coûte quelques centimes, et une fois que tu valides l'import, l'app retient la traduction : le même bureau est ensuite relu gratuitement, hors ligne.
Nouvelle offre
L'offre à l'écran est enregistrée avant d'ouvrir la nouvelle : rien ne se perd.
N° attribué : —
Factures
Nouvelle facture
Le client et les dates
Références
La succursale se fige dès que le numéro est tiré :
un document ne change pas de série en cours de route.
Ce que tu saisis ici est un brouillon :
modifiable autant de fois qu'il faut, hors de l'encours, et il s'imprime barré BROUILLON,
sans bulletin de versement — impossible de l'envoyer par mégarde. C'est la
comptabilisation qui lui donne sa date de facturation et en fait une pièce comptable.
Les mêmes lignes, regroupées par désignation puis par
groupe de tarif : c'est la vue qui sert à contrôler une facture avant de l'envoyer — deux fois
la même pièce à deux prix différents se voit tout de suite ici, jamais dans la liste.
Un encaissement fait descendre le solde : la
comptabilisation a inscrit la vente à l'encours, l'argent reçu l'en retire. Quand le solde
tombe à zéro, la facture se clôture toute seule — personne ne coche « payée » à la main.
L'escompte n'est pas de l'argent reçu : c'est un rabais consenti pour paiement rapide,
il solde la facture sans entrer dans le total encaissé.
Un rappel s'inscrit ici et s'imprime : le
bouton 🖨 de la ligne sort la lettre, avec le décompte du retard et son propre bulletin de
versement — le client n'a pas à retrouver la facture pour payer. Les frais sont notés
pour mémoire : ils ne s'ajoutent pas au solde de la facture, qui reste la pièce comptable.
Le degré proposé est toujours le suivant, mais rien n'oblige à les enchaîner — une mise en
demeure directe est possible si le dossier le demande.
D'où vient une facture
Deux chemins, et ils mènent au même endroit. Le premier
part d'une offre : ouvre-la, clique sur Facturer une offre, coche les
positions et corrige les quantités si le chantier n'est fait qu'à moitié — tu peux
facturer une offre en plusieurs fois, l'application retient ce qui est déjà
parti et ne propose que le reste. Le second est la facture libre : un dépannage,
une heure de main-d'oeuvre, une pièce vendue au comptoir n'ont jamais eu de devis.
Clique sur Nouvelle facture, tape tes lignes ou va chercher les articles dans
le catalogue, et prends le client au registre ou écris son nom à la main.
Dans les deux cas, les rabais de groupe, le rabais
global et le taux de TVA sont recopiés dans la facture le jour où elle est
créée : si tu changes tes tarifs le mois suivant, une facture déjà envoyée ne bouge
pas d'un centime.
Brouillon, puis pièce comptable
Une facture naît toujours brouillon. Elle porte
déjà son numéro — il lui est acquis pour toujours — mais elle n'est encore qu'un papier
de travail : elle se corrige librement, elle ne compte pas dans l'encours, et elle sort
de l'imprimante barrée BROUILLON, sans bulletin de versement. On ne peut donc
pas l'envoyer par mégarde.
Comptabiliser est le geste qui en fait une
facture : il fixe la date de facturation, recalcule l'échéance depuis ton délai
de paiement, et ferme le document. Décomptabiliser fait le chemin inverse quand
il faut corriger : la facture redevient un brouillon modifiable, sans jamais changer de
numéro. C'est réservé à l'administrateur, et refusé dès qu'un franc est encaissé — une
facture payée ne se réécrit pas, on en fait un avoir.
Rien ne s'efface
Une facture numérotée est une pièce comptable :
elle ne se supprime pas, elle s'annule. Elle reste au registre avec son numéro,
sort des totaux, et ses quantités redeviennent facturables sur l'offre.
Les numéros ne sont jamais réattribués, même après une annulation ou un import.
Le QR de paiement suisse s'imprime tout seul en bas
de la facture, dès que l'IBAN et l'adresse du bénéficiaire sont remplis dans
Configuration → Coordonnées de paiement. Le code se calcule ici, sur le poste,
sans le moindre accès à Internet : l'IBAN ne quitte jamais l'atelier.
Facturer une offre
La quantité proposée est ce qu'il reste à facturer :
corrige-la si le chantier n'est livré qu'en partie.
Les rabais de groupe, le rabais global et le taux de TVA de
cette offre sont recopiés dans la facture : un changement de tarif plus tard ne
touchera plus ce document.
N° attribué à la validation
Rappels
Le registre des rappels
Tout ce qui est parti, du plus récent au plus ancien, toutes
factures confondues. Un rappel est un fait, pas un état : on n'écrit nulle
part « facture rappelée », on inscrit qu'un courrier de tel degré est parti tel
jour, par qui. C'est ce qu'on retrouve six mois plus tard, quand le client jure
n'avoir jamais rien reçu. Le 🖨 réimprime la lettre telle qu'elle est partie.
Encaissements banque
Dépose ici le fichier camt.054 que la banque t'a envoyé — le .zip tel quel fait
aussi l'affaire, il sera ouvert tout seul. Rien ne sera comptabilisé avant que tu cliques
sur Importer.
Ce que fait cet écran
Chaque mois la banque envoie un fichier
camt.054 — l'avis de crédit. Dépose-le ici : l'application le lit, retrouve
chaque paiement qu'il contient, et cherche à quelle facture il correspond
grâce à la référence QRR de 27 chiffres imprimée sur le bulletin de versement.
Attention à un piège que la banque tend à tout le
monde : une seule écriture, du genre « TRAITEMENT QR FACTURES CHF », peut contenir
vingt paiements de vingt clients différents. L'application descend donc
toujours au détail de l'écriture, jamais à son total.
Le tableau ci-dessus n'est qu'un aperçu :
regarde-le, et clique sur Importer quand il te convient. Ce n'est qu'à ce
moment-là que les encaissements sont posés, que les soldes redescendent et que les
factures entièrement payées se clôturent toutes seules.
Ce qui peut clocher
Facture inconnue — la référence ne correspond
à aucune facture comptabilisée. C'est le cas normal pour un paiement destiné à une
facture de l'ancien ERP, ou pour un virement sans QR.
Déjà importé — ce paiement est déjà au registre
de la facture. Redéposer deux fois le même fichier ne crée donc aucun doublon.
Plusieurs factures — deux factures portent le
même numéro dans deux séries différentes. L'application ne devine pas : ouvre la
bonne facture et encaisse à la main depuis l'onglet Mouvements monétaires.
Les lignes au débit (ce que nous payons) et les
paiements dans une autre devise que le franc sont écartés d'office.
Les fichiers déjà lus
Clients
Nouveau client
Conditions de rabais de ce client
🔒 Conditions verrouillées.
Tu es connecté avec un compte employé : tu vois les conditions du client, tu ne les changes pas.
Seul un administrateur fixe les rabais.
Rabais global (%)
Les taux livrés avec l'application sont ceux de l'ERP : ils
s'affichent en gris dans les champs. Écrire un taux ici le recouvre pour ce client, sur ce poste
seulement — le tableau d'origine n'est jamais réécrit. Vider le champ rend la main au taux livré.
Ces conditions se posent sur l'offre dès que le client est choisi, et le chantier passe devant :
on négocie un tarif avec une entreprise, puis un rabais particulier pour un chantier donné.
D'où vient cette fiche
Le carnet livré avec l'application (634 contacts) n'est
jamais réécrit : ce que tu corriges ici vit à côté, dans la mémoire chiffrée de ce poste,
et recouvre le carnet au démarrage. Une nouvelle version de l'application garde donc tes fiches.
Rien ne se supprime : une fiche se désactive, elle sort des recherches, elle reste
consultable ici et les offres qui la citent ne bougent pas.
Tout le monde peut ajouter un client — on en rencontre en écrivant une offre.
Chantiers (mandats)
Nouveau chantier
Conditions tarifaires de ce chantier
🔒 Conditions verrouillées.
Tu es connecté avec un compte employé : tu vois les conditions du chantier, tu ne les changes pas.
Seul un administrateur fixe les rabais.
Rabais global (%)
Un champ laissé vide ne dit rien : le rabais habituel de la carte
Tarifs s'applique. Un champ rempli — même à 0 — est la condition du chantier : elle se pose sur
l'offre dès que tu choisis ce chantier dans le titre, et elle part telle quelle dans la facture,
puisqu'une facture recopie les taux de l'offre au moment où elle naît.
Articles
🔒 Articles verrouillés.
Tu es connecté avec un compte employé : tu peux chercher et consulter les articles,
mais seul un administrateur crée un article ou corrige un prix. C'est la même règle que les tarifs.
La photo n'entre pas dans l'application : c'est le chemin vers le serveur qui est
gardé. Une photo remplacée sur le serveur se met donc à jour toute seule sur les bons de
préparation. Écris le chemin comme Windows te le donne
(\\serveur\Photos\… ou I:\Photos\…) : l'application le traduit.
Coche les lignes avec les cases, Ctrl+A pour tout ce qui est affiché,
Maj+clic pour une plage, Ctrl+clic pour ajouter une ligne.
Nouvel article
D'où vient ce prix
Le catalogue livré avec l'application (31'343 références)
n'est jamais réécrit : ta correction vit à côté et le recouvre au démarrage — une
nouvelle version de l'application ne l'efface pas. Le bouton Catalogue, dans la liste,
remet la référence à son prix d'origine. Un article créé ici ne se supprime pas : il se
désactive, il sort des recherches et les offres qui le citent restent lisibles.
Le prix corrigé part partout : recherche du catalogue, import de soumission, bon d'atelier.
Journal des corrections de prix
Nouvelle position
Au clavier : Tab entre dans la liste, ↑ ↓ choisissent la
référence, Entrée la prend. Échap referme la liste.
Poids–
Isolation–
Prix unitaire–
Total ligne–
Positions
☑
#
Gr.
Désignation
Qté
Poids u. (kg)
PU (CHF)
Rab. %
Total HT2
Aucune position — ajoute une pièce ci-dessus.
Coche 2 à 10 positions puis clique Créer un assemblage — une ligne de majoration (25% de la somme des pièces cochées) est ajoutée, comme dans l'ASCV. Bon de préparation atelier : sort la liste du matériel à préparer, sans aucun prix, un groupe de matériel par page (quadratique, rond, standard, flexibles, colliers, isolation). Coche des positions pour n'en sortir qu'une partie — sans rien cocher, tout le bon est édité.
Totaux
Total HT
0.00
Total rabais groupes
0.00
Remise
0.00
Total HT2
0.00
TVA
0.00
Total TTC CHF
0.00
« Copier pour l'ERP » place les lignes (désignation, qté, PU, total) au format tabulé dans le presse-papiers — colle-les directement dans ton ERP ou dans Excel. Les poids proviennent des tableaux du chapitre 15 ; les dimensions intermédiaires sont interpolées linéairement. Prix unitaires au centime (comme l'ASCV), total TTC arrondi à 0.05 CHF.
Tarifs (modifiables)
🔒 Tarifs verrouillés.
Tu es connecté avec un compte employé : tu peux consulter les tarifs mais pas les modifier.
Seul un administrateur change les prix.
Caractéristiques des matériaux
Rabais par groupe d'articles (%)
Divers
TVA (%)
Remise globale (%)
Majoration assemblage (%)
Prix min. majoration assemblage
Types d'isolation — prix CHF/m²
Ces prix sont ceux de la liste déroulante « Isolation int. » de la saisie. Modifie-les ici : le nouveau prix s'applique tout de suite et part dans l'export de tes réglages.
Longueurs conseillées (mm)
Types de pièce et mode de calcul
Vocabulaire des bureaux d'ingénieurs
Chaque ligne traduit un terme employé par un bureau vers le terme Air-Ventil. Ajoute ici tout ce que l'app ne comprend pas encore — allemand, italien, abréviations maison. La traduction est appliquée avant la reconnaissance, à l'import comme au collage de texte.
Lexique du catalogue
Le catalogue ne contient que des codes : « SR 315 Galva 0.5mm » ne dit nulle part « tube spiro ». Ce lexique rattache une famille de références aux mots que les bureaux emploient. Une ligne suffit à rendre toute une famille trouvable. À gauche les préfixes de référence (séparés par un espace), à droite les mots.
Équivalences de références
Ici tu dis simplement : « ce que le bureau écrit » = « nos références ». Exemple : les SLU de Lindab, chez nous ce sont les SL Safe. À gauche le mot employé dans la soumission, à droite le début de notre référence (plusieurs possibles, séparées par une virgule : la première qui existe gagne). L'application cherche ensuite le bon diamètre, la bonne longueur ou la bonne taille toute seule. Une équivalence ne peut jamais inventer un prix : si rien ne correspond au catalogue, la ligne reste en rouge « à chiffrer ».
Essayer une ligne
Colle une ligne telle qu'elle est écrite dans une soumission : l'application montre tout de suite la référence et le prix qu'elle retiendrait. Rien n'est enregistré, rien n'est chiffré — c'est un banc d'essai.
Tarifs CHF/kg selon classe d'étanchéité (B = standard, C = étanche, D = désenfumage), repris de l'application ASCV — modifiables, ils s'appliquent immédiatement à toutes les lignes. « Gaines droites » s'applique aux gaines ; « Pièces de forme » aux coudes, déviations et tés. Poids de référence : catalogue Air-Ventil 2025, chapitre 15 (tôle galvanisée) ; pour les autres matières, le poids est ajusté selon le poids surfacique (kg/m²) de la section (tableau « Caractéristiques des matériaux »). Prix des isolations par type, sélectionnables à la saisie.
Mémoire de l'application
Tes prix, tes rabais, le vocabulaire des bureaux et tout ce que l'app a appris de tes corrections sont enregistrés automatiquement. Exporte ce fichier avant chaque nouvelle version et réimporte-le ensuite : c'est ce qui fait que l'application ne repart jamais de zéro.
Utilisateurs et accès
Le mot de passe est la clé de chiffrement des données de ce poste :
catalogue, tarifs et offres sont illisibles sans lui. Ce qu'il protège : un exemplaire du fichier copié
ailleurs — clé USB, serveur, sauvegarde — ne s'ouvre pas et ne montre ni prix, ni référence, ni client.
Ce qu'il ne protège pas : quelqu'un assis à ce poste, avec le mot de passe, voit tout. Ce n'est pas non plus
une sauvegarde. Chaque utilisateur a sa succursale par défaut et son nom, repris automatiquement dans
« Notre réf. » de l'offre.
Changer mon mot de passe
Utilisateurs
Un compte employé chiffre, ouvre et enregistre des offres.
Il ne peut ni modifier les tarifs, ni changer la numérotation, ni créer de compte.
Seul un administrateur le peut.
Les sociétés du groupe
🔒 Fiches des sociétés verrouillées.
Tu es connecté avec un compte employé : tu peux les lire, mais seul un administrateur
modifie l'en-tête et la numérotation d'une société.
Le groupe facture sous trois raisons :
Air-Ventil SA à Romanel, Air-Ventil SA à Boudry, et HK Teknik A.Ş. en Turquie.
Chaque facture appartient à une société et à une seule — c'est elle qui donne
l'en-tête imprimé, la monnaie, la série de numéros et la loi fiscale appliquée.
Ce qui est saisi ici est rangé dans la mémoire chiffrée de ce poste.
Contrôle
Coordonnées de paiement — facture QR suisse
🔒 Coordonnées de paiement verrouillées.
Tu es connecté avec un compte employé : tu peux les lire, mais seul un administrateur
modifie l'IBAN de la société. C'est la même règle que les tarifs.
Ce que tu tapes ici s'imprime au bas de chaque facture, dans le
bulletin de versement QR. Les coordonnées d'Air-Ventil sont déjà remplies d'usine — le compte BCV
figure de toute façon dans l'en-tête imprimé de chaque facture. Tu peux tout corriger : ce que tu
enregistres ici est rangé dans la mémoire chiffrée de ce poste et recouvre les valeurs d'usine.
Le code QR est calculé ici, sans le moindre accès à Internet.
Le compte
Le n° d'identification est celui que la banque a attribué
à la société : il ouvre la référence QRR, le numéro de facture la termine. Laisse les deux champs
d'escompte à zéro pour facturer net sans escompte.
Un QR-IBAN (compte commençant par CH.. 3xxxx) impose de lui-même
une référence QRR à 27 chiffres, construite sur le numéro de facture : la banque te dit alors
quelle facture a été payée. Sur un IBAN ordinaire, la référence SCOR est facultative.
Le bénéficiaire (adresse structurée, comme l'exige la norme depuis novembre 2025)
Contrôle
Pièces rondes — ce que l'atelier commande
Une dérivation dessinée se chiffre en TC SAFE. Dès que le catalogue
n'en a plus à la taille demandée, elle passe en T Roulé, payé sur la plus grande section
du dessin — un té 280/315/280 se paye au ø315 — et l'intitulé garde les sections du plan. Les autres
familles du catalogue (piquages, croix, tés à 45°) ne servent ni au chiffrage ni à la facture : elles
ne sont pas proposées.
Deux habitudes que le dessin ne dit pas
Un coude posé entre deux tubes lisses se commande lisse, avec son
cadre METU : c'est le BSELM, nettement plus cher que le coude à segments. Le catalogue n'en fait
pas en galva au-dessous de ø450 — en dessous, le chiffrage retombe tout seul sur l'embouti puis sur
le coude à segments.
Le configurateur voit le décalage des axes. Sans cette case, la
réduction reste concentrique (RC). Cochée — c'est le réglage d'usine — elle part en RE — ou en REL
si la case du lisse l'est aussi.
Ce que le chiffrage reconnaît aujourd'hui
—
Licence et protection
Calculateur ASCV — logiciel de calculation et de chiffrage
de la société Air-Ventil Sàrl, Romanel-sur-Lausanne et Boudry. Développé pour son usage interne.
Le catalogue, les tarifs et les prix de revient qu'il contient sont la propriété d'Air-Ventil Sàrl :
ni le fichier, ni son contenu ne peuvent être copiés, revendus ou remis à un tiers.
Poste n° —. À donner en cas de question : il identifie
l'installation, il ne contient aucun secret.
Clé de secours
Elle rouvre les données de ce poste si un mot de passe est perdu.
Une seule est valable à la fois : en tirer une nouvelle annule immédiatement l'ancienne.
Sans mot de passe et sans clé de secours, les offres sont définitivement illisibles —
le chiffrement n'a pas de porte de service, ni pour ton service informatique, ni pour moi.
À propos de cette version
Version 53 — Import et Assistant IA sont
devenus deux onglets de la fiche d'offre, à côté de « Lignes de l'offre » : plus besoin de quitter
l'offre pour lire une demande ou régler l'assistant ·
Résumé des pièces refait en lecture d'atelier : kilos de gaines droites, kilos de pièces de
forme et nombre d'articles par groupe du catalogue (le détail des pièces identiques reste
disponible, replié) ·
numérotation des offres réparée : plus de numéro perdu, les trous sont repris automatiquement, une fiche vide n'écrase plus une offre enregistrée, l'offre en cours est rouverte au démarrage, le n° n'est attribué qu'à la première vraie sauvegarde (reprise des n° libres débrayable par l'administrateur) · import accessible depuis « Lignes de l'offre », dans une fenêtre, sans changer de page · table d'équivalences éditable
(« ce que le bureau écrit » = « nos références », ex. SLU Lindab = SL Safe) avec banc d'essai ·
grilles pare-pluie sur mesure chiffrées à la taille catalogue qui couvre l'ouverture,
selon la matière annoncée ·
chaque ligne importée indique sa page
et son n° de position dans le document d'origine · bague d'obturation ø140 chiffrée comme
support de thermomètre (et non comme thermomètre) ·
offres non supprimables ·
tarifs et numérotation réservés aux administrateurs · n° d'offre unique et non modifiable ·
fenêtre de saisie (références à gauche, calculation à droite) ·
catalogue articles du 26.07.2026 (31'343 réf.) ·
succursales VD / NE · page Offres (grille + Détails / Lignes / Résumé) · bon de préparation atelier ·
lecture des demandes d'offre dessinées (ProAir / Concepto) et des plans cotés (AutoCAD).
Pièces quadratiques calculées selon le chapitre 15, articles au prix catalogue, prix CHF hors TVA.
Tout fonctionne hors ligne : sans clé API, aucun appel réseau n'est fait.
Nouvelle position
Références catalogue
Clique une référence : elle passe à droite
avec son prix catalogue. Pour une pièce quadratique, choisis simplement le type
de pièce à droite et laisse cette liste de côté.
Les positions ajoutées apparaissent dans l'onglet « Lignes de l'offre ».
Importer une demande d'offre dans l'offre à l'écran
Les positions reconnues s'ajoutent directement aux lignes de l'offre.
Croquis de la pièce
Outil
Valeur à poser
Feuille
Classeur commun
Rien n'est effacé : en cas de doute, les deux versions sont gardées et listées ici.
Configurateur absent de ce fichier.');t.innerHTML="";const s=document.createElement("iframe");s.id="cfg3dFrame",s.setAttribute("title","Configurateur de réseau Air-Ventil"),s.srcdoc=r,t.appendChild(s),CFG3D_FAIT=!0}function cfg3dRecharger(){CFG3D_FAIT=!1,cfg3dOuvrir(!0),toast("Configurateur rechargé.")}function cfg3dPleinEcran(){const e=document.getElementById("cfg3dBoite");document.fullscreenElement?document.exitFullscreen():e&&e.requestFullscreen?e.requestFullscreen().catch(()=>toast("Plein écran refusé par le navigateur.")):toast("Plein écran indisponible ici.")}const CFG_MAT={galva:"galva",desenf:"galvadf",alu:"alu",inox:"inox",v4a:"v4a",cuivre:"cuivre"},CFG_TY={straight:"gaine",elbow:"coude",cone:"cone",dev:"cone",sqround:"rondcarre",roundsq:"rondcarre",t:"te"},CFG_ROND={roundstr:1,roundlisse:1,roundelbow:1,roundtee:1,roundred:1};function cfg3dClasse(){const e=document.getElementById("pCls"),t=e?e.value:"B";return"B"===t||"C"===t||"D"===t?t:"B"}var RCAT=null;function rcatIdx(){if(RCAT)return RCAT;const e={sr:{},srl:{},b:{},bse:{},bselm:{},tc:{},te:{},tq:{},trs:{},trv:{},txc:{},trx:{},tr:{},ps:{},psb:{},pse:{},psv:{},rc:{},rcl:{},re:{},rel:{}},t=void 0!==ART&&ART&&ART.a?ART.a:[];for(let r=0;r0))continue;let o;(o=/^BSE\s+(\d+)\s+SAFE\s+(\d+)\s*$/i.exec(i))?e.bse[+o[1]+":"+ +o[2]]=s:(o=/^B(\d+)\s+SAFE\s+(\d+)\s*$/i.exec(i))?e.b[+o[1]+":"+ +o[2]]=s:(o=/^SRL\s+(\d+)\s+L\.\s*(\d+)\s*$/i.exec(i))?e.srl[+o[1]+":"+ +o[2]]=s:(o=/^SR\s+(\d+)\s+Galva\b/i.exec(i))?e.sr[+o[1]]=s:(o=/^TC\s+SAFE\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.tc[+o[1]+":"+ +o[2]]=s:(o=/^T\s+Roul[e\u00e9]\s+(\d+)\s+Galva\s*$/i.exec(i))?e.tr[+o[1]]=s:(o=/^RC\s+Safe\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.rc[+o[1]+":"+ +o[2]]=s:(o=/^BSELM\s+(\d+)\s+Galva\s+(\d+)\s*$/i.exec(i))?e.bselm[+o[1]+":"+ +o[2]]=s:(o=/^T\s+emboutis\s+SAFE\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.te[+o[1]+":"+ +o[2]]=s:(o=/^T[e\u00e9]\s+Conique\s+SAFE\s+(\d+)\s*\/\s*(\d+)\s*$/i.exec(i))?e.tq[+o[1]+":"+ +o[2]]=s:(o=/^TXC\s+Safe\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.txc[+o[1]+":"+ +o[2]]=s:(o=/^TRX\s+Safe\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.trx[+o[1]+":"+ +o[2]]=s:(o=/^TRV\s+Safe\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.trv[+o[1]+":"+ +o[2]]=s:(o=/^TR\s+(?:SAFE\s+)?(\d+)\/(\d+)\s*$/i.exec(i))?e.trs[+o[1]+":"+ +o[2]]=s:(o=/^PSB\s+SAFE\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.psb[+o[1]+":"+ +o[2]]=s:(o=/^PSE\s+Safe\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.pse[+o[1]+":"+ +o[2]]=s:(o=/^PSV\s+Safe\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.psv[+o[1]+":"+ +o[2]]=s:(o=/^PS\s+SAFE\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.ps[+o[1]+":"+ +o[2]]=s:(o=/^RCL\s+Galva\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.rcl[+o[1]+":"+ +o[2]]=s:(o=/^REL\s+Galva\s+(\d+)\/(\d+)\s*$/i.exec(i))?e.rel[+o[1]+":"+ +o[2]]=s:(o=/^RE\s+Safe\s+(\d+)\/(\d+)\s*$/i.exec(i))&&(e.re[+o[1]+":"+ +o[2]]=s)}const un=e=>Object.keys(e).map(Number).sort((e,t)=>e-t),de=(e,t)=>{const r={};return Object.keys(e).forEach(e=>{r[+e.split(":")[t]]=1}),Object.keys(r).map(Number).sort((e,t)=>e-t)};return e.srD=un(e.sr),e.trD=un(e.tr),e.srlD=de(e.srl,0),e.srlL=de(e.srl,1),e.bA=de(e.b,0),e.bD=de(e.b,1),e.bseA=de(e.bse,0),e.bseD=de(e.bse,1),e.tcD=de(e.tc,0),e.tcd=de(e.tc,1),e.rcD=de(e.rc,0),e.rcd=de(e.rc,1),["te","tq","trs","trv","txc","trx","ps","psb","pse","psv","rcl","re","rel","bselm"].forEach(t=>{e[t+"A"]=de(e[t],0),e[t+"B"]=de(e[t],1)}),e.tcA=e.tcD,e.tcB=e.tcd,e.rcA=e.rcD,e.rcB=e.rcd,RCAT=e,e}function rcSup(e,t){for(let r=0;r=t-.1)return e[r];return null}function rcApres(e,t){const r=e.indexOf(t);return r<0||null==e[r+1]?null:e[r+1]}function rcAngle(e,t){let r=null,s=1/0;for(let i=0;i0))return null;const i=e.t;if("roundstr"===i){const t=rcSup(r.srD,s);if(null==t)return null;const i=r.sr[t],o=Math.max(.001,(+e.L||160)/1e3);return{a:i,pu:r2(i[2]*o),geo:"Ø"+t+" · L "+Math.round(1e3*o)+" mm",info:i[0]+" · "+fmt(i[2])+" CHF/m × "+fmt(o)+" m"}}if("roundlisse"===i){const t=rcSup(r.srlD,s);if(null==t)return null;const i=r.srlL[r.srlL.length-1];let o=Math.max(1,Math.round(+e.L||160)),l=0,u=null;const d=[];for(;o>i+.1;){const e=r.srl[t+":"+i];if(!e)return null;l+=e[2],d.push(i),o-=i,u=u||e}const m=rcSup(r.srlL,o);if(null==m)return null;const h=r.srl[t+":"+m];if(!h)return null;l+=h[2],d.push(m),u=u||h;const g={};d.forEach(e=>{g[e]=(g[e]||0)+1});const b=Object.keys(g).map(Number).sort((e,t)=>t-e).map(e=>g[e]+"× L."+e).join(" + ");return{a:u,pu:r2(l),geo:"Ø"+t+" · L "+Math.round(+e.L||160)+" mm",info:"SRL "+t+" · "+b+" · prix par pièce"}}if("roundelbow"===i){const t=+e.ang||90,cherche=(e,r,i)=>{const o=rcAngle(r,t);if(null==o)return null;let l=rcSup(i,s);for(;null!=l;){const t=e[o+":"+l];if(t)return{a:t,ang:o,d:l};l=rcApres(i,l)}return null},i=rondReglage(),o=r.bselmB.length?r.bselmB[0]:1e9;let l=null,u=!1;if(i.lisse&&e.lis&&s>=o-.1&&(l=cherche(r.bselm,r.bselmA,r.bselmB),l&&(u=!0)),l||(l=cherche(r.b,r.bA,r.bD)||cherche(r.bse,r.bseA,r.bseD)),!l)return null;const d=/^B\d/i.test(l.a[0]);return{a:l.a,pu:r2(l.a[2]),geo:l.ang+"° Ø"+l.d,info:l.a[0]+" · coude "+(u?"lisse à cadre METU":d?"embouti":"à segments")+(Math.abs(l.ang-t)>.5?" · angle dessiné "+Math.round(t)+"° ramené à "+l.ang+"°":"")}}if("roundtee"===i){const t=Math.round(+e.brD||0)||s;let i=rcSup(r.tcD,s);for(;null!=i;){let e=rcSup(r.tcd,t);for(;null!=e&&e<=i;){const t=r.tc[i+":"+e];if(t)return{a:t,pu:r2(t[2]),geo:"Ø"+i+"/"+e,info:t[0]+" · té conique SAFE"};e=rcApres(r.tcd,e)}i=rcApres(r.tcD,i)}const o=Math.max(s,t),l=rcSup(r.trD,o);if(null!=l){const e=r.tr[l];return{a:e,pu:r2(e[2]),geo:"Ø"+(t&&t!==s?s+"/"+t+"/"+s:s),info:e[0]+" · té roulé (aucun TC SAFE à cette taille), chiffré sur la plus grande section ø"+o+(l!==o?" au tarif ø"+l:"")}}return null}if("roundred"===i){const t=Math.round(+e.rdE||+e.W||0)||s,i=Math.max(s,t),o=Math.min(s,t),l=rondReglage(),u=!(!l.lisse||!e.lis),d=!(!l.exc||!e.exc)?u?["rel","re","rcl","rc"]:["re","rel","rc","rcl"]:u?["rcl","rc"]:["rc"],m={rc:"réduction SAFE",rcl:"réduction lisse",re:"réduction excentrée SAFE",rel:"réduction excentrée lisse"};for(let e=0;et2e3){const t=decoupeGaine(s,s.L).map(e=>calcPiece(e)).filter(e=>e.ok);if(!t.length)return{k:e.k,ok:!1,nom:r,motif:"cotes hors plage"};const i=r2(t.reduce((e,t)=>e+t.pu*t.q,0)),o=r2(t.reduce((e,t)=>e+t.poids*t.q,0));return{k:e.k,ok:!0,manuel:!1,desc:t[0].desc.replace(/L=\d+/,"L="+s.L),poids:o,isoM2:0,pu:i,q:1,tot:i,warn:"",info:"découpe machine : "+t.map(e=>e.q+"×L"+e.fab.L).join(" + "),grp:GRP_QUA,src:"ascv",parts:t}}const l=calcPiece(s);return l.ok?Object.assign({k:e.k,src:"ascv"},l):{k:e.k,ok:!1,nom:r,motif:"cotes hors plage"}}const l=Math.max(0,r2(+e.wt||0)),u=rate(t,"pf",i);if(!l||!u)return{k:e.k,ok:!1,nom:r,motif:"pas de tarif"};const d=num("min_"+t+"_pf"),m=r2(Math.max(l*u,d));return{k:e.k,ok:!0,manuel:!1,desc:(r+s).trim()+", "+MAT[i]+", "+CLS[t],poids:l,isoM2:0,pu:m,q:1,tot:m,warn:"",info:"chiffré au poids · "+u.toFixed(2)+" CHF/kg × "+fmt(l)+" kg"+(l*ucfg3dLigne(e,t));let s=0,i=0,o=0;const l=r.map(e=>{if(!e.ok)return o++,{k:e.k,ok:!1,motif:e.motif};const t=grpRab(e.grp||GRP_QUA);return s+=e.pu*e.q,i+=e.pu*e.q*(1-t/100),{k:e.k,ok:!0,pu:e.pu,q:e.q,poids:e.poids,desc:e.desc,grp:e.grp,rab:t,net:r2(e.pu*e.q*(1-t/100)),info:e.info||"",src:e.src}});return{cls:t,lignes:l,brut:r2(s),net:r2(i),hors:o}}function cfg3dVersOffre(e){const t=e&&e.cls||cfg3dClasse(),r=(e&&e.pieces||[]).map(e=>cfg3dLigne(e,t)),s=r.filter(e=>e.ok),i=r.length-s.length;if(!s.length)return void toast("Rien à reprendre : aucune pièce chiffrable dans le réseau.");let o=0;s.forEach(e=>{e.parts?o+=e.parts.length:o++}),confirm("Reprendre le réseau dans l'offre en cours ?\n\n"+o+" position(s) seront ajoutées à la suite des positions actuelles."+(i?"\n"+i+" pièce(s) sans tarif seront laissées de côté.":""))&&(s.forEach(e=>{if(e.parts)return void e.parts.forEach(e=>positions.push(e));const t=Object.assign({},e);delete t.k,delete t.src,delete t.parts,positions.push(t)}),render(),erpPage("offres"),toast(o+" position(s) reprises du configurateur"+(i?" · "+i+" pièce(s) sans tarif laissées de côté":"")+"."))}function cfg3dClsPousser(){cfg3dRepondre({ascv:"cfg3d",quoi:"erp",cls:cfg3dClasse()})}function cfg3dRepondre(e){const t=document.getElementById("cfg3dFrame");if(t&&t.contentWindow)try{t.contentWindow.postMessage(e,"*")}catch(e){}}function erpPageDe(e){const t=e&&e.closest?e.closest(".page"):null;return t?String(t.id).replace(/^pg_/,""):""}function isoDemarrer(){try{isoMaj()}catch(e){}}function erpDemarrer(){let e="offres";try{e=localStorage.getItem(ERP_PAGE_KEY)||"offres"}catch(e){}try{factPaysMettre(localStorage.getItem(FACT_PAYS_KEY))}catch(e){}try{factOngletsMaj()}catch(e){}erpPage(e)}function num(e){return parseFloat(document.getElementById(e).value)||0}function $(e){return document.getElementById(e)}function fmt(e,t=2){return e.toLocaleString("fr-CH",{minimumFractionDigits:t,maximumFractionDigits:t})}function pf(e,t=2){const r=(Math.round(100*e)/100).toFixed(t).split(".");return r[0].replace(/\B(?=(\d{3})+(?!\d))/g," ")+"."+r[1]}function pfD(e,t,r=2){if("TRY"!==String(t||"CHF").toUpperCase())return pf(e,r);const s=(Math.round(100*e)/100).toFixed(r).split(".");return s[0].replace(/\B(?=(\d{3})+(?!\d))/g,".")+(r?","+s[1]:"")}function r05(e){return Math.round(20*e)/20}function r2(e){return Math.round(100*e)/100}function toast(e){const t=$("msg");t.textContent=e,t.style.opacity=1,setTimeout(()=>t.style.opacity=0,2200)}function bracket(e,t){if(t<=e[0])return[0,0,0];if(t>=e[e.length-1])return[e.length-1,e.length-1,0];let r=0;for(;e[r+1]=0&&u>=0&&(s=e.V[u][l],null!=s))return s;for(let t=0;to&&([o,l]=[l,o]);const u=[...new Set([].concat(i.I.A,i.II.A,i.III.A))].sort((e,t)=>e-t),d=Math.min(Math.max(o,u[0]),u[u.length-1]),[m,h,g]=bracket(u,d),b=tableFor(i,u[m]),A=tableFor(i,u[h]),C=colVal(b,u[m],l);return C+(colVal(A,u[h],l)-C)*g}let selArt=null;const norm=e=>e.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g,"");let artIndex=null,artIndexC=null;function artIdx(){artIndex||(artIndex=ART.a.map(e=>norm(e[0]+" "+e[1])),artIndexC=ART.a.map(e=>norm(e[0])))}function catEsc(e){return String(e).replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function catRequetes(e){const t=[e];let r;try{r=eqvTous()}catch(e){return t}for(const s of r){const r=norm(String(s[0]||"")).trim();if(r&&!(e.indexOf(r)<0))for(const i of String(s[1]||"").split(",")){const s=norm(i).trim();if(!s)continue;const o=e.replace(r," "+s+" ").replace(/\s+/g," ").trim();o&&t.indexOf(o)<0&&t.push(o)}}return t}function catPlage(e,t){const r=/(\d{1,4})_(\d{2,5})/g;let s;for(;s=r.exec(e);)if(t>=+s[1]&&t<=+s[2])return!0;return!1}function catRang(e,t,r,s){return r===e?1e3:r.startsWith(e)?900:t.every(e=>e.test(r))?700:t.every(e=>r.indexOf(e.source.slice(e.source.lastIndexOf(")")+1))>=0)?500:0}function artRanger(e,t){const r=catRequetes(e).map(e=>{const t=e.split(/\s+/).filter(Boolean),r=t.filter(e=>/^\d{2,5}$/.test(e)).map(Number),s=t.filter(e=>!/^\d{2,5}$/.test(e)),bd=e=>new RegExp("(^|[^a-z0-9])"+catEsc(e)+(/^\d/.test(e)?"(?![0-9])":"")),dedans=e=>/^\d{2,5}$/.test(e)?new RegExp("(^|[^0-9])"+e+"(?![0-9])"):null;return{q:e,mots:t,nbs:r,dans:t.map(e=>[e,dedans(e)]),bord:t.map(bd),bordT:s.map(bd)}});artIdx();const s=new Set,i=[];for(let e=0;ee.test(o))?s=700:t.dans.every(([e,t])=>t?t.test(o):o.indexOf(e)>=0)?s=500:t.nbs.length&&t.bordT.every(e=>e.test(l))&&t.dans.every(([e,t])=>!t||t.test(o))?s=680:t.nbs.length&&t.bordT.every(e=>e.test(o))&&t.nbs.every(e=>catPlage(o,e))?s=650:t.bord.every(e=>e.test(l))?s=320:t.nbs.length&&t.bordT.every(e=>e.test(l))&&t.nbs.every(e=>catPlage(l,e))?s=310:t.dans.every(([e,t])=>t?t.test(l):l.indexOf(e)>=0)&&(s=300),s&&e&&(s-=40),s>u&&(u=s)}u<=0||(/^[a-z_]+ ?\d/.test(o)&&(u+=40),s.has(e)||(s.add(e),i.push([u-Math.min(60,o.length),e])))}return i.sort((e,t)=>t[0]-e[0]||e[1]-t[1]),i}var CAT_I=-1;function catLignes(e,t){const r=$("catRes");if(!r)return;const s=artRanger(e,t),i=s.length,o=s.slice(0,40).map(e=>e[1]);r.innerHTML=o.length?o.map(e=>`
${ART.a[e][0]}${ART.a[e][1]}${fmt(ART.a[e][2])}
`).join("")+(i>40?`
${i} références correspondent — les 40 plus proches sont affichées.
`}function totaux(){const e=grpResume();$("tGroupes").innerHTML=tableGroupesHTML(e);const t=e.reduce((e,t)=>e+t.ht,0),r=e.reduce((e,t)=>e+t.rab,0),s=num("txRem"),i=(t-r)*s/100,o=t-r-i,l=num("txTva"),u=o*l/100;$("tSous").textContent=fmt(t),$("rRabG").hidden=0===r,$("tRabG").textContent="−"+fmt(r),$("rRem").hidden=0===s,$("tRemPc").textContent=s?s+" %":"",$("tRem").textContent="−"+fmt(i),$("tHT").textContent=fmt(o),$("tTvaPc").textContent=l+" %",$("tTVA").textContent=fmt(u),$("tTTC").textContent=fmt(Math.round(20*(o+u))/20);const d=positions.filter(e=>e.achiffrer),m=$("alertePrix");if(m)if(d.length){const e=d.reduce((e,t)=>e+t.q,0);m.style.display="block",m.innerHTML=`⚠ ${d.length} position(s) sans prix (${e} pièce(s)) — lignes en rouge ci-dessus. Tant qu'elles sont à 0.00, le total de l'offre est sous-évalué. Ce sont les monoblocs, grilles sur mesure et articles hors catalogue à faire chiffrer.`}else m.style.display="none";offMiroirRetour(),ligRender(),resRender()}const CONDITIONS_HTML="
CONDITIONS GÉNÉRALES DE VENTE ET DE LIVRAISON
\n
Article 1 : Application des conditions générales de vente
\n
Ces conditions générales de vente sont systématiquement portées à la connaissance de chaque acheteur lors de la passation de la commande.
\n
Air Ventil SA est ci-après dénommé le Vendeur.
\n
Article 2 : Garantie de l'acheteur
\n
Le Vendeur est tenu de respecter la garantie légale des vices cachés. Le Vendeur est tenu de la garantie à raison des défauts cachés de la chose qui la rendent impropre à l'usage duquel on la destine.
\n
Article 3 : Prise de commande
\n
Le Vendeur confirme l'acceptation de sa commande au client selon un des moyens suivants : fax, téléphone, courrier électronique ou courrier.
\n
Article 4 : Prix
\n
Les produits sont fournis au prix en vigueur au moment de la passation de la commande. Il ne comprend pas notamment la TVA.
\n
Article 5 : Offres
\n
Les prix des offres sont valables pendant 60 jours à compter de la date d'envoi.
\n
Article 6 : Livraison
\n
Les livraisons sont gratuites pour les commandes de plus de 1'000.00 CHF et pour les distances qui n'excèdent pas 60 Km au départ de nos ateliers. Les commandes dont nous assurons le transport voyagent à notre risque et sont garanties depuis le départ de nos ateliers jusqu'au lieu de la livraison.
\n
Une signature du bon de livraison est exigée, le client sera tenu responsable par la suite.
\n
Le client doit aider à décharger des marchandises.
\n
Les commandes dont le poids total n'excède pas 30kg sont expédiées par la poste, les frais de port sont calculés selon le poids.
\n
Article 7 : Réception
\n
A la réception des marchandises, l'acheteur doit immédiatement vérifier leur état et leur conformité par rapport à la commande. Les défauts éventuels doivent être annoncés, l'acheteur les faire constater par le livreur et la marchandise doit être refusée.
\n
Toutes les réclamations relatives à un défaut des marchandises livrées, à une inexactitude dans les quantités ou à leur référence erronée par rapport à l'offre acceptée doivent être formulées par écrit dans un délai de 48 heures à réception des marchandises. Passée ce délai, le droit à réclamation cessera d'être acquis.
\n
Pour le matériel reconnu défectueux, le remplacement de la marchandise est assuré.
\n
Article 8 : Retours - Modalité
\n
Les retours ne sont pas acceptés.
\n
Article 9 : Paiement
\n
Le prix des articles commandés est payable en totalité dans 30 jours net à partir de la date de la facturation.
\n
Article 10 : Litige
\n
En cas de litige, le client effectuera ses réclamations auprès du service client du Vendeur.
\n
Le for juridique est celui du Tribunal du District du siège du Vendeur.
${s} pièce${s>1?"s":""} · ${fmt(t)} kg de tôle · ${fmt(r)} m² développés
\n
Longueur indiquée = longueur à fabriquer. « Tôle (m²) » est la surface développée\n déduite du poids et du poids surfacique de la matière — ajouter les chutes de coupe et de pliage.\n Classe d'étanchéité : B = standard, C = étanche, D = désenfumage.
`}function construireAtelier(e){const t=$("docDate").value?new Date($("docDate").value+"T12:00:00"):new Date,r={date:("0"+t.getDate()).slice(-2)+"."+("0"+(t.getMonth()+1)).slice(-2)+"."+t.getFullYear(),chantier:$("docChantier").value||"",client:$("docClient").value||"",no:($("docNo").value||"").trim(),ref:$("docRef").value||"",ent:offSucc()},s={},i={},o=[];e.forEach(e=>{const t=positions[e];if(t.asm)return;const r=e+1;if(t.achiffrer)return void o.push({n:r,p:t});const l=t.grp||"Standard";if((s[l]=s[l]||[]).push({n:r,p:t}),t.fab&&t.fab.iso){const e=t.fab.iso;i[e]=i[e]||{m2:0,pos:[]},i[e].m2+=(t.fab.isoM2||0)*t.q,i[e].pos.push(r)}});const l=[];return[...AT_ORDRE,...Object.keys(s).filter(e=>AT_ORDRE.indexOf(e)<0)].forEach(e=>{const t=s[e];if(!t||!t.length)return;const r=e===GRP_QUA&&t.some(e=>e.p.fab);l.push([AT_TITRE[e]||e,r?atTableFab(t):atTableArt(t)])}),Object.keys(i).length&&l.push(["Isolation — à préparer",atTableIso(i)]),o.length&&l.push(["Positions à préciser — hors catalogue",atTableArt(o)]),$("printDoc").innerHTML=l.map((e,t)=>atSection(e[0],e[1],r,t+1,l.length)).join(""),document.title="Bon atelier"+(r.no?" "+r.no:"")+(r.chantier?" — "+r.chantier:""),l.length}function imprimerAtelier(){if(!positions.length)return void toast("Aucune position — ajoute d'abord des pièces.");const e=[...document.querySelectorAll(".asmchk:checked")].map(e=>+e.dataset.i),t=e.length?e:positions.map((e,t)=>t);PRINTMODE="atelier";const r=construireAtelier(t);if(!r)return PRINTMODE="offre",void toast("Rien à préparer dans la sélection.");toast(`Bon de préparation : ${r} lot${r>1?"s":""} de matériel`+(e.length?` (${e.length} position(s) cochée(s))`:" (toutes les positions)")),window.print()}function lignesTxt(e){return[["Pos","Groupe","Désignation","Qté","Poids unit. (kg)","PU (CHF)","Rabais %","Total HT2 (CHF)"].join(e),...positions.map((t,r)=>{const s=t.grp||"Standard",i=grpRab(s);return[r+1,s,t.desc,t.q,t.manuel?"":t.poids.toFixed(2),t.pu.toFixed(2),i.toFixed(2),r2(t.pu*t.q*(1-i/100)).toFixed(2)].join(e)})].join("\n")}function copierERP(){if(!positions.length)return void toast("Aucune position à copier.");const e=lignesTxt("\t");navigator.clipboard.writeText(e).then(()=>toast("Lignes copiées — colle-les dans ton ERP"),()=>{const t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),t.remove(),toast("Lignes copiées")})}function exportCSV(){if(!positions.length)return void toast("Aucune position à exporter.");const e=`Client;${$("docClient").value}\nChantier;${$("docChantier").value}\nDate;${$("docDate").value}\n\n`,t=new Blob(["\ufeff"+e+lignesTxt(";")],{type:"text/csv;charset=utf-8"}),r=document.createElement("a");r.href=URL.createObjectURL(t),r.download="calcul-ascv.csv",r.click(),URL.revokeObjectURL(r.href)}window.addEventListener("beforeprint",()=>{"offre"===PRINTMODE&&construireOffre()}),window.addEventListener("afterprint",()=>{PRINTMODE="offre"});const STORE_VER=1,STORE_KEY="air-ventil.calculateur";function storeVide(){return{ver:1,reglages:{},appris:{},synonymes:[],lexique:[],equiv:[],profils:{},clients:{},chantiers:{},articles:{},histo:[],paiement:{},entites:{},rond:{},ia:iaEtatDef(),maj:""}}let STORE=storeVide();function storeChamps(){return[...document.querySelectorAll("#tarifsBody input[type=number][id]")]}function storeCollecter(){const e={};return storeChamps().forEach(t=>{""!==t.value&&(e[t.id]=t.value)}),STORE.reglages=e,STORE}function storeAppliquer(){let e=0;return storeChamps().forEach(t=>{Object.prototype.hasOwnProperty.call(STORE.reglages,t.id)&&t.value!==STORE.reglages[t.id]&&(t.value=STORE.reglages[t.id],e++)}),e}let STORE_OK=!0,STORE_T=null;function storeSave(){if(STORE_OK&&KDATA)try{storeCollecter(),STORE.maj=(new Date).toISOString(),cfrEcrire(STORE_KEY,JSON.stringify(STORE))}catch(e){STORE_OK=!1}}function storeSaveDiff(){clearTimeout(STORE_T),STORE_T=setTimeout(storeSave,600)}async function storeLoad(){const e=await cfrLire(STORE_KEY);if(e){try{storeFusion(JSON.parse(e))}catch(e){}storeAppliquer(),storeMaj()}}function storeFusion(e){if(e&&"object"==typeof e){if(e.reglages&&(STORE.reglages={...STORE.reglages,...e.reglages}),e.appris&&(STORE.appris={...STORE.appris,...e.appris}),e.profils&&(STORE.profils={...STORE.profils,...e.profils}),e.clients&&(STORE.clients={...STORE.clients,...e.clients},CLI_LISTE=null,CTIDX=null),e.chantiers&&(STORE.chantiers={...STORE.chantiers||{},...e.chantiers},MAN_LISTE=null),e.articles&&(STORE.articles={...STORE.articles,...e.articles},CAT_PRET&&arfAppliquer()),Array.isArray(e.histo)){STORE.histo=STORE.histo||[];const t=new Set(STORE.histo.map(e=>(e||[]).join("|")));e.histo.forEach(e=>{Array.isArray(e)&&!t.has(e.join("|"))&&(STORE.histo.push(e),t.add(e.join("|")))})}if(e.paiement&&(STORE.paiement={...STORE.paiement||{},...e.paiement}),e.entites&&"object"==typeof e.entites&&(STORE.entites=STORE.entites||{},Object.keys(e.entites).forEach(t=>{e.entites[t]&&"object"==typeof e.entites[t]&&(STORE.entites[t]={...STORE.entites[t]||{},...e.entites[t]})})),e.rond&&(STORE.rond={...STORE.rond||{},...e.rond}),e.ia&&(STORE.ia={...iaEtatDef(),...STORE.ia,...e.ia}),Array.isArray(e.synonymes)){const t=new Set(STORE.synonymes.map(e=>norm(e[0])));e.synonymes.forEach(e=>{Array.isArray(e)&&2===e.length&&!t.has(norm(e[0]))&&(STORE.synonymes.push([e[0],e[1]]),t.add(norm(e[0])))})}if(Array.isArray(e.lexique)){STORE.lexique=STORE.lexique||[];const t=new Set(STORE.lexique.map(e=>norm(e[0])+"|"+norm(e[1])));e.lexique.forEach(e=>{Array.isArray(e)&&2===e.length&&!t.has(norm(e[0])+"|"+norm(e[1]))&&(STORE.lexique.push([e[0],e[1]]),t.add(norm(e[0])+"|"+norm(e[1])))})}if(Array.isArray(e.equiv)){STORE.equiv=STORE.equiv||[];const t=new Set(STORE.equiv.map(e=>norm(e[0])));e.equiv.forEach(e=>{Array.isArray(e)&&e.length>=2&&!t.has(norm(e[0]))&&(STORE.equiv.push([e[0],e[1],e[2]||""]),t.add(norm(e[0])))})}synInvalider(),lexInvalider(),eqvInvalider()}}function storeExporter(){storeCollecter(),STORE.maj=(new Date).toISOString();const e=new Blob([JSON.stringify(STORE,null,1)],{type:"application/json"}),t=document.createElement("a");t.href=URL.createObjectURL(e);const r=new Date,p=e=>("0"+e).slice(-2);t.download=`reglages-air-ventil-${r.getFullYear()}${p(r.getMonth()+1)}${p(r.getDate())}.json`,t.click(),URL.revokeObjectURL(t.href),toast("Réglages exportés — garde ce fichier, il te suivra d'une version à l'autre.")}function storeImporter(e){if(!estAdmin())return e.value="",void toast("Seul un administrateur peut remplacer les réglages et les tarifs.");const t=e.files&&e.files[0];if(!t)return;const r=new FileReader;r.onload=()=>{let t;try{t=JSON.parse(r.result)}catch(t){return toast("Fichier illisible — ce n'est pas un fichier de réglages."),void(e.value="")}storeFusion(t);const s=storeAppliquer();storeSave(),storeMaj(),apercu(),render(),toast(`Réglages repris : ${s} valeur(s), ${Object.keys(STORE.appris).length} correspondance(s) apprise(s), ${STORE.synonymes.length} synonyme(s).`),e.value=""},r.readAsText(t)}function storeReset(){if(estAdmin()){if(confirm("Effacer tes réglages, le dictionnaire appris et les profils de bureau ?\nExporte-les d'abord si tu veux les garder.\n\nTes offres enregistrées et tes comptes utilisateurs ne sont pas touchés.")){STORE=storeVide();try{localStorage.removeItem(STORE_KEY)}catch(e){}synInvalider(),storeMaj(),toast("Mémoire effacée — recharge la page pour retrouver les valeurs d'usine.")}}else toast("Seul un administrateur peut réinitialiser les tarifs.")}function storeMaj(){const e=$("storeEtat");if(!e)return;const t=Object.keys(STORE.appris).length,r=STORE.synonymes.length,s=Object.keys(STORE.profils).length,i=Object.keys(STORE.reglages).length,o=Object.keys(STORE.clients||{}).length,l=Object.keys(STORE.articles||{}).length,u=Object.keys(STORE.chantiers||{}).length;e.innerHTML=STORE_OK?`Mémoire active — ${i} réglage(s), ${t} correspondance(s) apprise(s), ${r} synonyme(s), ${s} profil(s) de bureau, ${o} fiche(s) client, ${l} fiche(s) article, ${u} chantier(s).`:"⚠ Ce navigateur refuse la sauvegarde automatique pour un fichier local. Utilise Exporter avant de fermer, et Importer en rouvrant."}const SYN_DEF=[["kanal","gaine"],["luftkanal","gaine"],["wickelfalzrohr","spiro"],["spiralrohr","spiro"],["bogen","coude"],["winkel","coude"],["segmentbogen","coude"],["uebergang","cone"],["übergang","cone"],["reduktion","cone"],["reduzierung","cone"],["etagenversatz","deviation"],["schalldaempfer","silencieux"],["schalldämpfer","silencieux"],["rohrschalldämpfer","silencieux"],["brandschutzklappe","clapet coupe-feu"],["absperrklappe","clapet"],["drosselklappe","clapet"],["rohrschelle","collier"],["schelle","collier"],["revisionsoeffnung","portillon de visite"],["revisionsöffnung","portillon de visite"],["wetterschutzgitter","grille pare-pluie"],["ausblasgitter","grille"],["dachhaube","sortie de toiture"],["flexrohr","flexible"],["flexschlauch","flexible"],["deckel","fond"],["enddeckel","fond"],["stutzen","piquage"],["abzweig","piquage"],["daemmung","isolation"],["dämmung","isolation"],["isolierung","isolation"],["laenge","longueur"],["länge","longueur"],["stk","pce"],["stueck","pce"],["stück","pce"],["canale","gaine"],["curva","coude"],["riduzione","cone"],["silenziatore","silencieux"],["serranda tagliafuoco","clapet coupe-feu"],["serranda","clapet"],["fascetta","collier"],["isolamento","isolation"],["griglia","grille"],["lunghezza","longueur"],["tubo spiro","spiro"],["conduit rectangulaire","gaine"],["conduite rectangulaire","gaine"],["gaine rectangulaire","gaine"],["conduit circulaire","spiro"],["conduite circulaire","spiro"],["tube spiralé","spiro"],["transformation","cone"],["adaptateur","cone"],["pièce de transformation","cone"],["changement de section","cone"],["about","piquage"],["collier de fixation","collier"],["trappe de visite","portillon de visite"],["registre","clapet"],["volet","clapet"],["silenc","silencieux"],["silenc.","silencieux"],["amort","amortisseur"],["cyl","cylindrique"],["circ","circulaire"],["rect","rectangulaire"],["lg","longueur"],["long","longueur"],["diam","ø"],["galv","galva"],["galvanisé","galva"],["quadr","quadratique"],["susp","suspension"]];let SYN_RE=null;function synTous(){return[...SYN_DEF,...STORE.synonymes]}function synInvalider(){SYN_RE=null,buildSyn()}function synCompiler(){const e=synTous().map(([e,t])=>[norm(e).trim(),norm(t).trim()]).filter(e=>e[0]&&e[1]&&e[0]!==e[1]).sort((e,t)=>t[0].length-e[0].length);return SYN_RE=e.map(([e,t])=>[new RegExp("(^|[^a-z0-9])"+e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(?![a-z0-9])","g"),t]),SYN_RE}function synAppliquer(e){const t=SYN_RE||synCompiler();let r=e;for(const[e,s]of t)r=r.replace(e,(e,t)=>t+s);return r}function buildSyn(){const e=$("synBox");e&&(e.innerHTML=STORE.synonymes.length?STORE.synonymes.map((e,t)=>`
${e[0]} → ${e[1]}\n
`).join(""):`
${SYN_DEF.length} traductions d'usine (allemand, italien, tournures courantes). Aucune de ta main pour l'instant.
`)}function synAjouter(){const e=$("synDe").value.trim(),t=$("synVers").value.trim();e&&t?(STORE.synonymes.push([e,t]),$("synDe").value="",$("synVers").value="",synInvalider(),storeSave(),storeMaj(),toast(`« ${e} » sera désormais lu comme « ${t} ».`)):toast("Indique le terme du bureau et son équivalent Air-Ventil.")}function synSuppr(e){STORE.synonymes.splice(e,1),synInvalider(),storeSave(),storeMaj()}function buildLex(){const e=$("lexBox");if(!e)return;const t=STORE.lexique||[];e.innerHTML=t.length?t.map((e,t)=>`
${e[0]} → ${e[1]}\n
`).join(""):`
${LEX_DEF.length} familles décrites d'usine. Aucune de ta main pour l'instant.
`}function lexAjouter(){const e=$("lexFam").value.trim(),t=$("lexMots").value.trim();e&&t?(STORE.lexique=STORE.lexique||[],STORE.lexique.push([e,t]),$("lexFam").value="",$("lexMots").value="",lexInvalider(),storeSave(),storeMaj(),toast(`Les références « ${e} » répondront désormais à : ${t}.`)):toast("Indique les préfixes de référence et les mots correspondants.")}function lexSuppr(e){STORE.lexique.splice(e,1),lexInvalider(),storeSave(),storeMaj()}const FZ_STOP=new Set("de des du dela la le les un une en pour avec sans et ou au aux sur par mm cm ml pce pcs pc piece pieces stk unite unites qte qty type ref no num pos position compris y compris selon dont environ env soit".split(/\s+/));function fzStem(e){return e.length>3&&("s"===e.charAt(e.length-1)||"x"===e.charAt(e.length-1))?e.slice(0,-1):e}function fzMots(e){return[...new Set(e.split(/[^a-z]+/).filter(e=>e.length>1&&!FZ_STOP.has(e)).map(fzStem))]}const EQV_SAUF_TUBE="coude|courbe|reduction|reducteur|piquage|derivation|dérivation|manchon|manchette|bride|collier|chapeau|clapet|silencieux|amortisseur|registre|bouche|grille|cone|cône|transformation|bouchon|fond|selle|support|isolation|calorifug|te 90|te 45|culotte|croix",EQV_DEF=[["slu","sl safe","silencieux / amortisseur de bruit cylindrique Lindab"],["manchette flexible","m_souple ronde, m_souple","manchette souple — ronde si la ligne porte un ø, quadratique sinon"],["manchette souple","m_souple ronde, m_souple","idem"],["manchon flexible","m_souple ronde, m_souple","idem"],["spiro","sr","gaine en tôle galvanisée de section ronde SPIRO = SR au catalogue",EQV_SAUF_TUBE],["gaine ronde","sr","tube spiro galvanisé",EQV_SAUF_TUBE],["gaine circulaire","sr","tube spiro galvanisé",EQV_SAUF_TUBE],["tube spiro","sr","tube spiro galvanisé",EQV_SAUF_TUBE]],EQV_INFO=[["collier, Rohrschelle, Schelle","BRDI"],["couvercle, Deckel","Fond quadratique"],["clapet VFC (Trox)","DS"],["CBR","chapeau bicônique"],["silencieux quadratique","SLQN (prix d'une coulisse × nombre)"],["coulisse amortisseur de bruit, coulisses pour gaines","SLQC (coulisses seules)"],["pièce de transformation, Übergang, adaptateur","cône"],["portillon / trappe de visite","RD"],["soupape LSV","OPF / OPT"]];let EQV_RE=null,EQVSZ=null;function eqvTous(){return[...STORE.equiv||[],...EQV_DEF]}function eqvInvalider(){EQV_RE=null,EQVSZ=null,EQVPL=null,"function"==typeof buildEqv&&buildEqv()}function eqvEsc(e){return String(e).replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function eqvCompiler(){const e=eqvTous().map(e=>[norm(e[0]||"").trim(),String(e[1]||"").split(",").map(e=>norm(e).trim()).filter(Boolean),e[3]?norm(String(e[3])):""]).filter(e=>e[0]&&e[1].length);return e.sort((e,t)=>t[0].length-e[0].length),EQV_RE=e.map(([e,t,r])=>[new RegExp("(^|[^a-z0-9])"+eqvEsc(e)+"(?![a-z0-9])"),t,r?new RegExp(r):null]),EQV_RE}function eqvRefs(e){const t=EQV_RE||eqvCompiler(),r=norm(e||"");for(const[e,s,i]of t)if(e.test(r)&&(!i||!i.test(r)))return s;return null}function eqvIndex(){return NORMREFS||chercherRef([]),NORMREFS}function eqvLongueurs(e,t){eqvIndex();const r=new RegExp("^"+eqvEsc(e)+" "+t+" l\\. ?(\\d{2,5})$"),s=[];for(let e=0;ee-t)}function eqvTailles(e){if(eqvIndex(),EQVSZ=EQVSZ||{},EQVSZ[e])return EQVSZ[e];const t=new RegExp("^"+eqvEsc(e)+" (\\d{2,4})/(\\d{2,4})$"),r=[];for(let e=0;ee[1]-t[1]),EQVPL[e]=r}function eqvPlage(e,t){for(const[r,s,i]of eqvPlages(e))if(t>=r&&t<=s)return i;return null}function eqvOrdre(e,t){const r=e.filter(e=>/\brond/.test(e)),s=e.filter(e=>!/\brond/.test(e));return t?r.concat(s):s.concat(r)}const PPDISPO={gpp:{alu:"alu",galva:"galva",v2a:"iv2a",v4a:"iv4a"},cn:{alu:"alu",galva:"galva",cuivre:"cuivre",v2a:"iv2a"},gpp_c:{alu:"alu",cuivre:"cuivre",v2a:"v2a"}},PPREPLI={alu:["alu","galva","v2a","v4a","cuivre"],galva:["galva","alu","v2a","v4a","cuivre"],cuivre:["cuivre","alu","galva","v2a"],v2a:["v2a","v4a","galva","alu"],v4a:["v4a","v2a","galva","alu"]};function ppMat(e,t){const r=" "+e+" "+(t&&t.ppMat?t.ppMat+" ":"");return/v ?4 ?a/.test(r)?"v4a":/inox|v ?2 ?a/.test(r)?"v2a":/cuivre/.test(r)?"cuivre":/galvanis|\bgalva\b/.test(r)?"galva":(/alu(?:m|man|minium)?\b/.test(r),"alu")}function ppOrdre(e,t){const r=PPDISPO[e]||{},s=[];for(const i of PPREPLI[t]||PPREPLI.alu){const t=r[i];t&&s.indexOf(e+" "+t)<0&&s.push(e+" "+t)}return s}function plageCote(e,t){return Math.round((+e+ +t)/2)}function libPlage(e,t){if(null==e||!ART.a[e]||!t)return null;const r=/\d{1,4}_\d{2,5}/,s=ART.a[e][0]||"",i=ART.a[e][1]||"";if(!r.test(s)&&!r.test(i))return null;const sub=e=>e.replace(new RegExp((/^\u00f8/.test(t)?"(?:ø ?)?":"")+"\\d{1,4}_\\d{2,5}"),t).replace(/\s{2,}/g," ").trim(),o=sub(s),l=sub(i);return o+(l&&l!==o?" — "+l:"")}function eqvCouvre(e,t,r){const s=Math.max(t,r),i=Math.min(t,r);for(const t of e){let e=null,r=1e12;for(const[o,l,u,d]of eqvTailles(t)){const t=Math.max(o,l),m=Math.min(o,l);t>=s&&m>=i&&d>0&&de>=i-50);void 0===s&&(s=r[r.length-1]);const o=chercherRef([`${e} ${t} l.${s}`]);if(null!==o)return o}}const r=chercherRef([`${e} ${t}`]);if(null!==r)return r;const s=eqvPlage(e,t);if(null!==s)return s}return null}function eqvRect(e,t,r){const s=eqvRefs(e);return s?eqvCouvre(eqvOrdre(s,!1),t,r):null}function buildEqv(){const e=$("eqvInfo");e&&(e.innerHTML="Déjà connues d'usine, inutile de les ressaisir : "+EQV_INFO.map(e=>`${e[0]} = ${e[1]}`).join(" · ")+".");const t=$("eqvBox");if(!t)return;const r=STORE.equiv||[];t.innerHTML=r.length?r.map((e,t)=>`
${e[0]} → ${e[1]}${e[2]?` (${e[2]})`:""}\n
`).join(""):`
${EQV_DEF.length} équivalence(s) d'usine : ${EQV_DEF.map(e=>e[0]+" → "+e[1]).join(", ")}. Aucune de ta main pour l'instant.
`}function eqvAjouter(){const e=$("eqvQ").value.trim(),t=$("eqvR").value.trim();e&&t?(STORE.equiv=STORE.equiv||[],STORE.equiv.push([e,t,""]),$("eqvQ").value="",$("eqvR").value="",eqvInvalider(),storeSave(),storeMaj(),toast(`« ${e} » sera cherché dans le catalogue sous « ${t} ».`)):toast("Indique le mot du bureau et le début de notre référence.")}function eqvSuppr(e){STORE.equiv=STORE.equiv||[],STORE.equiv.splice(e,1),eqvInvalider(),storeSave(),storeMaj()}function eqvEssai(){const e=$("eqvTestRes");if(!e)return;const t=($("eqvTest").value||"").trim();if(!t)return void(e.innerHTML="");let r;try{r=parseLigne(t,{label:"",cls:"B"})}catch(t){return void(e.innerHTML=`Erreur de lecture : ${t.message}`)}if(null!=r){if("article"===r.kind&&null!=r.art){const t=ART.a[r.art],s=null!=r.puOv?r.puOv:t[2];return void(e.innerHTML=`Chiffrée au catalogue : ${t[0]} — ${t[1]||""} · ${(+s).toFixed(2)} CHF l'unité${r.q?` · quantité lue : ${r.q}`:""}`)}if("piece"===r.kind){const t=r.params||{};return void(e.innerHTML=`Pièce calculée ASCV : ${t.ty||"?"} ${t.A||"?"}/${t.B||"?"}${t.L?` L.${t.L}`:""}${t.ang?` ${t.ang}°`:""} · ${t.mat||""} classe ${t.cls||""}${t.q?` · quantité ${t.q}`:""}`)}"inconnu"!==r.kind?e.innerHTML=`Lue comme ${r.kind}${r.titre?` : ${r.titre}`:""}.`:e.innerHTML="Reconnue comme position, mais aucune référence au catalogue : la ligne resterait en rouge « à chiffrer ». C'est ici qu'une équivalence peut aider."}else e.innerHTML="Ligne ignorée : l'app ne la considère pas comme une position à chiffrer."}const LEX_DEF=[["sr srp","tube spiro conduit rond gaine ronde tuyau spiralé canal wickelfalzrohr rohr luftkanal"],["srl srlm","tube lisse conduit lisse glattrohr"],["srd","tube double manteau isolé"],["il ilf ill","manchette bord bague piquage droit raccord stutzen"],["ilv","piquage oblique plat"],["ps psl psb pse","piquage tube selle"],["psv psvl","piquage oblique tube"],["em emf emg emgf emgv eml emd","fond couvercle bouchon deckel tappo"],["me med","manchette extérieure"],["mf","manchon femelle"],["np npd","manchon nipple raccord mâle"],["mc","manchon coulissant"],["metu","bride assemblage cadre"],["brdi","!collier suspension attache fixation schelle rohrschelle bügel"],["sb sbrl","serrage rapide"],["b90 b45 b30 b15 bd90 bd45 bd30","!coude !embouti courbe !bogen"],["bl90 bl45 bl30 bc90 bcl90","coude bord lisse bogen"],["bse bsel bselm","coude segments segmentbogen"],["rc rcl rcd rce","réduction concentrique réducteur reduktion"],["re rel red","réduction excentrique réducteur reduktion"],["t tc tb tbd","té piquage dérivation abzweig"],["tr trv","té réduit dérivation"],["trx trxv45 txc","té croix dérivation"],["yv30 yv45 yvl yvml tvl xvl","culotte té oblique"],["ds dsj dsd dsdk dsdkj dst","clapet réglage manuel registre volet drosselklappe"],["cr crj cm cmj","clapet réglage quadratique registre"],["cs","clapet surpression"],["ccf","clapet coupe feu !brandschutzklappe"],["cmd crd","clapet désenfumage motorisé"],["rsk","clapet"],["ibu","obturateur iris"],["sl slfx slrq","silencieux amortisseur bruit cylindrique schalldämpfer"],["slqn slqp slqr","silencieux amortisseur bruit quadratique rectangulaire schalldämpfer"],["slqc slqpc slqrc","coulisse baffle"],["dfhc dfhci dfhi cbr cbrs","chapeau bicônique refoulement sortie toiture"],["dh dhc dhp","chapeau chinois protection pluie regenhaube"],["clc clci clq clqs","chapeau lamelles"],["gpp gpp_c cn","grille pare pluie extérieure wetterschutzgitter"],["gp","grille phonique"],["grille","grille reprise soufflage"],["rdsr rdi rd","portillon visite trappe regard revisionsöffnung revisionsklappe"],["sc scq scqq scqs","col cygne"],["gr gst gsti gstp","garniture plaque collage sortie toiture"],["fl","bride plate"],["caisson","caisson plénum"],["collerette","collerette"],["fond","fond quadratique"],["m_t","manchette télescopique"],["m_souple","manchette souple joint"],["etv","étage décalage"],["afs afsi afr","tube souple flexible conduite souple flexrohr flexschlauch"],["opf","soupape"],["plaque","plaque collage"]];function lexTous(){return[...LEX_DEF,...STORE.lexique||[]]}let LEXMAP=null,LEXPRIO=null;function lexInvalider(){LEXMAP=null,LEXPRIO=null,FZ=null,"function"==typeof buildLex&&buildLex()}function lexTable(){if(LEXMAP)return LEXMAP;LEXMAP={},LEXPRIO={};for(const[e,t]of lexTous()){const r=norm(e).split(/\s+/).filter(Boolean),s=norm(t).split(/\s+/).filter(Boolean),i=fzMots(norm(t).replace(/!/g," "));for(const e of r)LEXMAP[e]=[...new Set([...LEXMAP[e]||[],...i])];for(const e of s)if("!"===e.charAt(0)){const t=fzMots(e.slice(1))[0];t&&(LEXPRIO[t]=new Set([...LEXPRIO[t]||[],...r]))}}return LEXMAP}function lexPrio(){return lexTable(),LEXPRIO}function lexFamille(e){return norm(e||"").split(/[^a-z0-9_]+/)[0]||""}const FZ_MAT=[["alu","alu aluminium"],["inox","inox v2a v4a"],["cu","cuivre cu"],["galva","galva galvanise safe"]];function fzMat(e){const t=" "+norm(String(e||"")).replace(/[^a-z0-9]+/g," ")+" ";for(const[e,r]of FZ_MAT)for(const s of r.split(" "))if(t.indexOf(" "+s+" ")>=0)return e;return null}const fzMatiere=fzMat,fzMatArticle=fzMat;let FZ=null;function fzBuild(){if(FZ)return FZ;const e=lexTable(),t=ART.a.length,r={},s=new Array(t),i=new Array(t),o=new Array(t),l=new Array(t),u=new Array(t),d=new Array(t),m={},h=/(?e>=10))],R=(e[A]||[]).filter(e=>b.indexOf(e)<0),E=[];let S;for(h.lastIndex=0;S=h.exec(t);){const e=+S[1],t=+S[2];t>e&&t<=3e3&&E.push([e,t])}s[g]=b,i[g]=C,o[g]=R,l[g]=fzMat(t),u[g]=E,d[g]=A;for(const e of b)r[e]=(r[e]||0)+1,(m[e]=m[e]||[]).push(g);for(const e of R)r[e]=(r[e]||0)+1,(m[e]=m[e]||[]).push(g);for(const e of C){const t="#"+e;(m[t]=m[t]||[]).push(g)}}const g={};for(const e in r)g[e]=Math.log(t/r[e]);return FZ={toks:s,nums:i,lex:o,mat:l,rng:u,fam:d,inv:m,idf:g,N:t},FZ}function fzDecoupe(e){const t=synAppliquer(" "+norm(e||"").replace(/(\d)['’](\d{3})/g,"$1$2")+" "),r=(t.match(/\d+/g)||[]).map(Number).filter(e=>e>=10&&e<=2e4);let s,i=/(\d{1,2})[.,]?(\d)?\s*m(?![a-z0-9])/g;for(;s=i.exec(t);){const e=1e3*+s[1]+(s[2]?100*+s[2]:0);e>=100&&e<=2e4&&r.push(e)}return{mots:fzMots(t),nbs:[...new Set(r)],brut:t}}function fzChercher(e,t,r){r=r||5;const s=fzBuild(),i=fzDecoupe(e);if(!i.mots.length&&!i.nbs.length)return[];const o=new Map,ajoute=e=>{const t=s.inv[e];if(t&&!(t.length>6e3))for(const e of t)o.set(e,1)};if(i.mots.forEach(e=>ajoute(e)),i.nbs.forEach(e=>ajoute("#"+e)),!o.size)return[];const l=i.mots.reduce((e,t)=>e+(s.idf[t]||0),0)||1,u=fzMat(i.brut),d=lexPrio(),m=i.mots.map(e=>d[e]).filter(Boolean),h=[];for(const e of o.keys()){if(t&&(ART.g[ART.a[e][3]]||"Standard")!==t)continue;const r=s.toks[e],o=s.nums[e],d=s.lex[e],g=s.rng[e];let b=0;for(const e of i.mots)r.indexOf(e)>=0?b+=s.idf[e]||0:d.indexOf(e)>=0&&(b+=.85*(s.idf[e]||0));const A=b/l;let C=1,R=0;if(i.nbs.length){for(const e of i.nbs)o.indexOf(e)>=0?R+=1:g.length&&g.some(t=>e>=t[0]&&e<=t[1])&&(R+=.8);C=Math.min(1,R/i.nbs.length)}const E=g.length?new Set([].concat(...g)):null,S=o.filter(e=>i.nbs.indexOf(e)<0&&!(E&&E.has(e))).length;let y=Math.min(.3,.12*S);const x=s.mat[e];u?x&&x!==u&&(y+=.22):x&&"galva"!==x&&(y+=.1);for(const t of m)t.has(s.fam[e])||(y+=.14);if(A<.08&&R<.001)continue;const M=Math.max(0,(i.nbs.length?.42*A+.58*C:A)-y);M>.12&&h.push({art:e,score:M,mots:A,nbs:C})}return h.sort((e,t)=>t.score-e.score||e.art-t.art),h.slice(0,r)}function apKeyExact(e){return synAppliquer(" "+norm(e||"").replace(/(\d)['’](\d{3})/g,"$1$2")+" ").replace(/^\s*\d+([.,]\d+)?\s*/," ").replace(/[^a-z0-9ø.]+/g," ").replace(/\s+/g," ").trim()}function apKeyMotif(e){const t=[];return{cle:apKeyExact(e).replace(/\d+/g,e=>(t.push(+e),"#")),nbs:t}}function apGabarit(e,t){const{nbs:r}=apKeyMotif(e);if(!r.length)return null;let s=norm(t),i=0;const o=new Set;return s=s.replace(/\d+/g,e=>{const t=r.indexOf(+e);return t>=0&&!o.has(t)?(o.add(t),i++,"{"+t+"}"):e}),i?s:null}function apGabaritRendu(e,t){let r=!0;const s=e.replace(/\{(\d+)\}/g,(e,s)=>void 0===t[+s]?(r=!1,e):String(t[+s]));return r?s:null}function apChercher(e){const t=apKeyExact(e),r=STORE.appris[t];if(r&&void 0!==r.art&&ART.a[r.art])return{art:r.art,score:1,via:"exact"};const{cle:s,nbs:i}=apKeyMotif(e),o=STORE.appris["~"+s];if(o&&o.gab){const e=apGabaritRendu(o.gab,i);if(e){const t=chercherRef([e]);if(null!==t)return{art:t,score:.6,via:"motif"}}}return null}function apRetenir(e,t,r){const s=apKeyExact(e);if(s){if(STORE.appris[s]={art:t},r){const r=apGabarit(e,ART.a[t][0]);r&&(STORE.appris["~"+apKeyMotif(e).cle]={gab:r})}storeSave(),storeMaj()}}function apOublier(e){const t=apKeyExact(e);delete STORE.appris[t],delete STORE.appris["~"+apKeyMotif(e).cle],delete STORE.appris[">"+t],storeSave(),storeMaj()}function apRetenirLigne(e,t){const r=apKeyExact(e);r&&t&&(STORE.appris[">"+r]={ligne:t},storeSave(),storeMaj())}function apLigne(e){const t=STORE.appris[">"+apKeyExact(e)];return t&&t.ligne?t.ligne:null}function apRejouer(e){const t=apLigne(e);if(!t)return null;let r=null;try{r=parseLigne(t,{label:"",cls:"B"})}catch(e){return null}return r&&"chap"!==r.kind&&("inconnu"!==r.kind||r.pu>0)?(r.src=e,r.viaIA="appris",r.iaCanon=t,r):null}document.getElementById("tarifsBody").addEventListener("input",storeSaveDiff),window.addEventListener("beforeunload",storeSave),document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&(storeSave(),"function"==typeof offSauver&&offSauver())});let IMP=[],REFIDX=null;function buildRefIdx(){REFIDX||(REFIDX={},ART.a.forEach((e,t)=>{const r=norm(e[0]).split(/[^a-z0-9]+/).filter(Boolean);r.length<2||(REFIDX[r[0]]=REFIDX[r[0]]||[]).push([t,r])}))}const TYPEKW=[["rondcarre",/rond[\s-]?carr|carre?[\s-]?rond|quadratique.{0,4}circulaire|circulaire.{0,4}quadratique/],["cone",/c[oô]ne|d[ée]viation|r[ée]duction|transformation/],["gaine",/gaine|canaux? rect|pi[èe]ces? droites?|canaux? droits?|conduits? droits?/],["coude",/coude/],["te",/(^|[^a-z])tes?([^a-z]|$)|t-?st[uü]ck/]],ISOKW=[["climatel",0],["climalene 9",1],["climalene 19",2],["dinaphone",3],["armaflex ext",4],["idikel",5],["pyramidale",6],["alu laine mineral",7],["armaflex 32",8],["armaflex 25",9],["laine minerale 50",10],["reflex",11],["armaflex 9",12],["laine de roch 25",13],["laine de roch 30",14],["minerale ep. 30",15],["laine de roche 30",16],["anti-feu ei60",17],["anti feu ei60",17],["ei60",17],["anti-feu ei30",18],["ei30",18],["thermique ep. 60",19],["thermique 100",20],["thermique ep. 30",21],["dinophone 22",22],["dinophone 50",23],["armaflex",9]],CATMAP=[[/bagues? d.?obturation|supports? (?:de )?thermometres?/,(e,t)=>{if(!(e>=130&&e<=150))return[];const r=/profondeur\D{0,12}(\d{2,3}) ?mm|(\d{2,3}) ?mm de profondeur/.exec(t);return(r?+(r[1]||r[2]):0)>60?["support thermometre 100","support thermometre 30"]:["support thermometre 30","support thermometre 100"]}],[/pare.{0,3}pluie|wetterschutzgitter|lucoma|\bwsg\b|\bwgs\b/,(e,t)=>{const r=[];for(const s of ppOrdre("gpp_c",ppMat(t,null)))r.push(s+" "+e);return r}],[/silencieux|amortisseur/,(e,t)=>{const r=t.match(/long(?:ueur)?\.? ?(?:de )?:? ?(\d{3,4})(?![\d])/)||t.match(/\bl ?\.? ?(\d{3,4})(?![\d])/),s=r?r[1]:"1000";return[`sl safe ${e} l.${s}`,`slnt ${e} l.${s}`,`sl safe ${e} l.1000`]}],[/piquage oblique sur plat|ilv/,e=>[`ilv 45 galva ${e}`,`ilv 45 safe ${e}`,`ilv 45 ${e}`]],[/piquage oblique sur tube|psv/,e=>[`psv safe ${e}`,`psv galva ${e}`,`psv ${e}`]],[/piquage sur tube/,e=>[`ps safe ${e}`,`ps galva ${e}`]],[/piquages? ronds?|piquage(?: \d{1,3} ?°)? ?sur ?gaine/,e=>[`il safe ${e}`,`il galva ${e}`]],[/piquage/,(e,t)=>{const r=[...new Set((t.match(/ø ?\d{2,4}/g)||[]).map(e=>+e.replace(/\D/g,"")))].filter(t=>t!==e)[0]||0;return(r>0&&/ sur /.test(t)?[`ps safe ${e}/${r}`,`psb safe ${e}/${r}`,`ps galva ${e}/${r}`,`psb galva ${e}/${r}`]:[]).concat([`il safe ${e}`,`il galva ${e}`])}],[/\bpvc\b|greydec/,e=>[`pvc ${e}`]],[/tubes? souples? isol|conduites? souples? isol|afsi/,e=>[`afsi ${e}`]],[/tubes? souples?|conduites? souples?|gaines? souples?|tuyau|flexible|afs/,e=>[`afs ${e}`]],[/brides? de serrage|colliers? de serrage/,e=>[`sb ${e}`]],[/colliers? avec joint/,e=>[`m_souple ronde ${e<=400?"0_400":e<=700?"401_700":e<=1e3?"701_1000":e<=1400?"1001_1400":e<=1700?"1401_1700":"1701_2000"}`]],[/colliers? de suspension|brdi/,e=>[`brdi ${e}`]],[/colliers?/,e=>[`brdi ${e}`]],[/bride d.assemblage|metu/,e=>[`metu galva ${e}`]],[/manchons? coulissants?/,e=>[`mc galva ${e}`]],[/manchons?/,e=>[`np safe ${e}`,`mf galva ${e}`]],[/fonds? grillages?|couvercles? grillages?|grilles? de fond/,e=>[`emg galva ${e}`,`emg safe ${e}`]],[/couvercles?|fonds?/,e=>[`emf galva ${e}`,`em galva ${e}`,`eml galva ${e}`]],[/te culotte|culotte/,e=>[`yv45 galva ${e}`]],[/te et te reduit|te reduit/,e=>e<=200?[`t emboutis safe ${e}/${e}`,`t roule ${e} galva`]:[`t roule ${e} galva`,`t roule ${e}`,`tr ${e}/${e}`]],[/reductions?/,(e,t)=>{const r=[...new Set((t.match(/ø ?\d{2,4}/g)||[]).map(e=>+e.replace(/\D/g,"")))];if(r.length>=2){const e=Math.max(r[0],r[1]),t=Math.min(r[0],r[1]);return[`rc safe ${e}/${t}`,`rc galva ${e}/${t}`,`rc ${e}/${t}`,`rce safe ${e}/${t}`,`rce galva ${e}/${t}`]}return[`rc safe ${e}`,`rc galva ${e}`,`rc ${e}`]}],[/(^|[^a-z])tes?([^a-z]|$)/,e=>[`tc safe ${e}`,`t emboutis safe ${e}`]],[/coupe.{0,3}feu|ccf/,e=>[`ccf ${e}`]],[/obturateur|\bibu\b/,e=>[`ibu ${e}`]],[/iris|\bspi\b/,e=>[`ds safe ${e}`,`ibu ${e}`]],[/chapeau|biconique|dfh/,e=>[`dfhc galva ${e}`,`dfhc safe ${e}`]],[/thermometre/,e=>["thermometre"]],[/clapets?/,e=>[`ds safe ${e}`,`cr ${e}`,`dsj ${e}`]],[/canaux|conduits?|tubes?|spiro|pi[èe]ces? droites?|gaines? (droites?|circulaires?|rondes?)/,e=>(e<=300?["0.5","0.6","0.8","1.0"]:e<=710?["0.6","0.8","0.5","1.0"]:e<=1e3?["0.8","1.0","0.6"]:["1.0","0.8"]).map(t=>`sr ${e} galva ${t}`).concat([`sr ${e} `])]];let NORMREFS=null,FIRSTIDX=null;function chercherRef(e){NORMREFS||(NORMREFS=ART.a.map(e=>norm(e[0])),FIRSTIDX={},NORMREFS.forEach((e,t)=>{const r=e.split(/[^a-z0-9.]+/)[0];(FIRSTIDX[r]=FIRSTIDX[r]||[]).push(t)}));for(const t of e){const e=FIRSTIDX[t.split(/[^a-z0-9.]+/)[0]];if(e)for(const r of e){const e=NORMREFS[r];if(e.startsWith(t)&&(e.length===t.length||!/[0-9]/.test(e[t.length])))return r}}return null}let SLQ_IDX=null;function slqnIndex(e){if(!SLQ_IDX){SLQ_IDX={SLQN:{},SLQC:{}};for(let e=0;ee-t),u=l.find(t=>e<=t)||l[l.length-1];if(!u)return null;const d=Object.keys(o[u]).map(Number).sort((e,t)=>e-t),m=d.find(e=>+t<=e)||d[d.length-1];if(!m)return null;const h=o[u][m],g=Object.keys(h).map(Number).sort((e,t)=>e-t),b=g[g.length-1];let A=r,C=0,R=[],E=[];for(;A>b;)C+=h[b][0],R.push(b),E.push(h[b][1]),A-=b;const S=(e=>{const t=g.find(t=>e<=t);return void 0===t?b:t})(A);return C+=h[S][0],R.push(S),E.push(h[S][1]),{unit:Math.round(100*C)/100,pu:Math.round(C*s*100)/100,h:u,c:m,det:R,fam:(i||"SLQN").toUpperCase(),nc:s,art:1===R.length?E[0]:null}}const CBR_T=[150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1e3,1200,1400,1600,1800,2e3];function cbrRef(e,t,r){const s=Math.max(e,t),i=CBR_T.find(e=>s<=e);if(!i)return null;const o=norm(r||"");return chercherRef([`cbr ${/cuivre|\bcu\b/.test(o)?"cu":/v4a/.test(o)?"iv4a":/v2a|inox/.test(o)?"iv2a":/zinc|titane/.test(o)?"zinc_t":/galva/.test(o)?"galva":"alu"} ${i}/${i}`,`cbr alu ${i}/${i}`,`cbr galva ${i}/${i}`])}let DFHC_IDX=null;function dfhcIndex(){if(DFHC_IDX)return DFHC_IDX;DFHC_IDX={};for(let e=0;ee-t);if(!o.length)return null;const l=o.find(t=>e<=t);return void 0!==l?{art:i[l],pu:ART.a[i[l]][2],d:l,extrap:0}:null}let KGLIST=null;function kgMatch(e,t){KGLIST||(KGLIST=[],ART.a.forEach((e,t)=>{const r=norm(e[0]).match(/^grille kg (\d+)\/(\d+)$/);r&&KGLIST.push([t,+r[1],+r[2]])}));const r=Math.max(e,t)+15,s=Math.min(e,t)+15;let i=null,o=1e9;for(const[e,t,l]of KGLIST){const u=Math.abs(t-r)+Math.abs(l-s);uMath.abs(e-t)<=6&&e!==t);s&&r.push(s);let i=null,o=1e9;for(const e of DIAMSTD){const r=Math.abs(e-t);r","":"=>","":"=>","":"lambda","":"µ","":"°","":"±"};function puaRepare(e){return/[-]/.test(e)?e.replace(/[-]/g,e=>void 0!==SYMPUA[e]?SYMPUA[e]:" "):e}function ocrRepare(e){return(e=(e=(e=e.replace(/[\u2192\u21d2]|-+>|\^>|=»/g,"=>")).replace(/(\d)['\u2019](\d) (\d{2})(?![\d])/g,"$1$2$3")).replace(/(^|[\s(>=])0\s?(\d{2,4})\s?mm\b/g,"$1ø$2 mm")).indexOf("ø")>=0&&(e=e.replace(/(^|[\s(>=])0\s?(\d{2,4})(?![\d.,])/g,"$1ø$2")),e=e.replace(/(^|\s)m[!(i\u00a1][.,]/g,"$1ml.")}function coteDe(e,t){const r=e.match(/(\d{2,4}) ?[x×\/*] ?(\d{2,4})/);if(r)return{rond:!1,A:+r[1],B:+r[2]};const s=e.match(/ø ?(\d{2,4})/)||("rond"===t?e.match(/(?:^|[\s:])(\d{2,4})(?: ?mm)?(?=\s|$)/):null);return s?{rond:!0,d:+s[1]}:null}const MOTCASSE=[[/\b(?:R\s+[ée]duction|R[ée]\s+duction|R[ée]du\s+ction|R[ée]duc\s+tion)/gi,"Réduction"],[/\b(?:C\s+oude|Co\s+ude|Cou\s+de)/gi,"Coude"],[/\b(?:M\s+anchon|Ma\s+nchon|Man\s+chon|Manch\s+on)/gi,"Manchon"],[/\b(?:P\s+iquage|Pi\s+quage|Piq\s+uage|Piqu\s+age)/gi,"Piquage"],[/\b(?:G\s+aine|Ga\s+ine|Gai\s+ne)/gi,"Gaine"],[/\b(?:S\s+ilencieux|Si\s+lencieux|Sil\s+encieux|Silen\s+cieux)/gi,"Silencieux"],[/\b(?:C\s+ollier|Co\s+llier|Col\s+lier|Coll\s+ier)/gi,"Collier"],[/\b(?:Trans\s+formation|Transfor\s+mation|Transform\s+ation)/gi,"Transformation"]];function motRecolle(e){if(!/[A-Za-z]\s+[a-zà-ÿ]/.test(e))return e;for(const[t,r]of MOTCASSE)e=e.replace(t,e=>/[A-Z]/.test(e[0])?r:r.toLowerCase());return e}const RUBKW=/canaux|conduits?|conduites?|gaines?|pieces?|coudes?|piquages?|manchons?|manchettes?|fonds?|couvercles?|\btes?\b|reductions?|reducteurs?|transformations?|derivations?|cones?|deviations?|brides?|colliers?|silencieux|amortisseurs?|grilles?|clapets?|bouches?|diffuseurs?|portillons?|chapeaux?|soupapes?|registres?|spiro|tubes?|tuyaux?|flexibles?|culottes?|te droit|te oblique/,BRUITKW=/selon|compris|norme|debit|resistance|puissance|tension|frequence|classe ie|encrassement|pertes? de charge|niveau sonore|plaquettes?|etiquettes?|chainette|indicatrice|reperage|adressage|mise en service|trou de mesure|obturation|maximale?|minimale?|profondeur|documentation|garantie|separateurs? de gouttes|condensat|baffles?|dim(?:ension)?\.? env|a confirmer|indications? de l|equipotentialite|mise a terre|m3\/h|m³\/h|\bsol ?:|\bplafond ?:|\bmur ?:/,PLASTKW=/\bpps?\b|\bppn\b|\bpehd\b|\bpe-hd\b|\bpvdf\b|polypropylene|polyethylene|matieres? (?:synthetiques?|plastiques?)|\bplastiques?\b|kunststoff/;function estPlastique(e){return PLASTKW.test(e)&&!/\bpvc\b|greydec/.test(e)&&!/galvanis|\binox\b|\bv ?[24] ?a\b|\bacier\b|\balu(?:m|man|minium)?\b|cuivre/.test(e)}const RUBNON=/\b(selon|compris|conforme|norme|inclus|prix|total|report|page|sous-total|remarque|offre|position)\b/,CHAMPFICHE=/^(marque|type|dimensions?|inclus|avec|sans|pos|report|total|page|ex[eé]cution|mat[eé]riaux?|mat[iè]ere?|mati[eè]re|montage|module|longueur|hauteur|largeur|profondeur|d[eé]bit|vitesse|niveau|perte|nombre|[eé]paisseur|fournisseur|mod[eè]le|couleur|finition|classe|norme|puissance|poids|isolation|coloris|teinte|raccordement|fixation)\b/i,CMD_MAT=["safe","galva","alu","iv2a","iv4a","inox","cu"],CMD_SLL=[300,500,700,1e3,1500,2e3];function cmdRefCourte(e,t){const r=2===t.length?[t[0]+"/"+t[1],t[1]+"/"+t[0]]:[t[0]];for(const t of r){const r=chercherRef([e+" "+t]);if(null!==r)return{art:r,sur:!1}}for(const t of CMD_MAT)for(const s of r){const r=chercherRef([e+" "+t+" "+s]);if(null!==r)return{art:r,sur:!0}}return null}function cmdAtelier(e,t,r){if(r&&r.plast)return;if(e.length>70)return;const s=t.replace(/\s+/g," ").trim();if(!s)return;const i=s.match(/^(\d{1,3}) ?x +/),o=i?+i[1]:1,l=(i?s.slice(i[0].length):s).trim();if(!(o>=1&&o<=999))return;const u=l.match(/^(\d{2,4}) ?[x×\/] ?(\d{2,4}) +(\d{3,4}) ?mm\b(.*)$/);if(u){const t=+u[1],s=+u[2],i=+u[3];let l=u[4]||"";const d=/\breglables?\b/.test(l);l=l.replace(/\breglables?\b/g," ").trim();const m=l.match(/^m ?(\d{2}|ez)$/);if((""===l||m)&&t>=100&&t<=2500&&s>=100&&s<=2500&&i>=200&&i<=3e3){let l="";return d&&(l+=" reglable"),m&&(l+=" M"+m[1].toUpperCase()),{kind:"piece",src:e,cmdAt:!0,params:{ty:"gaine",A:t,B:s,L:i,ang:90,mat:r&&r.qua&&r.qua.mat||"galva",cls:r&&r.cls||"B",isoIdx:null,q:o,note:l.replace("reglable","réglable"),metres:0}}}return}const d=l.match(/^sr +(\d{2,4}) +(\d{2,4})$/);if(d){let t=+d[1],r=+d[2];if(t=250&&t<=6e3&&r>=60&&r<=1600){const s=chercherRef(["sr "+r+" galva","sr "+r+" "]);if(null!==s){return{kind:"article",src:e,art:s,q:Math.round(o*t/1e3*100)/100,cmdAt:!0,cmdM:!0}}}return}const m=l.match(/^sl +(\d{3,4}) ?mm? +\u00f8 ?(\d{2,4})\b/)||l.match(/^sl +\u00f8 ?(\d{2,4}) +(\d{3,4}) ?mm?\b/);if(m){let t,r;if(/^sl +\d{3,4}/.test(l)?(t=+m[1],r=+m[2]):(r=+m[1],t=+m[2]),r>=60&&r<=1600&&t>=200&&t<=3e3){let s=chercherRef(["sl safe "+r+" l."+t,"sl "+r+" l."+t]);if(null===s){const i=CMD_SLL.slice().sort((e,r)=>Math.abs(e-t)-Math.abs(r-t));for(const e of i)if(s=chercherRef(["sl safe "+r+" l."+e,"sl "+r+" l."+e]),null!==s)break;if(null!==s)return{kind:"article",src:e,art:s,q:o,cmdAt:!0,cmdSur:!0}}if(null!==s)return{kind:"article",src:e,art:s,q:o,cmdAt:!0}}return}const h=l.match(/^([a-z]{2,6}) +(\d{2,4})(?: ?\/ ?(\d{2,4}))?$/);if(h){const t=h[3]?[h[2],h[3]]:[h[2]],r=cmdRefCourte(h[1],t);if(r)return{kind:"article",src:e,art:r.art,q:o,cmdAt:!0,cmdSur:r.sur}}}function parseLigne(e,t){e=motRecolle(puaRepare(e));const r=t.label,s=e.replace(/\.{2,}/g," ").replace(/\s+/g," ").trim();if(!s)return null;const i=synAppliquer(" "+ocrRepare(norm(s).replace(/(\d)['\u2019](\d{3})/g,"$1$2"))+" ");{const e=cmdAtelier(s,i,t);if(void 0!==e)return e}if(/etanch|eurovent|desenfum/.test(i)){const e=i.match(/classe\s*d['’]?\s*etancheite\s*:?\s*([bcd])(?![a-z])/)||i.match(/etancheite\s*(?:a\s*respecter\s*)?classe\s*:?\s*([bcd])(?![a-z])/)||i.match(/classe\s*:?\s*([bcd])(?![a-z'’])/);e&&(t.cls=e[1].toUpperCase())}/(reseaux?|gaines?|conduits?|conduites?|canaux|tuyauteries?)/.test(i)&&!/\d{2,}/.test(i)&&(estPlastique(i)?t.plast=!0:/galvanis|acier|\btole|inox|\balu|cuivre|\bv ?[24] ?a\b/.test(i)&&(t.plast=!1)),/(conduites?|conduits?|gaines?|reseaux?|canaux) (?:\w+ ){0,2}(quadratiques?|rectangulaires?)/.test(i)&&!estPlastique(i)&&(t.qua={mat:/inox v ?4 ?a/.test(i)?"v4a":/inox/.test(i)?"inox":/cuivre/.test(i)?"cuivre":/alu/.test(i)?"alu":"galva",cls:(i.match(/classe ?([bcd])\b/)||[])[1]?i.match(/classe ?([bcd])\b/)[1].toUpperCase():t.cls||"B"}),/(gaines?|canaux|conduits?|conduites?|reseaux?|tuyauteries?)\s*(?:\w+\s+){0,3}(circulaires?|rondes?|spiro)/.test(i)?t.forme="rond":/(gaines?|canaux|conduits?|conduites?|reseaux?)\s*(?:\w+\s+){0,3}(quadratiques?|rectangulaires?|carrees?)/.test(i)&&(t.forme="quad");const o=/^ ?- /.test(i);if(/compris\s+(?:dans|au|aux)\s+(?:la\s+position|le\s+prix|ce\s+prix|les\s+prix|cette\s+position)/.test(i)||/inclus\s+dans\s+(?:la\s+position|le\s+prix|ce\s+prix)/.test(i)||/prestations?\s+comprises?\s*:/.test(i)?t.inclus=!0:t.inclus&&(/[øØ]/.test(s)||/\d{2,}/.test(i))&&(t.inclus=!1),o&&!t.inclus){const e=i.replace(/^ ?- /,"").replace(/ø.*$/,"").replace(/\d{2,4} ?[x×\/].*$/,"").replace(/\bmm\b.*$/,"").trim();e.length>=2&&(t.label=e)}let l=o;const u=/^ ?(grilles?|clapets?|chapeaux?|silencieux|amortisseurs?|portillons?|thermometres?|bouchons?|unites terminales|diffuseurs?|soupapes?|tubes? souples?|conduites? souples?|colliers?|brides?|manchons?|manchettes?)\b/,d=s.replace(/^\s*\d{1,3}(?:\.\d{1,3}){0,2}\s*[.)]\s+/,""),m=/^[A-ZÈÉÀÇÂÎÔÛÄÖÜ]/.test(d),h=i.trim().replace(/^\d{1,3}(?:\.\d{1,3}){0,2}[.)] */,""),g=s.match(/^\s*\d{1,3}(?:\.\d{1,3}){0,2}\s*[.)]\s+([A-Za-zÀ-ÿ'’\-]{3,})/);if(g&&!o&&t.label){const e=" "+norm(g[1])+" ";RUBKW.test(e)||u.test(e)||(t.label=null)}!o&&m&&!/\. +[A-ZÈÉÀÇÂÎÔÛÄÖÜ]/.test(d)&&u.test(h+" ")?(t.label=h.replace(/ø.*$/,"").replace(/\d{2,4} ?[x×\/*].*$/,"").slice(0,80).trim(),l=!0):!o&&m&&!/^\s/.test(e)&&!/\d/.test(s)&&s.trim().length>=4&&s.trim().length<=40&&s.trim().split(/\s+/).length<=3&&!CHAMPFICHE.test(s.trim())&&!CHAMPFICHE.test(i.trim())&&(t.label=null);const b=norm(t.prec||"").replace(/[\s.:]+$/,""),A=/[,;\-–—’']$/.test(b)||/(?:^|\s)(de|des|du|d|le|la|les|l|un|une|au|aux|et|ou|en|pour|avec|sans|sur|sous|par|dans|entre|vers|selon|ainsi que|type de|comprenant|incluant)$/.test(b);l||o||m||A||!u.test(h+" ")||/[øØ]/.test(s)||/\b(pces?|pcs?|stk|st|ml|bl)\b/.test(i)||/\d{2,4} ?[x×\/] ?\d{2,4}/.test(i)||BRUITKW.test(i)||!(h.length>=8)||!(h.length<=90)||(t.label=h.replace(/\s+/g," ").slice(0,80).trim(),l=!0);const C=(e.replace(/\.{2,}/g," ").replace(/\t/g," ").trim().split(/\s{2,}/)[0]||"").trim();if(!l&&C.length>=3&&C.length<=60&&!/^[:=]/.test(C)&&!/\d{2,}/.test(C.replace(/\d{1,3}\s*°/g,""))&&C.split(/\s+/).length<=8&&RUBKW.test(norm(C))&&!RUBNON.test(norm(C))&&(t.label=norm(C),l=!0),!l){const e=s.match(/^ *[-\u2013\u2022]* *((?:[A-Za-z\u00c0-\u00d6\u00d9-\u00f6\u00f9-\u017f'\u2019()\/.]+|\d{1,3} *\u00b0)(?: +(?:[A-Za-z\u00c0-\u00d6\u00d9-\u00f6\u00f9-\u017f'\u2019()\/.]+|\d{1,3} *\u00b0))*) *:? *(?=[\u00d8\u00f8]|\d)/);if(e){const r=norm(e[1].trim());r.length>=3&&r.length<=45&&r.split(/\s+/).length<=6&&RUBKW.test(r)&&!RUBNON.test(r)&&(t.label=r,l=!0)}}if(t.label!==r){t.ppMat=null,t.ppHoriz=!1;const e=" "+(t.label||"")+" ";t.label&&(estPlastique(e)?t.plast=!0:(/galvanis|\bacier\b|\btole\b|\binox\b|\balu(?:m|man|minium)?\b|cuivre|\bv ?[24] ?a\b|spiro/.test(e)||u.test(e.slice(1)))&&(t.plast=!1))}if(estPlastique(i)&&(CHAMPFICHE.test(i.trim())||/^(execution|ex[eé]cution|protection|revetement|rev[eê]tement)\b/.test(i.trim()))&&(t.plast=!0),/tube|tuyau|conduite|flexible|souple/.test(t.label||"")&&/\bpvc\b|greydec/.test(i)&&!/pvc/.test(t.label||"")&&(t.label+=" pvc"),/silencieux/.test(t.label||"")&&/type /.test(i)){const e=i.match(/[x×] ?(\d{3,4}) ?\//);e&&(t.label=t.label.replace(/ l\.\d+/,"")+" l."+e[1])}if(/silencieux|amortisseur/.test(t.label||"")){const e=i.match(/longueur ?(?:de )?:? ?(\d{3,4})(?![\d])/);e&&(t.label=t.label.replace(/ l\.\d+/,"")+" l."+e[1])}if(/grille/.test(t.label||"")&&(!/pare.{0,3}pluie/.test(t.label)&&/lucoma|wetterschutzgitter|\bwsg\b|\bwgs\b|contre la pluie|anti.?pluie|intemperies/.test(i)&&(t.label+=" pare-pluie"),/pare.{0,3}pluie|lucoma|\bwsg\b|\bwgs\b/.test(t.label)&&(/toiture|en toit|horizontale?\b|sur le toit/.test(i)&&(t.ppHoriz=!0),/facade|verticale?\b|mur\b/.test(i)&&(t.ppHoriz=!1),CHAMPFICHE.test(i.trim())||/^(aluminium|acier|inox|cuivre|alu)\b/.test(i.trim())))){const e=/v ?4 ?a/.test(i)?"v4a":/inoxydable|\binox\b|v ?2 ?a/.test(i)?"v2a":/cuivre/.test(i)?"cuivre":/galvanis/.test(i)?"galva":/alu(?:m|man|minium)?\b/.test(i)?"alu":null;e&&(t.ppMat=e)}const R=" "+(t.label||"")+" ";let E=i.split(/[^a-z]+/).filter(Boolean).filter(e=>!["x","h","p","m","mm","o","ml","mt","mi","pc","pce","pces","pcs","st","stk","bl","bloc","blocs","fr","chf","kg","dimensions","dimension","diametre","diametres","marque","type","env"].includes(e)).join("").length<4;/^[:=]/.test(C)&&(E=!0);const S=l||E?R+" "+i:i;if(t.plast||estPlastique(i))return;const y=i.replace(/(\d[\d\s'.,]*)\s?m\s?[³²3]\s?\/\s?h/g," ").replace(/(\d[\d\s'.,]*)\s?m\s?[³²]/g," ").replace(/(\d[\d\s'.,]*)\s?m\s?\/\s?s\b/g," ");let x=null,M=1;const L=y.match(/ (\d{1,5}(?:[.,]\d{1,2})?) ?(ml|mt|mi|m|pces?|pcs?|p|blocs?|bl|stk|st)\.? *$/);if(L)x=L[2],M=Math.round(parseFloat(L[1].replace(",",".")));else{const e=[...y.matchAll(/[ :](ml|mt|mi|m|pces?|pcs?|p|blocs?|bl|stk|st)[.,;!]* +(\d{1,5})(?:[.,]\d{1,2})?(?![\d.,\/x×])/g)];if(e.length){const t=e[e.length-1];x=t[1],M=Math.round(parseFloat(t[2]))}}if(x&&(x=/^(ml|mt|mi|m)$/.test(x)?"m":/^bl/.test(x)?"bloc":"p"),null===x){let e=y.match(/^ (\d{1,3}) ?[x×] (?=[a-z])/)||y.match(/^ (\d{1,3})[x×] +(?=\d)/)||y.match(/^ (\d{1,3}) (?![x×] ?\d)(?=[a-z])/);if(e)M=+e[1];else if(e=y.match(/ (\d{1,4}) ?(?:pce?s?|pieces?|stk)\b/),e)M=+e[1];else{const e=[...y.matchAll(/ (\d{1,4}(?:[.,]\d+)?) ?m['’]?l?(?![a-z0-9²³\/])/g)];if(e.length)M=Math.round(parseFloat(e[e.length-1][1].replace(",","."))),x="m";else{const e=y.match(/ (\d{1,4})[.,]0 *$/);e&&(M=+e[1])}}}M>=1||(M=1);const I=i.match(/\b([xm]s ?-? ?f)\s?\/\s?(\d{3,4}) ?x ?(\d{3,4}) ?x ?(\d{3,4}) ?\/ ?(\d{1,2}) ?x ?(\d{2,3})/);if(I){const[,e,t,r,s,i,o]=I,l=slqnPrix(+i*+o>+t&&+i*+o<=+r?+r:+t,+o,+s,+i),u=`Amortisseur de bruit ${t}/${r} L.${s} avec ${i} coulisse de ${l?l.c:o}`;return l?{kind:"inconnu",q:M,src:u,mono:!0,slqn:l,pu:l.pu}:{kind:"inconnu",q:M,src:u,mono:!0}}if(/cou[a-z]{0,2}isse/.test(norm((t.prec||"")+" "+s))){const e=norm(s).replace(/(\d)['’](\d{3})/g,"$1$2"),r=norm((t.prec||"")+" "+s).replace(/(\d)['’](\d{3})/g,"$1$2"),i=r.match(/(\d{1,2}) ?cou[a-z]{0,2}isses? ?(?:de )?(\d{2,3})?/),o=e.match(/(\d{3,4}) ?[\/,.] ?(\d{3,4})/);let l=e.match(/longueur ?:? ?(?:de )?(\d{3,4})/);if(!l){const t=e.match(/longueur ?:? ?(?:de )?([\dolit]['’ ]?[\do]{3})\b/);if(t){const e=t[1].replace(/['’ ]/g,"").replace(/[olit]/g,e=>"o"===e?"0":"1");/^\d{4}$/.test(e)&&(l=[t[0],e])}}if(i&&o&&l){const e=+i[1],u=+(i[2]||100),d=+l[1];let m=+o[1],h=+o[2];if(e*u>m&&e*u<=h){const e=m;m=h,h=e}if(/cou[a-z]{0,2}isses? ?(?:de )?\d{0,3} ?(?:mm )?pour (?:les )?gaines?/.test(r)||/cou[a-z]{0,2}isse/.test(norm(t.label||""))){const t=slqnPrix(m,u,d,e,"SLQC");if(t&&null!==t.art)return{kind:"article",src:s,art:t.art,q:M*e,slqc:t,libOv:`Coulisse en tôle galvanisée ${m}/${h} L.${d} Largeur: ${t.c}`};if(t)return{kind:"inconnu",q:M,mono:!0,slqn:t,pu:t.pu,src:`Coulisses en tôle galvanisée ${m}/${h} L.${d} — ${e} × largeur ${t.c}`}}const g=slqnPrix(m,u,d,e);if(g)return{kind:"inconnu",q:M,mono:!0,slqn:g,pu:g.pu,src:`Amortisseur de bruit galva ${m}/${h} L.${d} avec ${e} coulisse de ${g.c}`}}else if(i&&(o||l)&&/pour (?:les )?gaines?|longueur/.test(e))return{kind:"inconnu",q:M,src:`${i[1]} coulisses de ${i[2]||100} — ${o?o[1]+"/"+o[2]:"section illisible"} L.${l?l[1]:"?"} (à compléter : scan illisible)`}}let _=0;if(t.qua){let e=i.match(/ (\d{1,6})(?:[.,](\d{1,3}))? ?kgs?(?![a-z])/);e?_=parseFloat(e[1]+(e[2]?"."+e[2]:"")):(e=i.match(/ kgs?\.? ?(\d{1,3}(?:[ '’]\d{3})+|\d{1,6})(?:[.,](\d{1,3}))?(?![\d])/),e&&(_=parseFloat(e[1].replace(/[ '’]/g,"")+(e[2]?"."+e[2]:""))))}if(_>0&&/gaines? droites?|pieces? droites?|pieces? de formes?|pieces? formes?/.test(S))return{kind:"kg",src:s,q:_,params:{kind:/gaines? droites?|pieces? droites?/.test(S)?"gd":"pf",mat:t.qua.mat,cls:t.qua.cls}};if(/silencieux|amortisseur/.test(t.label||"")){const e=i.match(/\/ ?(\d{2,4}) ?[x×] ?(\d{3,4}) ?\/ ?(\d{2,3})(?![0-9])/);if(e){const t=chercherRef([`sl safe ${+e[1]} l.${+e[2]}`,`slnt ${+e[1]} l.${+e[2]}`,`sl safe ${+e[1]} l.1000`]);return null!==t?{kind:"article",src:s,art:t,q:M}:{kind:"inconnu",q:M,src:s}}}if(/=>/.test(i)&&!/coupe.{0,3}feu|grille|clapet|portillon|chapeau/.test(S)){const e=i.split("=>"),r=coteDe(e[0],t.forme),o=coteDe(e[1],t.forme);if(r&&o){if(r.rond&&o.rond){const e=Math.max(r.d,o.d),t=Math.min(r.d,o.d),i=chercherRef([`rc safe ${e}/${t}`,`rc galva ${e}/${t}`,`rce safe ${e}/${t}`,`rc ${e}/${t}`]);return null!==i?{kind:"article",src:s,art:i,q:M}:{kind:"inconnu",src:s,q:M}}let e="galva";/v4a/.test(S)?e="v4a":/inox|v2a/.test(S)?e="inox":/alu/.test(S)?e="alu":/cuivre| cu /.test(S)&&(e="cuivre");let l=t.cls||"B";/etanche|cl\.? ?c\b/.test(i)&&(l="C"),/desenfum|cl\.? ?d\b/.test(i)&&(l="D");const u=i.match(/(?:^|[^a-z])(?:longueur|long|lg|l) ?[.:=]? ?(\d{3,4})\b/),d=u?+u[1]:0;if(r.rond||o.rond){const t=r.rond?o:r;if(t.A>=100&&t.B>=100)return{kind:"piece",src:s,params:{ty:"rondcarre",A:t.A,B:t.B,L:d,ang:90,mat:e,cls:l,isoIdx:null,q:M,note:"",metres:0}}}else{const t=r.A*r.B>=o.A*o.B?r:o;if(t.A>=100&&t.B>=100)return{kind:"piece",src:s,params:{ty:"cone",A:t.A,B:t.B,L:d,ang:90,mat:e,cls:l,isoIdx:null,q:M,note:"",metres:0}}}}}if("rond"===t.forme&&!/=>/.test(i)&&/reduction|reducteur|transformation/.test(S)){const e=i.match(/(\d{2,4}) ?[\/x×] ?(\d{2,4})/);if(e&&+e[1]>=60&&+e[2]>=60){const t=Math.max(+e[1],+e[2]),r=Math.min(+e[1],+e[2]),i=chercherRef([`rc safe ${t}/${r}`,`rc galva ${t}/${r}`,`rce safe ${t}/${r}`,`rc ${t}/${r}`]);return null!==i?{kind:"article",src:s,art:i,q:M}:{kind:"inconnu",src:s,q:M}}}const F=i.match(/(\d{2,4}) ?[x×\/*] ?(\d{2,4})(?: ?(?:mm)? ?(?:[x×] ?)?h?(\d{2,4}))?/),D=!F&&i.match(/ø ?(\d{2,4})/);let O=D;if(!F&&!D&&"rond"===t.forme&&x&&!/longueur|hauteur|largeur|epaisseur|poids|surface|classe|debit|pression|vitesse|temperature|sonore|perte de charge|puissance|maximale?|minimale?|profondeur|tolerance|rendement|humidite/.test(i)){const e=i.match(/(?:^|[\s:])(\d{2,4}) ?mm\b/)||i.match(/(?:^|[\s:])(\d{2,4})(?=\s)/);e&&+e[1]>=60&&+e[1]<=2e3&&(O=e)}if(F&&+F[1]>=40&&+F[2]>=40){{const e=eqvRect(S,+F[1],+F[2]);if(null!==e)return{kind:"article",src:s,art:e,q:M,libOv:libPlage(e,F[1]+"/"+F[2])}}if(/coupe.{0,3}feu|brandschutz/.test(S)&&/clapet|volet|ccf|klappe/.test(S)&&!/manchette|manchon|cadre|habillage|gaine/.test(S))return;if(/piquage/.test(S)){const e=Math.max(+F[1],+F[2]);let t=null;if(!/90\s*°?/.test(i)){const r=e<=400?"0_400":e<=700?"401_700":e<=1e3?"701_1000":e<=1400?"1001_1400":e<=2e3?"1401_2000":e<=2400?"2001_2400":e<=2700?"2401_2700":"2701_3000";t=chercherRef([`decoupe biseau 45° ${r}`,`decoupe biseau 45 ${r}`,`decoupe biseau ${r}`])}else{t=chercherRef([`manchette ${e<=400?"0_400":e<=700?"401_700":e<=1e3?"701_1000":e<=1400?"1001_1400":e<=1700?"1401_1700":"1701_2000"}`])}return null!==t?{kind:"article",src:s,art:t,q:M,libOv:libPlage(t,F[1]+"/"+F[2])}:{kind:"inconnu",src:s,q:M}}if(/chapeau|biconique/.test(S)){const e=cbrRef(+F[1],+F[2],i);return null!==e?{kind:"article",src:s,art:e,q:M}:{kind:"inconnu",src:s,q:M}}if(/portillon/.test(S)){const e=Math.max(+F[1],+F[2]),t=Math.min(+F[1],+F[2]),r=""+Math.round(e/100)+Math.round(t/100),i=chercherRef([`rd ${r} galva`,`rd ${r}`]);return null!==i?{kind:"article",src:s,art:i,q:M}:{kind:"inconnu",src:s,q:M}}if(/pare.{0,3}pluie|lucoma|\bwsg\b|\bwgs\b/.test(S)){const e=Math.max(+F[1],+F[2]),r=Math.min(+F[1],+F[2]),i=ppOrdre(t.ppHoriz||/toiture|horizontale?\b|en toit/.test(S)?"cn":"gpp",ppMat(S,t)),o=[];for(const t of i)o.push(`${t} ${r}/${e}`),o.push(`${t} ${e}/${r}`);let l=chercherRef(o);return null===l&&(l=eqvCouvre(i,e,r)),null!==l?{kind:"article",src:s,art:l,q:M}:{kind:"inconnu",src:s,q:M}}if(/grille/.test(S)){const e=kgMatch(+F[1],+F[2]);return null!==e?{kind:"article",src:s,art:e,q:M}:{kind:"inconnu",src:s,q:M}}if(/clapet/.test(S)){if(/manchette|manchon/.test(S)&&!/clapet|registre|volet|klappe/.test(i))return{kind:"inconnu",src:s,q:M};const e=+F[1],t=+F[2],r=Math.min(2e3,100*Math.ceil(t/100)),o=Math.min(1500,100*Math.ceil(e/100)),l=chercherRef([`cr ${t}/${e}`,`cr ${r}/${o}`,`crj ${r}/${o}`]);return null!==l?{kind:"article",src:s,art:l,q:M}:{kind:"inconnu",src:s,q:M}}if(/couvercle|fond/.test(S)){const e=Math.max(+F[1],+F[2]),t=chercherRef([`fond ${e<=400?"0_400":e<=700?"401_700":e<=1e3?"701_1000":"1001_1400"}`]);return null!==t?{kind:"article",src:s,art:t,q:M,libOv:libPlage(t,F[1]+"/"+F[2])}:{kind:"inconnu",src:s,q:M}}let e=null;for(const[t,r]of TYPEKW)if(r.test(S)){e=t;break}if(e&&(+F[1]<100||+F[2]<100)&&(e=null),e){let r=+F[1],o=+F[2],l="",u=0;const d=i.match(/(?:^|[^a-z])(?:longueur|long|lg|l) ?[.:=]? ?(\d{3,4})\b/);d&&+d[1]!==r&&+d[1]!==o&&(u=+d[1]);let m=0;"gaine"!==e||"m"!==x||u||(m=M);let h="galva";/v4a/.test(S)?h="v4a":/inox|v2a/.test(S)?h="inox":/alu/.test(S)?h="alu":/cuivre| cu /.test(S)&&(h="cuivre");let g=t.cls||"B";/etanche|cl\.? ?c\b/.test(i)&&(g="C"),/desenfum|cl\.? ?d\b/.test(i)&&(g="D");let b=null;if(/isol|climatel|armaflex|laine|dinaphone|dinophone|idikel|reflex|ei ?[36]0/.test(i)){b=0;for(const[e,t]of ISOKW)if(i.includes(e)){b=t;break}}let A=90;if("coude"===e){const e=(i+R).match(/\b(90|60|45|30|15)\s*°/)||R.match(/\b(90|60|45|30|15)\b/);e&&(A=+e[1])}return{kind:"piece",src:s,params:{ty:e,A:r,B:o,L:u,ang:A,mat:h,cls:g,isoIdx:b,q:M,note:l,metres:m}}}}if(O){const e=+O[1];if(/chapeau|biconique|dfh/.test(S)){const t=dfhcPrix(e,S);if(t){if(!t.extrap)return{kind:"article",src:s,art:t.art,q:M};const r=ART.a[t.art][0].replace(/\d+\s*$/,e),i=(ART.a[t.art][1]||"").replace(/ø ?\d+/,"ø"+e);return{kind:"article",src:s,art:t.art,q:M,puOv:t.pu,libOv:`${r} — ${i}`,extrap:`hors catalogue : prix ø${t.d} × ${t.extrap}`}}}if(/soupape/.test(S)){const t=chercherRef(/pulsion|soufflage/.test(S)?[`opt ${e}`,`opf ${e}`]:[`opf ${e}`,`opt ${e}`]);if(null!==t)return{kind:"article",src:s,art:t,q:M}}const r=artPourRond(t.ppMat&&/pare.{0,3}pluie|lucoma|\bwsg\b|\bwgs\b/.test(S)?S+" "+t.ppMat:S,e);if(null!==r)return{kind:"article",src:s,art:r,q:M,libOv:libPlage(r,"ø"+e)};if(BRUITKW.test(i))return;return x||o||/[a-z]{4}/.test(i)?{kind:"inconnu",src:s,q:M}:void 0}if(/soupape/.test(S)){const e=i.match(/\b(80|100|125|150|160|180|200)\b/);if(e){const t=chercherRef(/pulsion|soufflage/.test(S)?[`opt ${e[1]}`,`opf ${e[1]}`]:[`opf ${e[1]}`,`opt ${e[1]}`]);if(null!==t)return{kind:"article",src:s,art:t,q:M}}}if(/^ *(?:\d{1,2}[.)] *)?(types?|typ|marques?|fabrication|fabricant|modele|designation|execution|reference)\b *:/.test(i.trim()))return;buildRefIdx();const P=new Set(i.split(/[^a-z0-9]+/).filter(Boolean));let N=null,k=0;for(const e of P){const t=REFIDX[e];if(t)for(const[e,r]of t)if(r.every(e=>P.has(e))){const t=100*r.length+ART.a[e][0].length;t>k&&(k=t,N=e)}}if(null!==N)return{kind:"article",src:s,art:N,q:M};if(/\bbloc\b/.test(i)&&!BRUITKW.test(i)){return{kind:"inconnu",src:((t.prec&&!/bloc/i.test(t.prec)?t.prec+" — ":"")+s).slice(0,150),q:1}}const w=/ø ?\d{2,4}|\d{2,4} ?[x×\/*] ?\d{2,4}|\b\d{2,4} ?mm\b/.test(i);return(o||/ [mp] \d/.test(i))&&w&&!BRUITKW.test(i)?{kind:"inconnu",src:s,q:M}:void 0}var REP_PAGE=String.fromCharCode(2),REP_POS=/^\s*(\d{1,3}(?:\.\d{1,3}){0,2})\s*[.)]?\s+(?=[A-Za-zÀ-ÿ«"(])/;function repLoc(e){const t=e.page?"page "+e.page:"",r=e.pos?"pos. "+e.pos:"";return t&&r?t+" · "+r:t||r}function majSupportThermo(e){if(!(e>60))return;const t=chercherRef(["support thermometre 100"]);if(null!==t)for(let e=IMP.length-1;e>=0&&e>=IMP.length-3;e--){const r=IMP[e];if(r&&"article"===r.kind&&null!=r.art&&/^support thermometre 30/.test(norm(ART.a[r.art][0]||"")))return r.art=t,void(r.profOk=!0)}}var IMPPLANS=0;function filtrerPlans(e){IMPPLANS=0;const t=e.split(/\r?\n/),r=[];let s={l:[]};for(const e of t)2===e.charCodeAt(0)?(r.push(s),s={l:[e]}):s.l.push(e);if(r.push(s),r.length<6)return e;const i=/Fr\.\s*[.·_]{3,}/,o=r.map(e=>{const t=e.l.join("\n"),r=e.l.filter(e=>e.trim()).length,s=e.l.filter(e=>e.length>250).length;return{plan:!i.test(t)&&s>=3,titreAnnexe:r>0&&r<15&&/annexe/i.test(t)&&/sch[eé]ma\s+de\s+principe|^\s*plans?\s*$|^\s*coupes?\s*$/im.test(t)}});let l=-1;for(let e=Math.floor(.5*r.length);e=0&&e>=l||o[e].plan)r[e].l.length&&IMPPLANS++;else for(const t of r[e].l)u.push(t);return u.join("\n")}function analyser(e){e=filtrerPlans(e);const t={label:"",cls:"B"};IMP=[],IMPREJ=[],REJVOIR=!1,REJTOUT=!1;let r=0,s=0,i=null;IMPPLAST=0,t.prevBlank=!0;for(const o of e.split(/\r?\n/)){if(2===o.charCodeAt(0)){t.page=+o.slice(1)||t.page;continue}if(!o.trim()){t.prevBlank=!0;continue}const e=/profondeur\s*(?:de\s*)?(\d{2,3})\s?mm/i.exec(o);e&&majSupportThermo(+e[1]);const l=REP_POS.exec(o);l&&!/^\d{1,3}(?:[.,]\d)?\s*(mm|cm|m|kg|m2|m3|p|pce|ens)\b/i.test(o.trim())&&(t.pos=l[1].replace(/\.$/,""));const u=o.trim().match(/^(\d{3}\.\d(?:\.\d)?)\s+([A-ZÈÉÀÂÎÔÛÄÖÜ][A-ZÈÉÀÂÎÔÛÄÖÜ '&.\-]{3,60})$/);if(u&&!/TOTAL|REPORT/i.test(u[2])){i=u[1]+" — "+u[2].trim(),t.pos="",t.plast=!1;continue}const d=repLoc(t),ranger=e=>{e&&!e.loc&&d&&(e.loc=d),e&&!e.lab&&t.label&&(e.lab=t.label),IMP.push(e)},m=parseLigne(o,t);if(t.prec=o.trim(),void 0===m){if(t.plast){r++,IMPPLAST++;continue}const e=apChercher(o.trim());if(e){i&&(IMP.push({kind:"chap",titre:i}),i=null),ranger({kind:"article",src:o.trim(),art:e.art,q:rejQte(o),viaAppris:e.via}),t.prevBlank=!1;continue}const s=apRejouer(o.trim());if(s){i&&(IMP.push({kind:"chap",titre:i}),i=null),ranger(s),t.prevBlank=!1;continue}r++,rejPlausible(o)&&IMPREJ.push({src:o.trim(),loc:d});continue}t.prevBlank=!1,m&&("inconnu"===m.kind&&s++,i&&(IMP.push({kind:"chap",titre:i}),i=null),"piece"===m.kind&&m.params.metres?decoupeGaine({...m.params,metres:0},Math.round(1e3*m.params.metres)).forEach(e=>ranger({kind:"piece",src:m.src,params:e})):ranger(m))}renderImport(r,s)}function propositionBrdi(){}let IMPMETA={ign:0,nc:0},IMPFILTRE=!1;function impAppliquerAppris(){IMP.forEach(e=>{if(e._ap||"inconnu"!==e.kind||e.mono||e.slqn)return;e._ap=1;const t=apChercher(e.src);if(t)return e.kind="article",e.art=t.art,void(e.viaAppris=t.via);const r=apRejouer(e.src);r&&Object.assign(e,r)})}function impConf(e){return"chap"===e.kind?"":e.viaIA&&"inconnu"!==e.kind?"o":"inconnu"===e.kind?e.slqn||e.pu>0?"o":"r":"article"===e.kind?e.extrap||e.viaFz||e.cmdSur||"motif"===e.viaAppris?"o":"v":"piece"===e.kind&&e.calc&&e.calc.warn?"o":"v"}function impCand(e){const t=IMP[e];if(!t._cand){const e=t._rech||(t.lab?t.lab+" ":"")+t.src;if(t._cand=fzChercher(e,null,6),!t._rech&&t.lab&&t._cand.length<3){const e=fzChercher(t.src,null,6),r=new Set(t._cand.map(e=>e.art));for(const s of e)r.has(s.art)||t._cand.push(s);t._cand=t._cand.slice(0,6)}}return t._cand}function impRech(e,t){IMP[e]._rech=t,IMP[e]._cand=null,renderImport()}function impOuvrir(e){IMP[e]._ouvert=!IMP[e]._ouvert,renderImport()}function impRetenirCase(e,t){IMP[e]._ret=!!t}function impRetenirFam(e,t){IMP[e]._retFam=!!t}function eqvPrefixe(e){const t=norm(e||"").trim().split(/\s+/);let r=-1;for(let e=0;enorm(e[0])===norm(s));if(i){const e=String(i[1]||"").split(",").map(e=>e.trim()).filter(Boolean);e.indexOf(r)<0&&(e.unshift(r),i[1]=e.join(", "))}else STORE.equiv.push([s,r,"appris à l'écran de révision"]);return eqvInvalider(),storeSave(),storeMaj(),r}function impChoisir(e,t){if(""===t)return;const r=IMP[e];if(r._ret&&apRetenir(r.src,+t,!0),r._retFam&&r.lab){const e=eqvDepuisChoix(r.lab,+t);toast(e?`« ${r.lab} » sera cherché sous « ${e} » — visible et modifiable dans Tarifs › équivalences.`:"Cette référence ne porte pas de cote : impossible d'en déduire une famille.")}r.kind="article",r.art=+t,r.viaFz=!0,r._ouvert=!1,delete r.pu,delete r.puOv,delete r.libOv,delete r.extrap,renderImport()}function impDefaire(e){const t=IMP[e];apOublier(t.src),t.kind="inconnu",t._ouvert=!0,t._cand=null,delete t.art,delete t.viaFz,delete t.viaAppris,delete t.libOv,delete t.extrap,renderImport()}function impFiltre(e){IMPFILTRE=!!e,renderImport()}let IMPREJ=[],REJVOIR=!1,REJTOUT=!1;const REJ_MAX=60,REJ_ADMIN=/^(total|report|page|remarque|selon|norme|nota|voir|idem|dito|suite|reserve|divers|projet|chantier|objet|adresse|rue|av |avenue|chemin|ch\. )/,REJ_BRUIT=/_{4,}|\bt[eé]l\b|\bfax\b|e-?mail|@|www\.|https?:|\bnpa\b|case postale|\bcfc\b|signature|delai de|conditions generales|table des matieres/,REJ_PROSE=/\b(sera|seront|devra|devront|doit|doivent|etre|comprenant|comprend|conformement|repondant|prevoir|prevu[es]?|effectuer?|realise[es]?|respecter?|permettant|assurer?|fourniture et pose de|y compris)\b/,REJ_TITRE=/^\d+(?:[.\-]\d+)*\s*[–—\-:]?\s*[a-zà-ÿ' ]{3,40}$/,REJ_POS=/\d{2,4} ?[x×] ?\d{2,4}|ø ?\d{2,4}|\b\d{1,4} ?(?:pce?s?|pieces?|stk|stck)\b|\b[mp] \d|\d ?m[²2]|\bkg\b|\bml\b/;let REJ_MOTS=null;function rejMotsMetier(){if(REJ_MOTS)return REJ_MOTS;const e=new Set;for(const t of Object.values(lexTable()).flat())t.length>=4&&e.add(t);for(const t of["gaine","conduit","canal","tube","coude","piquage","reduction","registre","clapet","silencieux","grille","bouche","diffuseur","caisson","filtre","batterie","ventilateur","isolation","laine","manchette","collier","bride","trappe","chapeau","fond","couvercle","souche","toiture","flexible","spiro","rectangulaire","galvanise","aluminium","inox"])e.add(t);return REJ_MOTS=e,e}function rejPlausible(e){const t=norm(e);if(/[.…]{4,}\s*\d{1,3}\s*$/.test(t))return!1;const r=t.replace(/[.…]{3,}/g," ").replace(/\s+/g," ").trim();if(r.length<10||!/\d/.test(r))return!1;if(/^[•o\-–]?\s*(zone|niveau|etage|local|secteur)\b/.test(r))return!1;if(REJ_ADMIN.test(r)||REJ_BRUIT.test(r)||REJ_PROSE.test(r)||REJ_TITRE.test(r))return!1;if(/coupe.{0,3}feu|\bfk\d?[ -]?eu\b|\bbsk\b/.test(r)&&/\d{2,4} ?[x×] ?\d{2,4}/.test(r))return!1;const s=/ø ?\d{2,4}|\d{2,4} ?[x×] ?\d{2,4}|\bd ?n ?\d{2,3}\b/.test(r),i=r.split(/[^a-z]+/).filter(e=>e.length>=3);if(!i.length)return s;if(!REJ_POS.test(r)&&i.length>8)return!1;const o=rejMotsMetier(),l=i.some(e=>o.has(e)||o.has(fzStem(e)));return s||l}function rejQte(e){const t=" "+norm(e).replace(/(\d)['’](\d{3})/g,"$1$2")+" ",r=t.match(/ (\d{1,5}(?:[.,]\d{1,2})?) ?(?:ml|mt|mi|m|pces?|pcs?|p|blocs?|bl|stk|st)\.? *$/);if(r)return Math.max(1,Math.round(parseFloat(r[1].replace(",","."))));const s=[...t.matchAll(/[ :](?:ml|mt|mi|m|pces?|pcs?|p|blocs?|bl|stk|st)[.,;!]* +(\d{1,5})(?![\d.,\/x×])/g)];if(s.length)return Math.max(1,+s[s.length-1][1]);let i=t.match(/ (\d{1,4}) ?(?:pce?s?|pieces?|stk|stck)\b/)||t.match(/ (\d{1,4}(?:[.,]\d+)?) ?m['’]?l?(?![a-z])/);return i?Math.max(1,Math.round(parseFloat(String(i[1]).replace(",",".")))):(i=t.match(/^ (\d{1,3}) ?[x×] (?=[a-z])/),i?Math.max(1,+i[1]):1)}function rejCand(e){const t=IMPREJ[e];return t._cand||(t._cand=fzChercher(t._rech||t.src,null,6)),t._cand}function rejVoir(){REJVOIR=!REJVOIR,renderImport()}function rejTout(e){REJTOUT=!!e,renderImport()}function rejRech(e,t){IMPREJ[e]._rech=t,IMPREJ[e]._cand=null,renderImport()}function rejRetenirCase(e,t){IMPREJ[e]._ret=!!t}function rejQteSet(e,t){IMPREJ[e].q=Math.max(1,Math.round(+t||1))}function rejChoisir(e,t){if(""===t)return;const r=IMPREJ[e];r._ret&&apRetenir(r.src,+t,!0),IMP.push({kind:"article",src:r.src,art:+t,q:r.q||rejQte(r.src),viaFz:!0,loc:r.loc}),IMPREJ.splice(e,1),IMPMETA.ign>0&&IMPMETA.ign--,renderImport()}const IA_URL="https://api.anthropic.com/v1/messages",IA_CLE_KEY="air-ventil.cle-ia",IA_MODELES=[["claude-sonnet-5","Sonnet 5 — recommandé (bon lecteur, peu cher)",2,10],["claude-opus-5","Opus 5 — le plus fin sur les plans difficiles",5,25],["claude-haiku-4-5","Haiku 4.5 — le plus rapide et le moins cher",1,5]],IA_USD_CHF=.8;let IA_CLE="",IA_OCCUPE=!1;function iaEtatDef(){return{modele:"claude-sonnet-5",actif:!1,in:0,out:0,appels:0,usd:0}}function iaEtat(){return STORE.ia||(STORE.ia=iaEtatDef()),STORE.ia}async function iaCleCharger(){IA_CLE=await cfrLire(IA_CLE_KEY)||""}function iaMasque(){return IA_CLE?IA_CLE.slice(0,7)+"…"+IA_CLE.slice(-4):""}function iaCleSet(e){if((e=(e||"").trim())&&e.indexOf("…")>=0)return void iaMaj();if(IA_CLE=e,IA_CLE)cfrEcrire(IA_CLE_KEY,IA_CLE);else try{localStorage.removeItem(IA_CLE_KEY)}catch(e){}if(IA_CLE&&!iaEtat().actif){iaEtat().actif=!0;const e=$("iaActif");e&&(e.checked=!0)}const t=$("iaCle");t&&"password"===t.type&&(t.value=iaMasque()),storeSave(),iaMaj(),renderImport0()}function iaModele(e){iaEtat().modele=e,storeSave(),iaMaj()}function iaActif(e){iaEtat().actif=!!e,storeSave(),iaMaj(),renderImport0()}function iaDispo(){return!!IA_CLE&&!!iaEtat().actif}function iaRAZ(){const e=iaEtat();e.in=0,e.out=0,e.appels=0,e.usd=0,storeSave(),iaMaj()}function renderImport0(){IMP&&IMP.length&&!$("impRes").hidden&&renderImport()}async function iaAppel(e,t,r,s){if(!IA_CLE)throw new Error("Aucune clé n'est enregistrée : ouvre « Assistant IA » et colle ta clé.");const i=iaEtat().modele,o={model:i,max_tokens:s||4e3,system:t,messages:[{role:"user",content:e}]};let l;r&&(o.tools=[r],o.tool_choice={type:"tool",name:r.name});try{l=await fetch(IA_URL,{method:"POST",headers:{"content-type":"application/json","x-api-key":IA_CLE,"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"},body:JSON.stringify(o)})}catch(e){throw new Error("Aucune réponse : vérifie la connexion internet. Si ça se reproduit toujours au même endroit, dis-le moi — il faudra passer par un petit relais.")}if(!l.ok){let e="";try{const t=await l.json();e=t.error&&t.error.message||""}catch(e){}if(401===l.status)throw new Error("Clé refusée : recopie-la depuis la console Anthropic (elle commence par sk-ant-).");if(403===l.status)throw new Error("Accès refusé pour cette clé.");if(429===l.status)throw new Error("Trop de demandes d'un coup — attends quelques secondes et relance.");if(400===l.status&&/credit|balance/i.test(e))throw new Error("Crédit épuisé sur le compte API : recharge dans la console Anthropic (Billing).");if(l.status>=500)throw new Error("Le service est occupé pour l'instant — réessaie dans une minute.");throw new Error("Refus de l'IA ("+l.status+") "+e.slice(0,160))}const u=await l.json(),d=u.usage||{},m=iaEtat(),h=IA_MODELES.find(e=>e[0]===i)||IA_MODELES[0];if(m.in+=d.input_tokens||0,m.out+=d.output_tokens||0,m.appels++,m.usd+=(d.input_tokens||0)/1e6*h[2]+(d.output_tokens||0)/1e6*h[3],storeSaveDiff(),iaMaj(),r){const e=(u.content||[]).find(e=>"tool_use"===e.type);if(!e)throw new Error("L'IA n'a rien renvoyé d'exploitable sur ce passage.");return e.input}return(u.content||[]).filter(e=>"text"===e.type).map(e=>e.text).join("\n")}const IA_FAMILLES=["gaine","coude","cone","deviation","reduction","rond-carre","te","piquage","silencieux","clapet","grille","grille-pare-pluie","chapeau","fond","manchette","collier","portillon","tube","soupape","autre"],IA_OUTIL={name:"positions_lues",description:"Renvoie les positions de ventilation lues dans le document. Ne donne jamais de prix, jamais de référence ni de code de catalogue.",input_schema:{type:"object",properties:{positions:{type:"array",description:"une entrée par position chiffrable ; laisse de côté les textes administratifs",items:{type:"object",properties:{n:{type:"integer",description:"le numéro écrit devant la ligne d'origine"},texte:{type:"string",description:"la ligne d'origine recopiée telle quelle, sans correction"},famille:{type:"string",enum:IA_FAMILLES},forme:{type:"string",enum:["quadratique","rond"]},a:{type:"number",description:"1re cote en mm : largeur si quadratique, diamètre si rond"},b:{type:"number",description:"2e cote en mm (hauteur), seulement si quadratique"},a2:{type:"number",description:"largeur de la 2e section (cône, déviation, rond-carré, té réduit)"},b2:{type:"number",description:"hauteur de la 2e section"},d2:{type:"number",description:"diamètre de la 2e section (rond-carré, réduction ronde)"},longueur:{type:"number",description:"longueur de la pièce en mm"},angle:{type:"number",description:"angle en degrés d'un coude ou d'un piquage"},decalage:{type:"number",description:"décalage en mm d'une déviation ou d'un cône excentré ; rien si la pièce est concentrique (AXE)"},matiere:{type:"string",enum:["galva","inox","v4a","alu","cuivre"]},classe:{type:"string",enum:["B","C","D"],description:"B standard, C étanche, D désenfumage"},isolation:{type:"string",description:"nom de l'isolation si la position en porte une (climatel, armaflex, laine minérale 50…)"},quantite:{type:"number"},unite:{type:"string",enum:["piece","metre"]},assemblage:{type:"string",description:"même repère sur deux pièces qui doivent être assemblées ensemble"},confiance:{type:"number",description:"entre 0 et 1"}},required:["n","texte","famille","quantite"]}}},required:["positions"]}};function iaSysteme(){return"Tu assistes Air-Ventil Sàrl, atelier suisse de ferblanterie de ventilation (Romanel-sur-Lausanne).\nTon seul travail est de LIRE des demandes d'offre et d'en extraire les positions à chiffrer, sous forme structurée.\nTu ne donnes JAMAIS de prix, JAMAIS de référence ni de code de catalogue : le calcul et le catalogue appartiennent\nà l'application. Quand tu n'es pas sûr d'un champ, tu le laisses vide plutôt que de l'inventer, et tu baisses la confiance.\n\nCe qu'Air-Ventil fabrique et chiffre :\n- pièces quadratiques calculées à l'atelier : gaines droites, coudes, cônes, déviations, réductions, tés, rond-carré ;\n- articles ronds du catalogue : tubes spiro, coudes, piquages, silencieux, clapets de réglage, colliers, brides, chapeaux ;\n- accessoires : grilles, grilles pare-pluie, portillons de visite, soupapes, manchettes, fonds.\n\nVocabulaire de la maison :\n- un « couvercle » est un fond ; un « canal » ou « Kanal » est une gaine ; « Wickelfalzrohr » ou « spiro » est un tube rond ;\n- « Bogen » = coude, « Übergang » / « Reduktion » = cône, « Etagenversatz » = déviation, « Schalldämpfer » = silencieux ;\n- un régulateur de débit (Trox VFC, TVR) se chiffre chez nous comme un clapet de réglage.\n\nRègles de lecture :\n- les cotes sont en millimètres ; « 400/200 » et « 400x200 » écrivent la même section ; la 1re cote est la largeur, la 2e la hauteur ;\n- pour une pièce à deux sections (cône, déviation, rond-carré), donne bien les deux : a/b puis a2/b2 (ou d2 si la 2e est ronde) ;\n- « classe B » = standard, « classe C » = étanche, « classe D » = désenfumage ; sans mention, c'est B ;\n- un coude n'est pas toujours à 90° : relève l'angle écrit ;\n- dans une colonne de métré, « m 45 » ou « 45 m » = 45 mètres (unite = metre) ; « p 12 » ou « 12 pce » = 12 pièces ;\n- un clapet coupe-feu RECTANGULAIRE n'est pas vendu par Air-Ventil : ne le renvoie pas du tout ;\n- ne renvoie pas les lignes administratives : échafaudage, mise en service, équilibrage, documentation, nettoyage, réserve, transport, main-d'œuvre ;\n- recopie dans « texte » la ligne d'origine telle qu'elle est écrite : elle sert de trace et de contrôle."}const IA_MOT={gaine:"Gaine",coude:"Coude",cone:"Cône",deviation:"Déviation",reduction:"Réduction","rond-carre":"Rond-carré",te:"Té",piquage:"Piquage",silencieux:"Silencieux",clapet:"Clapet de réglage",grille:"Grille","grille-pare-pluie":"Grille pare-pluie",chapeau:"Chapeau bicônique",fond:"Fond",manchette:"Manchette",collier:"Collier",portillon:"Portillon de visite",tube:"Tube spiro",soupape:"Soupape",autre:""};function iaLigne(e){const n=e=>{const t=Math.round(+e);return isFinite(t)&&t>0?t:0},t=Math.max(1,Math.round(+e.quantite||1));let r=IA_MOT[e.famille];r||(r=String(e.texte||"").replace(/[0-9ø].*$/,"").replace(/[^A-Za-zÀ-ÿ' -]/g," ").replace(/\s+/g," ").trim().slice(0,40)||"Pièce"),"coude"===e.famille&&(r+=" "+(n(e.angle)||90)+"°"),"piquage"===e.famille&&(r+=" "+(n(e.angle)||45)+"°");const s=[r];if("rond"===e.forme||"quadratique"!==e.forme&&n(e.a)&&!n(e.b)){let t=n(e.a);n(e.d2)>t&&(t=n(e.d2)),t&&s.push("ø"+t)}else if(n(e.a)&&n(e.b)){let t=n(e.a),r=n(e.b);n(e.a2)&&n(e.b2)&&n(e.a2)*n(e.b2)>t*r&&(t=n(e.a2),r=n(e.b2)),s.push(t+"x"+r)}return n(e.longueur)&&s.push("L="+n(e.longueur)),s.push(e.matiere&&"galva"!==e.matiere?e.matiere:"galva"),"C"===e.classe?s.push("cl. C"):"D"===e.classe&&s.push("cl. D"),e.isolation&&s.push("isolation "+String(e.isolation).slice(0,30)),s.filter(Boolean).join(" ")+("metre"===e.unite?" m ":" p ")+t}function iaNoteDessin(e){const n=e=>{const t=Math.round(+e);return isFinite(t)&&t>0?t:0},t=n(e.a)&&n(e.b)?n(e.a)+"x"+n(e.b):n(e.a)?"ø"+n(e.a):"",r=n(e.a2)&&n(e.b2)?n(e.a2)+"x"+n(e.b2):n(e.d2)?"ø"+n(e.d2):"",s=[];return t&&r?s.push(t+" → "+r):t&&s.push(t),n(e.longueur)&&s.push("L."+n(e.longueur)),n(e.decalage)&&s.push("décalage "+n(e.decalage)),n(e.angle)&&s.push(n(e.angle)+"°"),s.length?" (dessin : "+s.join(", ")+")":""}const IA_BESOIN_L={gaine:1,cone:1,deviation:1,reduction:1,"rond-carre":1,silencieux:1,tube:1};function iaVersLigne(e){const t=iaLigne(e);let r=null;try{r=parseLigne(t,{label:"",cls:"B"})}catch(e){r=null}return r&&"chap"!==r.kind?(r.viaIA=!0,r.iaCanon=t,"piece"===r.kind&&r.params&&+e.decalage>0&&(r.params.dec=Math.round(+e.decalage)),e.assemblage&&(r.asmg="ia:"+String(e.assemblage).slice(0,20)),r):null}async function iaLireTextes(e,t){const r="Voici des lignes d'une demande d'offre que notre application n'a pas su lire"+(t?" (document : "+t+")":"")+". Rends-moi les positions chiffrables, avec le numéro de chaque ligne. Laisse tomber tout ce qui n'est pas du matériel de ventilation.\n\n"+e.map((e,t)=>t+1+". "+String(e).slice(0,200)).join("\n"),s=await iaAppel([{type:"text",text:r}],iaSysteme(),IA_OUTIL,4e3);return s&&Array.isArray(s.positions)?s.positions:[]}function iaStatut(e){const t=$("iaStatut");t&&(t.hidden=!e,t.innerHTML=e||"")}async function iaLireNonReconnues(){if(IA_OCCUPE)return;if(!iaDispo())return void toast("Active l'assistant IA et colle ta clé d'abord.");const e=[];if(IMP.forEach((t,r)=>{"r"===impConf(t)&&t.src&&e.push({src:t.src,ou:"imp",i:r})}),IMPREJ.forEach((t,r)=>{t.src&&e.push({src:t.src,ou:"rej",i:r})}),!e.length)return void toast("Aucune ligne à faire relire — tout est déjà reconnu.");IA_OCCUPE=!0;let t=0,r=0;const s=$("docChantier")&&$("docChantier").value||"";try{for(let i=0;ie.src),s)).forEach(e=>{const r=o[(0|e.n)-1];if(!r)return;0;const s=iaVersLigne(e);if(s&&("inconnu"!==s.kind||s.pu>0))if(t++,"imp"===r.ou){const e=IMP[r.i];IMP[r.i]={...s,src:e.src,loc:e.loc,q:void 0!==s.q?s.q:e.q}}else IMPREJ[r.i]._pris=!0,IMP.push({...s,src:IMPREJ[r.i].src,loc:IMPREJ[r.i].loc}),IMPMETA.ign>0&&IMPMETA.ign--})}}catch(e){return IA_OCCUPE=!1,iaStatut(""),IMPREJ=IMPREJ.filter(e=>!e._pris),renderImport(),void toast(e.message)}IMPREJ=IMPREJ.filter(e=>!e._pris),IA_OCCUPE=!1,iaStatut(""),renderImport(),toast(t+" ligne(s) chiffrée(s) par l'IA sur "+e.length+" — elles sont en orange, contrôle-les puis ajoute-les : l'app les retiendra.")}function iaMaj(){const e=$("iaEtat");if(!e)return;const t=iaEtat(),r=$("iaModele");r&&r.value!==t.modele&&(r.value=t.modele);const s=$("iaActif");s&&(s.checked=!!t.actif);const i=.8*t.usd;IA_CLE?t.actif?e.innerHTML="Assistant allumé — "+t.appels+" lecture(s) depuis la remise à zéro, environ "+fmt(i,2)+" CHF consommés ("+(t.in/1e3).toFixed(0)+"k jetons lus, "+(t.out/1e3).toFixed(1)+"k écrits).":e.innerHTML="Clé enregistrée mais assistant éteint : aucun appel n'est fait.":e.innerHTML="Aucune clé enregistrée — l'application travaille entièrement hors ligne, comme avant. Rien n'est envoyé nulle part."}function iaVoirCle(e){const t=$("iaCle");t&&(t.type=e?"text":"password",t.value=e?IA_CLE:iaMasque())}async function iaTester(){if(IA_CLE){iaStatut("Essai en cours…");try{const e=await iaAppel([{type:"text",text:"1. Coude 90° galva 400x200\n2. Echafaudage roulant\n3. Gaine 500/300 L=1500 classe C p 4"}],iaSysteme(),IA_OUTIL,1200),t=e&&e.positions?e.positions.length:0;iaStatut(""),toast("La connexion fonctionne — l'IA a lu "+t+" position(s) sur les 3 lignes d'essai (la 2e ne doit pas être retenue).")}catch(e){iaStatut(""),toast(e.message)}}else toast("Colle d'abord ta clé.")}async function iaInit(){await iaCleCharger();const e=$("iaModele");if(e){e.options.length||(e.innerHTML=IA_MODELES.map(function(e){return'"}).join(""));const t=iaEtat();e.value=t.modele,e.value!==t.modele&&(t.modele=IA_MODELES[0][0],e.value=t.modele,storeSave())}const t=$("iaCle");t&&IA_CLE&&(t.value=iaMasque());const r=$("iaActif");r&&(r.checked=!!iaEtat().actif),iaMaj()}const IA_VIS_MAX=1568;let IA_DERNIER_FICHIER=null;async function iaImagesPdf(e,t){const r=await chargerPdfJs(),s=await e.arrayBuffer(),i=await r.getDocument({data:s}).promise,o=Math.min(i.numPages,t||8),l=[];for(let e=1;e<=o;e++){iaStatut("Préparation de la page "+e+" sur "+o+"…");const t=await i.getPage(e),r=t.getViewport({scale:1}),s=1568/Math.max(r.width,r.height),u=t.getViewport({scale:s}),d=document.createElement("canvas");d.width=Math.round(u.width),d.height=Math.round(u.height);const m=d.getContext("2d");m.fillStyle="#fff",m.fillRect(0,0,d.width,d.height),await t.render({canvasContext:m,viewport:u}).promise,l.push(d.toDataURL("image/png")),d.width=d.height=1,await new Promise(e=>setTimeout(e,0))}return l}function iaImageBloc(e){const t=/^data:([^;]+);base64,(.*)$/.exec(e||"")||[];return{type:"image",source:{type:"base64",media_type:t[1]||"image/png",data:t[2]||""}}}async function iaImagesFichier(e,t){if(/\.pdf$/i.test(e.name))return await iaImagesPdf(e,t);const r=await new Promise((t,r)=>{const s=new FileReader;s.onload=()=>t(s.result),s.onerror=()=>r(new Error("Image illisible.")),s.readAsDataURL(e)}),s=await new Promise((e,t)=>{const s=new Image;s.onload=()=>e(s),s.onerror=()=>t(new Error("Image illisible.")),s.src=r}),i=Math.min(1,1568/Math.max(s.width,s.height));if(i>=1)return[r];const o=document.createElement("canvas");o.width=Math.round(s.width*i),o.height=Math.round(s.height*i);const l=o.getContext("2d");return l.imageSmoothingEnabled=!0,l.imageSmoothingQuality="high",l.fillStyle="#fff",l.fillRect(0,0,o.width,o.height),l.drawImage(s,0,0,o.width,o.height),[o.toDataURL("image/jpeg",.92)]}function iaSystemeVision(){return iaSysteme()+"\n\n=== TU REGARDES UN PLAN OU UN CROQUIS ===\nLes images sont soit une demande d'offre dessinée (une case par position, avec un numéro de position, les cotes écrites sur le dessin, parfois la surface développée en m² et une quantité), soit un plan coté d'assemblage sorti d'AutoCAD.\n- Lis les cotes telles qu'elles sont écrites, en millimètres. La première cote est la largeur, la seconde la hauteur. « 1029/938 » veut dire 1029 x 938.\n- Une longueur est souvent notée L.700, L=700 ou simplement portée le long de la pièce.\n- Une pièce qui change de section (cône, déviation, rond-carré) a deux sections cotées : donne-moi les deux (a/b pour la grande, a2/b2 ou d2 pour la petite).\n- Sur un plan d'assemblage, les pièces dessinées bout à bout font partie du même ensemble : donne-leur la même étiquette dans « assemblage » (A1, A2…). Une pièce seule n'a pas d'étiquette.\n- L'angle d'un coude est écrit sur le dessin ou se lit sur le croquis ; s'il n'est pas lisible, mets 90.\n- La quantité est le nombre noté à côté de la position (3x, Anz. 3, 3 Stk, 3 pce). Sans indication, 1.\n- N'invente jamais une cote que tu ne lis pas : laisse-la vide et baisse « confiance ».\n- Laisse tomber le cartouche, la légende, le nord, l'échelle, les noms de locaux et les cotes de bâtiment : je ne veux que les pièces de ventilation à fabriquer.\n\n=== CROQUIS À LA MAIN ===\nBeaucoup de demandes arrivent dessinées à la main, une position par bloc, numérotées AR1, AR2, P1, Pos. 1, séparées par un trait.\n- Deux nombres écrits l'un au-dessus de l'autre (350 sur 350) forment une section : 350 x 350.\n- Une pièce qui change de section porte deux sections cotées, une en haut et une en bas du croquis : donne-moi les deux (a/b pour l'une, a2/b2 pour l'autre).\n- LA LONGUEUR EST LA COTE ISOLÉE : un nombre écrit seul à côté de la pièce, entre deux flèches ou entre deux petits traits (par exemple un « 400 » seul à droite du croquis). C'est la longueur de la pièce et il me la faut absolument. Sans elle, l'application chiffre sur sa longueur standard et l'offre affiche une longueur fausse : c'est l'erreur la plus grave que tu puisses faire ici. Cherche-la avant de conclure.\n- Ne confonds pas cette cote isolée avec un rayon (R150), un décalage (souvent noté x100 ou porté par une flèche oblique — mets-le dans « decalage ») ou un repère de bride.\n- Abréviations de l'atelier : GD = gaine droite, B90° ou Bg 90° = coude 90°, R150 = rayon 150 (ce n'est jamais une longueur), AXE = concentrique (pas de décalage), un chiffre entouré comme 1x ou 2x = la quantité, M30 / MEZ = un type de bride, à ignorer.\n- Si une cote est illisible, laisse le champ vide et baisse « confiance » : je préfère un trou signalé à un chiffre inventé."}async function iaLirePlan(e,t){if(IA_OCCUPE)return;if(!iaDispo())return void toast("Allume l'assistant IA et colle ta clé d'abord.");if(!e)return void toast("Aucun document à faire relire — refais l'import.");IA_OCCUPE=!0;const r=[];try{const t=await iaImagesFichier(e,8);for(let e=0;e1?" et "+(e+2):"")+" sur "+t.length+"…");const i=s.map(iaImageBloc);i.push({type:"text",text:"Voici "+(s.length>1?"deux pages":"une page")+" d'une demande d'offre dessinée. Rends-moi les positions chiffrables avec leurs cotes en millimètres. Numérote-les à partir de "+(r.length+1)+"."});const o=await iaAppel(i,iaSystemeVision(),IA_OUTIL,8e3);o&&Array.isArray(o.positions)&&r.push(...o.positions)}}catch(e){return IA_OCCUPE=!1,iaStatut(""),void toast(e.message)}if(IA_OCCUPE=!1,iaStatut(""),!r.length)return void toast("L'IA n'a rien reconnu de chiffrable sur ce document.");!1!==t&&(IMP=[],IMPREJ=[],IMPMETA={ign:0,nc:0}),IMP.push({kind:"chap",titre:"PLAN LU PAR L'IA — "+String(e.name||"").replace(/\.[a-z0-9]+$/i,"")});let s=0;r.forEach(e=>{const t=iaVersLigne(e);t&&(t.viaIA="plan",t.src=(t.src||t.iaCanon||"")+iaNoteDessin(e),!IA_BESOIN_L[e.famille]||+e.longueur>0||(t.src+=" ⚠ longueur non lue sur le dessin — vérifie-la, l'app a pris sa longueur standard",t.iaSansL=!0),IMP.push(t),s++)});const i={};r.forEach(e=>{e.assemblage&&(i[e.assemblage]=1)}),renderImport(0,IMP.filter(e=>"inconnu"===e.kind).length);const o=IMP.filter(e=>e.iaSansL).length;toast(s+" position(s) lue(s) sur le plan"+(o?" · ⚠ "+o+" sans longueur lue : corrige la cote avant d'ajouter":"")+(Object.keys(i).length?" · "+Object.keys(i).length+" assemblage(s) repéré(s)":"")+" — tout est en orange : contrôle les cotes avant d'ajouter.")}function iaPlanFichier(e){const t=e.files&&e.files[0];e.value="",t&&(IA_DERNIER_FICHIER=t,iaLirePlan(t,!0))}var IMPPLAST=0;function renderImport(e,t){const r=$("impRes");try{"function"!=typeof erpPage||void 0!==IMPMODAL&&IMPMODAL||"import"===ERP_PAGE||erpPage("import")}catch(e){}if(void 0!==e&&(IMPMETA={ign:e||0,nc:t||0}),e=IMPMETA.ign,t=IMPMETA.nc,!IMP.length)return r.hidden=!1,void(r.innerHTML='
\n Références proposées pour « ${t.src} » :\n \n \n ${t.lab?``:""}\n Pas la bonne ? cherche autrement :\n \n ${s.length?"":'rien trouvé — saisis un prix unitaire à la place'}\n
`},s=IMP.map((e,t)=>{if("chap"===e.kind)return`
${e.titre}
`;let r,s,i="–",o="–",l=!0;if("piece"===e.kind){const t=calcPiece({...e.params});t.ok?(e.calc=t,r='ASCV',s=t.desc,i=fmt(t.pu),o=fmt(t.pu*e.params.q)):e.kind="inconnu"}if("kg"===e.kind){const t=calcKg({...e.params,kg:e.q});t.ok?(e.calc=t,r='ASCV · au kg',s=t.desc+" — "+fmt(e.q)+" kg",i=fmt(t.pu),o=fmt(t.tot)):e.kind="inconnu"}const u=impConf(e);if(IMPFILTRE&&"v"===u)return"";if("article"===e.kind){const i=ART.a[e.art];r='Catalogue',s=e.libOv||i[0]+(i[1]&&i[1]!==i[0]?" — "+i[1]:""),e.extrap&&(r='Extrapolé');const o=void 0!==e.puOv?e.puOv:i[2],l="exact"===e.viaAppris?"appris de tes corrections":"motif"===e.viaAppris?"appris sur un cas voisin — à contrôler":e.cmdSur?"matière sous-entendue : complétée d'après le catalogue — à contrôler":e.viaFz?"choisi par toi dans les propositions":"";return`
`+propRow(t,e,u)}if("inconnu"===e.kind){r=e.slqn?'Catalogue SLQN':'À chiffrer',l=!1;const i=norm(e.src).replace(/(\d)['\u2019](\d{3})/g,"$1$2"),o=/ne pas oublier|taxe|echafaudage|equilibrage|dossier|instruction|mesure des debits|nettoyage|mise en service|documentation|plan complet|location|essai|reception|garantie|divers|reserve|forfait pour|deplacement/;e.prod=!!e.mono||!o.test(i)&&/\d{2,4}\s?[x×\/]\s?\d{2,4}|ø ?\d{2,4}/.test(i)&&/(chapeau|biconique|grille|amortisseur|silencieux|monobloc|clapet|caisson|batterie|ventilateur|filtre|volet|registre|porte|trappe|couvercle|fond|piquage|manchette|cadre|plenum|bouche|diffuseur|soupape|toiture|sortie|prise d air|rejet)/.test(i),s=(e.mono?"":"Article manuel : ")+"« "+e.src+" »";const d=e.pu||0;return`
0?"checked":""}>
\n
${r}
\n
${s}${locBadge(e)}${e.slqn?`SLQN ${e.slqn.h}/${e.slqn.c} · L.${e.slqn.det.join(" + L.")} · ${fmt(e.slqn.unit)} × ${Math.round(d/e.slqn.unit)} coulisses`:d>0?"prix saisi":e.prod?"PU à saisir — la ligne part dans l'offre à 0.00 (en rouge) tant qu'il manque":"texte de soumission — décoche si ce n'est pas un article"}
${l} amortisseur(s) de bruit rectangulaire(s) chiffré(s) automatiquement via le catalogue SLQN (plage = largeur de la gaine, épaisseur de coulisse, longueur arrondie à la longueur catalogue supérieure, tronçons au-delà du plus grand format) — prix d'UNE coulisse × nombre de coulisses.
`:"",d=IMP.filter(e=>e.slqc).length,m=(IMPPLANS?`
${IMPPLANS} page(s) de plans ou d'annexes écartées (schémas de principe, coupes, plans cotés) : les débits et les cotes qu'on y lit ne sont pas des quantités à chiffrer.
`:"")+u+(d?`
${d} ligne(s) de coulisses seules chiffrée(s) via le catalogue SLQC (« N coulisses 100 pour gaines 350/150 ») : la soumission demande les coulisses, pas l'amortisseur entier — quantité = nombre de coulisses × nombre de pièces, comme dans l'ERP.
`:"")+(IMPPLAST?`
${IMPPLAST} ligne(s) en matière synthétique écartées (PPs, polypropylène…) — nous ne vendons pas de gainage plastique : ces lignes ne sont ni chiffrées, ni proposées à la relecture.
`:"")+`
${o} article(s) « À chiffrer » pré-cochés (chapeaux ronds, grilles sur mesure…) : ils partent dans l'offre à 0.00 et apparaissent en rouge tant que tu ne leur donnes pas de prix — c'est ce qui manquait par rapport à l'offre ERP. · ${e||0} lignes de texte ignorées · ${t||0} lignes « À chiffrer » (monoblocs, grilles spéciales, couvercles rectangulaires…) : saisis-leur un prix unitaire pour les intégrer, sinon elles ne seront pas importées.
\n Révision de l'import —\n ${g} sûre${g>1?"s":""}\n ${b} à contrôler\n ${A} non reconnue${A>1?"s":""}\n \n ${iaDispo()?``:""}\n ${iaDispo()&&IA_DERNIER_FICHIER?'':""}\n Une famille entière mal lue (« manchette flexible », « SLU », « gaine spiro ») se corrige une seule fois : choisis la bonne référence, coche retenir toute la famille, et l'équivalence part dans Tarifs › équivalences de références, où elle reste visible et modifiable.\n Vert : l'app est sûre d'elle. Orange : elle a dû interpréter (cote recalée, motif appris, référence approchée) — vérifie d'un coup d'œil. Rouge : elle n'a pas reconnu et te propose ses meilleures références ; coche « retenir ce choix » et elle s'en souviendra la prochaine fois.\n
\n ${IMPREJ.length} ligne(s) écartée(s) — donne-leur une référence et elles rejoignent l'offre.\n masquer\n \n ${t>0?`Seules les ${REJ_MAX} premières sont analysées ; ${t} autres attendent (traite celles-ci puis rouvre).`:""}\n
Ligne du document
Qté
Référence à lui donner
\n ${r||'
Aucun rapprochement convaincant — coche « rapprochements douteux » pour tout voir.
`,impPied()}function impPied(){const e=$("impBtnPied");if(!e)return;const t=IMP.filter(e=>"inconnu"!==e.kind).length;e.hidden=!(IMP.length&&!$("impRes").hidden),e.textContent="+ Ajouter à l'offre ("+t+")"}function impQte(e,t){if("kg"===IMP[e].kind)return IMP[e].q=Math.max(0,Math.round(100*(+t||0))/100),void renderImport();const r=Math.max(1,Math.round(+t||1));"piece"===IMP[e].kind?IMP[e].params.q=r:IMP[e].q=r,renderImport()}function impPU(e,t){IMP[e].pu=Math.max(0,parseFloat(t)||0),renderImport()}function impPUArt(e,t){IMP[e].puOv=Math.max(0,parseFloat(t)||0),renderImport()}function impAjouter(){const e=[...document.querySelectorAll(".impchk:checked")].map(e=>+e.dataset.i);if(!e.length)return void toast("Rien de sélectionné.");let t=0,r=0;const s={};e.forEach(e=>{const i=IMP[e];if(!0===i.viaIA&&i.src&&("article"===i.kind?apRetenir(i.src,i.art,!0):i.iaCanon&&apRetenirLigne(i.src,i.iaCanon),r++),"piece"===i.kind){const e=calcPiece({...i.params});e.ok&&(i.asmg&&(s[i.asmg]=s[i.asmg]||[]).push(positions.length),positions.push(e),t++)}else if("kg"===i.kind){const e=calcKg({...i.params,kg:i.q});e.ok&&(positions.push(e),t++)}else if("inconnu"===i.kind){const e=r2(i.pu||0);positions.push({ok:!0,manuel:!0,achiffrer:!(e>0),desc:i.src,poids:0,isoM2:0,pu:e,q:i.q,tot:e*i.q,warn:"",grp:"Standard"}),t++}else if("article"===i.kind){const e=ART.a[i.art],r=void 0!==i.puOv?i.puOv:e[2];positions.push({ok:!0,manuel:!0,desc:i.libOv||e[0]+(e[1]&&e[1]!==e[0]?" — "+e[1]:""),poids:0,isoM2:0,pu:r,q:i.q,tot:r*i.q,warn:"",grp:ART.g[e[3]]||"Standard"}),t++}});let i=0;Object.keys(s).map(e=>s[e]).filter(e=>e.length>1).sort((e,t)=>Math.max(...t)-Math.max(...e)).forEach(e=>{faireAssemblage(e.slice(0,10)),i++}),$("impRes").hidden=!0,IMP=[],impPied(),IMPMODAL&&impFermer(),erpPage("offres"),offOnglet("lig"),render(),toast(t+" position(s) ajoutée(s) depuis l'import"+(i?` · ${i} assemblage(s) créé(s)`:"")+(r?` · ${r} lecture(s) IA retenue(s) — la prochaine fois ce sera gratuit`:""))}function importTexte(){const e=$("impTxt").value;e.trim()?analyser(e):toast("Colle d'abord une liste.")}function impStatus(e){const t=$("impStatus");t.hidden=!e,t.textContent=e||""}let PDFJS=null;function chargerPdfJs(){if(PDFJS)return Promise.resolve(PDFJS);if(PDFJS=window.pdfjsLib,!PDFJS)return Promise.reject(new Error("lecteur PDF non disponible dans cette version."));const e=document.getElementById("pdfWorkerSrc").textContent;return PDFJS.GlobalWorkerOptions.workerSrc=URL.createObjectURL(new Blob([e],{type:"text/javascript"})),Promise.resolve(PDFJS)}async function extrairePdf(e){const t=await chargerPdfJs(),r=await e.arrayBuffer(),s=await t.getDocument({data:r}).promise;let i=[];const o=[];for(let e=1;e<=s.numPages;e++){impStatus(`Lecture du PDF… page ${e}/${s.numPages}`);const r=await s.getPage(e);i.push(REP_PAGE+e);const l=await r.getTextContent(),u=r.getViewport({scale:1}).height;o.push({mots:l.items.map(e=>{const t=(e.str||"").trim();if(!t)return null;const r=e.transform,s=Math.abs(r[1])>Math.abs(r[0]),i=r[4],o=u-r[5],l=e.width||0,d=e.height||10;return s?{t:t,x0:i-d/2,x1:i+d/2,y0:o-l,y1:o,rot:!0}:{t:t,x0:i,x1:i+l,y0:o-d,y1:o,rot:!1}}).filter(Boolean)});const d=r.getViewport({scale:1}),m=l.items.map(e=>{const r=e.transform,s=e.width||0,i=Math.hypot(r[0],r[1])||1,o=t.Util.applyTransform([r[4],r[5]],d.transform),l=t.Util.applyTransform([r[4]+s*r[0]/i,r[5]+s*r[1]/i],d.transform);return{str:e.str,x:o[0],xf:Math.max(o[0],l[0]),y:o[1]}}),h={};m.forEach(e=>{const t=3*Math.round((e.y-d.height)/3);(h[t]=h[t]||[]).push(e)}),Object.keys(h).map(Number).sort((e,t)=>e-t).forEach(e=>{const t=h[e].sort((e,t)=>e.x-t.x);let r="",s=null;for(const e of t){const t=e.x;null!==s&&t-s>1.2&&(r+=t-s>6?"":" "),r+=e.str,s=e.xf}r=puaRepare(r).replace(/[ \t]+/g," ").replace(/ ?\u0001 ?/g," ").trim(),r&&i.push(r)})}return{texte:i.join("\n"),pages:o}}async function importFichier(){const e=$("impFile").files[0];if(e){IA_DERNIER_FICHIER=e;try{if(/\.(png|jpe?g|webp)$/i.test(e.name))return $("impFile").value="",iaDispo()?void await iaLirePlan(e,!0):void impStatus("C'est une image : sans l'assistant IA, l'application ne sait pas la lire. Allume-le dans « Assistant IA », ou colle la liste à la main.");let t;if(/\.pdf$/i.test(e.name)){impStatus("Chargement du lecteur PDF…");const r=await extrairePdf(e);if(t=r.texte,t.replace(/\s/g,"").length<1500&&window.PAOCR&&window.PAOCR.asmDocument){const t=window.PAOCR.asmDocument(r.pages);if(t)return impStatus(""),asmVersImp(t,e.name),void($("impFile").value="")}}else t=await e.text();if(impStatus(""),t.replace(/\s/g,"").length<200)return window.PAOCR?(await importProAir(e),void($("impFile").value="")):void impStatus("Aucun texte lisible dans ce fichier (PDF scanné ? colle la liste manuellement).");analyser(t)}catch(e){impStatus("Erreur : "+e.message)}$("impFile").value=""}}async function ocrPages(e){const t=await chargerPdfJs(),r=await e.arrayBuffer(),s=await t.getDocument({data:r}).promise;impStatus("Préparation de la lecture optique (première fois : ~10 s)…");const i=await window.PAOCR.tessWorker(),o=[];for(let e=1;e<=s.numPages;e++){impStatus(`Lecture optique… page ${e}/${s.numPages}`);const t=await s.getPage(e),r=2481/t.getViewport({scale:1}).width,l=document.createElement("canvas"),u=l.getContext("2d",{willReadFrequently:!0});let d=!1;for(const e of[3,2,1])try{const s=t.getViewport({scale:e*r}),i=document.createElement("canvas");i.width=Math.round(s.width),i.height=Math.round(s.height);const o=i.getContext("2d");if(!o)throw new Error("canvas");o.fillStyle="#fff",o.fillRect(0,0,i.width,i.height),await t.render({canvasContext:o,viewport:s}).promise,l.width=Math.round(i.width/e),l.height=Math.round(i.height/e),u.imageSmoothingEnabled=!0,u.imageSmoothingQuality="high",u.fillStyle="#fff",u.fillRect(0,0,l.width,l.height),u.drawImage(i,0,0,l.width,l.height),i.width=i.height=1;const m=u.getImageData(0,0,l.width,l.height).data;let h=0;for(let e=0;e1)continue;d=!0;break}catch(e){}if(!d)throw new Error("Impossible de rendre la page "+e+" pour la lecture optique.");const m=await window.PAOCR.PA.pageWords(i,l),h=await window.PAOCR.PA.digitClusters(i,u,1050,l.width,0,l.height);o.push({name:"p"+e,words:m,dims:h,W:l.width,H:l.height}),await new Promise(e=>setTimeout(e,0))}return o}async function importProAir(e){const t=await ocrPages(e),r=window.PAOCR.paCover(t[0].words)||{},s=window.PAOCR.paDocument(t);if(impStatus(""),!s.length)return iaDispo()?(impStatus(""),void await iaLirePlan(e,!0)):void impStatus("Aucune position reconnue dans cette demande d'offre.");proairVersImp(s,r,e.name),renderImport(0,IMP.filter(e=>"inconnu"===e.kind).length),r.societe&&!$("docClient").value&&($("docClient").value=r.societe,$("docAdr").value=[r.societe,r.rue,r.ville].filter(Boolean).join("\n")),r.objet&&!$("docChantier").value&&($("docChantier").value=r.objet),r.contact&&!$("docVotreRef").value&&($("docVotreRef").value=r.contact)}function proairVersImp(e,t,r){IMP=[];const s=[r.replace(/\.pdf$/i,""),t.objet].filter(Boolean).join(" — ");IMP.push({kind:"chap",titre:"DEMANDE D'OFFRE ProAir — "+s});let i=0;for(const t of e){i+=(t.zone||0)*(t.qty||1);const e=t.pos?t.pos+" ":"",r=t.warn&&t.warn.length?" ⚠ "+t.warn.join(" ; "):"",s={mat:t.mat||"galva",cls:t.cls||"C",isoIdx:null,ang:t.ang||90};if("gaine"===t.ty&&t.A&&t.B&&t.L){const i=`${e}Gaine ${t.A}×${t.B} L.${t.L} — ${t.zone||"?"} m²${r}`;decoupeGaine({...s,ty:"gaine",A:t.A,B:t.B,L:t.L,q:1},t.L).forEach(e=>IMP.push({kind:"piece",src:i,params:{...e,q:e.q*(t.qty||1)}}));continue}if("cone"===t.ty&&t.A&&t.B)IMP.push({kind:"piece",src:`${e}Cône / étage ${t.A}×${t.B} L.${t.L||"std"}${t.dec?" décalage "+Math.round(t.dec):""} — ${t.zone||"?"} m²${r}`,params:{...s,ty:"cone",A:t.A,B:t.B,L:t.L||0,dec:t.dec||0,q:t.qty||1}});else if("coude"===t.ty&&t.A&&t.B)IMP.push({kind:"piece",src:`${e}Coude ${t.ang||90}° ${t.A}×${t.B} — ${t.zone||"?"} m²${r}`,params:{...s,ty:"coude",A:t.A,B:t.B,L:0,q:t.qty||1}});else if("rondcarre"===t.ty&&t.A&&t.B)IMP.push({kind:"piece",src:`${e}Rond-carré ${t.A}×${t.B}${t.diam?" / ø"+t.diam:""} L.${t.L||"std"} — ${t.zone||"?"} m² ⚠ cotes du dessin à contrôler`,params:{...s,ty:"rondcarre",A:t.A,B:t.B,L:t.L||0,q:t.qty||1}});else{if("silencieux"===t.ty&&t.A&&t.B&&t.L){const s=t.nCoul||0,i=t.epCoul||100,o=s?slqnPrix(s*i>t.A&&s*i<=t.B?t.B:t.A,i,t.L,s):null,l=`${e}Amortisseur de bruit ${t.A}/${t.B} L.${t.L} avec ${s||"?"} coulisse de ${o?o.c:i}${r}`;IMP.push(o?{kind:"inconnu",q:t.qty||1,src:l,mono:!0,slqn:o,pu:o.pu}:{kind:"inconnu",q:t.qty||1,src:l,mono:!0});continue}if("rond"===t.ty){const e=window.PAOCR.paRondCands(t),s=e.cands&&e.cands.length?chercherRef(e.cands):null,i=[t.d1&&"ø"+t.d1,t.d2&&"/"+t.d2,t.d3&&"/"+t.d3].filter(Boolean).join(""),o=e.metre?Math.max(1,Math.round((t.qty||1)*(t.mL||1))):t.qty||1,l=`${t.typ||"Pièce ronde"} ${i}${e.metre?` — ${t.qty}×${t.mL} m`:""}${r}`;null!==s?IMP.push({kind:"article",src:l,art:s,q:o}):IMP.push({kind:"inconnu",q:o,src:l});continue}IMP.push({kind:"inconnu",q:t.qty||1,src:`${e}${t.typ||"position"} — non reconnue${r}`})}}if(propositionBrdi(),t.surface){const e=Math.abs(i-t.surface);IMP.push({kind:"chap",titre:e<.05?`Contrôle : surface lue ${i.toFixed(2)} m² = surface totale du document ${t.surface} m² ✓`:`⚠ Contrôle : surface lue ${i.toFixed(2)} m² ≠ surface totale du document ${t.surface} m² — vérifie les positions signalées`})}}function asmVersImp(e,t){IMP=[],IMP.push({kind:"chap",titre:`PLAN D'ASSEMBLAGE — ${t.replace(/\.pdf$/i,"")} — ${e.pieces.length} pièce(s) relevée(s) sur le dessin coté`+(e.assemblage?` · assemblage créé automatiquement (majoration ${num("txAsm")||25} %)`:"")});for(const t of e.pieces){const r={mat:"galva",cls:"C",isoIdx:null,ang:90},s=t.pos?t.pos+" ":"",i=e.assemblage?t.grp:0,o=t.warn&&t.warn.length?" ⚠ "+t.warn.join(" ; "):"",l=t.A2&&t.B2?` (grande section ; sortie ${t.A2}×${t.B2})`:" (grande section)";if("rondcarre"===t.ty)IMP.push({kind:"piece",asmg:i,src:`${s}Rond-carré ${t.A}×${t.B} / ø${t.diam} L.${t.L}${o}`,params:{...r,ty:"rondcarre",A:t.A,B:t.B,L:t.L,q:t.qty||1}});else if("cone"===t.ty)IMP.push({kind:"piece",asmg:i,src:`${s}Cône / étage ${t.A}×${t.B} L.${t.L}${l}${o}`,params:{...r,ty:"cone",A:t.A,B:t.B,L:t.L,dec:t.dec||0,q:t.qty||1}});else if(t.L){const e=`${s}Gaine ${t.A}×${t.B} L.${t.L}${o}`;decoupeGaine({...r,ty:"gaine",A:t.A,B:t.B,L:t.L,q:1},t.L).forEach(r=>IMP.push({kind:"piece",asmg:i,src:e,params:{...r,q:r.q*(t.qty||1)}}))}else IMP.push({kind:"inconnu",q:1,src:`${s}pièce ${t.A}×${t.B} — dessin incomplet${o}`})}IMP.push({kind:"chap",titre:"Cotes lues directement dans le fichier DAO (aucune reconnaissance optique) — contrôle rapide conseillé sur le dessin"}),renderImport(0,IMP.filter(e=>"inconnu"===e.kind).length)}window.__ocrLog=e=>{/^ /.test(e)};const CFR_KEY="air-ventil.coffre",CFR_POSTE="air-ventil.poste",CFR_IT=6e5,CFR_ALPHA="0123456789ABCDEFGHJKMNPQRSTVWXYZ";let KCAT=null,KDATA=null,KCAT_RAW=null,KDATA_RAW=null,CFR=null;const cfrTE=new TextEncoder,cfrTD=new TextDecoder;function cfrB64(e){let t="";for(let r=0;r("0"+e.toString(16)).slice(-2)).join("").toUpperCase();try{localStorage.setItem(CFR_POSTE,e)}catch(e){}}return e}async function cfrEnvelopper(e){const t=cfrAlea(16),r=await cfrKek(e,t,6e5);return{s:cfrB64(t),it:6e5,id:CFR&&CFR.catId||"",cat:await cfrSceller(r,KCAT_RAW),dat:await cfrSceller(r,KDATA_RAW)}}async function cfrOuvrirEnv(e,t){const r=await cfrKek(t,cfrDeB64(e.s),e.it||6e5);return[await cfrDesceller(r,e.cat),await cfrDesceller(r,e.dat)]}async function cfrInstaller(e,t){KCAT_RAW=e,KDATA_RAW=t,KCAT=await cfrCle(e),KDATA=await cfrCle(t)}async function cfrActiver(e,t,r){const s=cfrUsine();if(!s)throw new Error("Ce fichier n'a pas de licence — il est incomplet.");let i;try{i=await cfrDesceller(await cfrKek(cfrNormCle(e),cfrDeB64(s.s),s.it),s.cat)}catch(e){throw new Error("Clé d'installation refusée. Recopie-la exactement, tirets compris.")}await cfrInstaller(i,cfrAlea(32));const o=cfrCleImprimable(25);return CFR={ver:1,catId:s.id,poste:cfrPoste(),cree:(new Date).toISOString(),u:{},rec:null},CFR.u[t]=await cfrEnvelopper(r),CFR.rec=await cfrEnvelopper(cfrNormCle(o)),cfrSauver(),o}async function cfrOuvrirSession(e,t){cfrCharger();const r=CFR&&CFR.u&&CFR.u[e];if(!r)return"inconnu";let s,i;try{[s,i]=await cfrOuvrirEnv(r,t)}catch(e){return"mdp"}await cfrInstaller(s,i);const o=cfrUsine();return o&&r.id&&r.id!==o.id?"catalogue":""}async function cfrSecours(e,t,r){if(cfrCharger(),!CFR||!CFR.rec)throw new Error("Ce poste n'a pas de clé de secours enregistrée.");let s,i;try{[s,i]=await cfrOuvrirEnv(CFR.rec,cfrNormCle(e))}catch(e){throw new Error("Clé de secours refusée.")}await cfrInstaller(s,i),CFR.u[t]=await cfrEnvelopper(r),cfrSauver()}async function cfrPoserMdp(e,t){if(!KCAT_RAW||!KDATA_RAW)throw new Error("Session non ouverte.");cfrCharger(),CFR&&(CFR.u[e]=await cfrEnvelopper(t),cfrSauver())}function cfrOublier(e){cfrCharger(),CFR&&CFR.u&&(delete CFR.u[e],cfrSauver())}async function cfrNouvelleCleSecours(){if(!KCAT_RAW)throw new Error("Session non ouverte.");const e=cfrCleImprimable(25);return CFR.rec=await cfrEnvelopper(cfrNormCle(e)),cfrSauver(),e}const ACC_KEY="air-ventil.acces";let ACC=null,ACC_USER=null;function accesDefaut(){return{ver:3,users:{celil:{nom:"Celil Küçükbiçakçi",succ:"NE",adm:!0}}}}function estAdmin(){return!!(ACC&&ACC_USER&&ACC.users[ACC_USER]&&ACC.users[ACC_USER].adm)}function accesCharger(){let e=null;try{e=localStorage.getItem(ACC_KEY)}catch(e){}if(e)try{ACC=JSON.parse(e)}catch(e){ACC=null}ACC&&ACC.users&&Object.keys(ACC.users).length||(ACC=accesDefaut(),accesSauver());const t=Object.keys(ACC.users);if(!t.some(e=>ACC.users[e].adm)){const e=ACC.users.celil?"celil":t[0];ACC.users[e].adm=!0,ACC.ver=3,accesSauver()}t.some(e=>ACC.users[e].h)&&(t.forEach(e=>{delete ACC.users[e].h}),ACC.ver=3,accesSauver())}function accesAppliquerRole(){const e=estAdmin();document.body.classList.toggle("employe",!e);const t=$("tarifsBody");t&&t.querySelectorAll("input, select, textarea, button").forEach(t=>{t.closest(".tar-lock")||t.closest(".tar-libre")||(t.disabled=!e,t.title=e?t.dataset.t0||t.title||"":"Seul un administrateur peut modifier les tarifs.")});const r=$("tarLock");r&&(r.hidden=e);try{arfRoleAppliquer()}catch(e){}try{manRoleAppliquer()}catch(e){}try{cliRoleAppliquer()}catch(e){}try{payRoleAppliquer()}catch(e){}const s=$("accNewBox");s&&(s.hidden=!e);const i=$("accNewAdm");i&&!e&&(i.checked=!1)}function accesSauver(){try{localStorage.setItem(ACC_KEY,JSON.stringify(ACC))}catch(e){}}function accesErreur(e){const t=$("accErr");t&&(t.textContent=e||"",t.hidden=!e)}let ACC_MSG="";async function accesConnexion(e,t){ACC||accesCharger();const r=String(e||"").toLowerCase().trim(),s=ACC.users[r];if(ACC_MSG="Identifiant ou mot de passe incorrect.",!s)return!1;const i=await cfrOuvrirSession(r,t);return"inconnu"===i?(ACC_MSG="Ce compte n'a pas encore de mot de passe sur ce poste — un administrateur doit lui en poser un.",!1):"catalogue"===i?($("accLicU")&&($("accLicU").value=r),accesPan("licence"),ACC_MSG="",!1):!i&&(ACC_MSG="",ACC_USER=r,await accesOuvrir(s,r),!0)}async function accesOuvrir(e,t){socDemarrer(ACC_SOC||e.succ),document.body.classList.remove("verrouille");const r=$("ecranAcces");r&&(r.hidden=!0);const s=$("accBarre");s&&(s.hidden=!1);const i=$("accQui");if(i&&(i.textContent=(e.nom||t)+(e.adm?" · administrateur":"")),socBoutonMaj(),await demarrerApp(),$("docRef")&&e.nom&&($("docRef").value=e.nom),!OFF.courant)try{offSuccOptions()}catch(e){}const o=$("accP");o&&(o.value="");try{localStorage.setItem(ACC_KEY+".dernier",t)}catch(e){}accesErreur(""),accesListe(),offRender(),accesAppliquerRole(),licMaj()}async function accesValider(e){e&&e.preventDefault();const t=$("accGo");t&&(t.disabled=!0,t.textContent="Ouverture du coffre…"),accesErreur("");let r=!1;try{r=await accesConnexion($("accU").value,$("accP").value)}catch(e){ACC_MSG="Le coffre n'a pas pu s'ouvrir : "+(e.message||e)}return t&&(t.disabled=!1,t.textContent="Ouvrir l'application"),!r&&ACC_MSG&&accesErreur(ACC_MSG),!1}const ACC_PANS={login:"accPanLogin",activation:"accPanActiv",cle:"accPanCle",secours:"accPanSecours",licence:"accPanLic",nocrypto:"accPanNo"};function accesPan(e){Object.keys(ACC_PANS).forEach(t=>{const r=$(ACC_PANS[t]);r&&(r.hidden=t!==e)})}function accesErr2(e,t){const r=$(e);r&&(r.textContent=t||"",r.hidden=!t)}let ACC_ACT_ID="";async function accesActiver(e){e&&e.preventDefault();const t=String($("accActU").value||"").toLowerCase().trim(),r=String($("accActNom").value||"").trim(),s=$("accActP").value,i=$("accActP2").value;if(accesErr2("accActErr",""),!/^[a-z0-9._-]{2,}$/.test(t))return accesErr2("accActErr","Identifiant : au moins 2 signes, sans espace ni accent."),!1;if(!s||s.length<4)return accesErr2("accActErr","Mot de passe : 4 signes au minimum."),!1;if(s!==i)return accesErr2("accActErr","Les deux mots de passe ne correspondent pas."),!1;const o=$("accActGo");o&&(o.disabled=!0,o.textContent="Activation…");let l="";try{l=await cfrActiver($("accActK").value,t,s)}catch(e){return accesErr2("accActErr",e.message||String(e)),o&&(o.disabled=!1,o.textContent="Activer ce poste"),!1}o&&(o.disabled=!1,o.textContent="Activer ce poste"),ACC||accesCharger();const u=String($("accActSucc")&&$("accActSucc").value||"").toUpperCase(),d=entDef()[u]?u:"NE";ACC.users[t]?(ACC.users[t].adm=!0,r&&(ACC.users[t].nom=r),ACC.users[t].succ=d):ACC.users[t]={nom:r||t,succ:d,adm:!0},accesSauver(),accesListe(),ACC_ACT_ID=t,$("accCleTexte").textContent=l;const m=$("accCleLu");m&&(m.checked=!1);const h=$("accCleGo");return h&&(h.disabled=!0),accesPan("cle"),!1}async function accesCleValider(){const e=ACC_ACT_ID||Object.keys(ACC.users)[0];$("accCleTexte").textContent="",ACC_USER=e,await accesOuvrir(ACC.users[e]||{},e)}function accesImprimerCle(){const e=$("accCleTexte").textContent,t=window.open("","_blank","width=680,height=520");t?(t.document.write("Clé de secours — Calculateur ASCV
Air-Ventil Sàrl — Calculateur ASCV
Clé de secours du poste n° "+cfrPoste()+" — "+(new Date).toLocaleDateString("fr-CH")+'
'+e+"
Cette clé rouvre les données de ce poste si le mot de passe est perdu. Sans elle et sans mot de passe, les offres et les tarifs enregistrés sont définitivement illisibles : le chiffrement n'a pas de porte de service.
Range cette feuille ailleurs que sur ce PC — un coffre, un classeur fermé. Quiconque l'a en main peut ouvrir l'application sur ce poste.
"),t.document.close(),t.focus(),setTimeout(()=>{try{t.print()}catch(e){}},260)):toast("Le navigateur a bloqué la fenêtre d'impression — recopie la clé à la main.")}async function accesSecours(e){e&&e.preventDefault();const t=String($("accSecU").value||"").toLowerCase().trim(),r=$("accSecP").value;if(accesErr2("accSecErr",""),ACC||accesCharger(),!ACC.users[t])return accesErr2("accSecErr","Cet identifiant n'existe pas sur ce poste."),!1;if(!r||r.length<4)return accesErr2("accSecErr","Mot de passe : 4 signes au minimum."),!1;try{await cfrSecours($("accSecK").value,t,r)}catch(e){return accesErr2("accSecErr",e.message||String(e)),!1}return ACC_USER=t,await accesOuvrir(ACC.users[t],t),!1}async function accesRecle(e){e&&e.preventDefault();const t=cfrUsine(),r=String($("accLicU").value||"").toLowerCase().trim(),s=$("accLicP").value;accesErr2("accLicErr",""),cfrCharger();const i=CFR&&CFR.u&&CFR.u[r];if(!i)return accesErr2("accLicErr","Compte inconnu sur ce poste."),!1;let o,l;try{o=(await cfrOuvrirEnv(i,s))[1]}catch(e){return accesErr2("accLicErr","Mot de passe incorrect."),!1}try{l=await cfrDesceller(await cfrKek(cfrNormCle($("accLicK").value),cfrDeB64(t.s),t.it),t.cat)}catch(e){return accesErr2("accLicErr","Clé d'installation refusée."),!1}return await cfrInstaller(l,o),CFR.catId=t.id,CFR.u[r]=await cfrEnvelopper(s),cfrSauver(),ACC_USER=r,await accesOuvrir(ACC.users[r]||{},r),!1}function licMaj(){const e=cfrPoste(),t=$("licPoste");t&&(t.textContent=e);const r=$("licPosteI");r&&(r.value=e);const s=cfrUsine(),i=$("licCat");i&&(i.value=s&&s.cat_nom?s.cat_nom:s&&s.id?s.id:"—");const o=$("licDate");if(o){let e=cfrCharger()&&CFR.cree?CFR.cree:"";o.value=e?new Date(e).toLocaleDateString("fr-CH"):"—"}}async function accesRefaireCle(){if(!estAdmin())return void toast("Seul un administrateur peut renouveler la clé de secours.");if(!confirm("Tirer une nouvelle clé de secours ?\n\nL'ancienne cessera immédiatement de fonctionner. Note ou imprime la nouvelle tout de suite."))return;let e="";try{e=await cfrNouvelleCleSecours()}catch(e){return void toast("Impossible : "+(e.message||e))}const t=$("licCleTexte");t&&(t.textContent=e,t.hidden=!1);const r=$("accCleTexte");r&&(r.textContent=e,accesImprimerCle(),r.textContent=""),toast("Nouvelle clé de secours — l'ancienne ne fonctionne plus.")}function accesVerrouiller(){offSauverMaintenant(),ACC_USER=null,accesPan("login"),ACC_SOC_MAIN=!1,ACC_SOC="",accesSocSuggerer(),accesSocRender(),document.body.classList.add("verrouille");const e=$("ecranAcces");e&&(e.hidden=!1);const t=$("accBarre");t&&(t.hidden=!0),socMenuFermer();const r=$("accP");r&&(r.value="",r.focus())}async function accesMdpChanger(){if(!ACC_USER)return void toast("Connecte-toi d'abord.");const e=$("accMdpA").value,t=$("accMdpN").value,r=$("accMdpC").value,s=cfrCharger()&&CFR.u?CFR.u[ACC_USER]:null;if(s){try{await cfrOuvrirEnv(s,e)}catch(e){return void toast("Mot de passe actuel incorrect.")}!t||t.length<4?toast("Le nouveau mot de passe doit faire au moins 4 caractères."):t===r?(await cfrPoserMdp(ACC_USER,t),$("accMdpA").value=$("accMdpN").value=$("accMdpC").value="",toast("Mot de passe changé. Ta clé de secours reste valable.")):toast("Les deux nouveaux mots de passe ne correspondent pas.")}else toast("Ce compte n'a pas d'enveloppe sur ce poste.")}async function accesAjouter(){if(!estAdmin())return void toast("Seul un administrateur peut créer un compte.");const e=String($("accNewU").value||"").toLowerCase().trim(),t=$("accNewNom").value.trim(),r=accSucc(),s=$("accNewMdp").value,i=!(!$("accNewAdm")||!$("accNewAdm").checked);/^[a-z0-9._-]{2,}$/.test(e)?ACC.users[e]?toast("Cet identifiant existe déjà."):!s||s.length<4?toast("Mot de passe : 4 caractères au minimum."):(ACC.users[e]={nom:t||e,succ:r,adm:i},await cfrPoserMdp(e,s),accesSauver(),accesListe(),$("accNewU").value=$("accNewNom").value=$("accNewMdp").value="",$("accNewAdm")&&($("accNewAdm").checked=!1),toast("Utilisateur « "+e+" » ajouté"+(i?" comme administrateur.":" (employé)."))):toast("Identifiant : au moins 2 caractères, sans espace ni accent.")}function accesRole(e){if(!estAdmin())return void toast("Seul un administrateur peut changer un rôle.");const t=ACC.users[e];t&&(t.adm&&Object.keys(ACC.users).filter(e=>ACC.users[e].adm).length<2?toast("Il doit rester au moins un administrateur."):(t.adm=!t.adm,accesSauver(),accesListe(),accesAppliquerRole(),toast("« "+e+" » est maintenant "+(t.adm?"administrateur.":"employé."))))}function accesSupprimer(e){estAdmin()?e!==ACC_USER?Object.keys(ACC.users).length<2?toast("Il doit rester au moins un utilisateur."):ACC.users[e]&&ACC.users[e].adm&&Object.keys(ACC.users).filter(e=>ACC.users[e].adm).length<2?toast("Il doit rester au moins un administrateur."):confirm("Supprimer l'utilisateur « "+e+" » ?")&&(delete ACC.users[e],cfrOublier(e),accesSauver(),accesListe()):toast("Impossible de supprimer le compte avec lequel tu es connecté."):toast("Seul un administrateur peut supprimer un compte.")}function accSucc(){const e=String($("accNewSucc")&&$("accNewSucc").value||"").toUpperCase();return entDef()[e]&&(socGroupe()||e===SOC_ACT)?e:SOC_ACT}function accActSuccOptions(){const e=$("accActSucc");if(!e)return;const t=entDef(),r=Object.keys(t).map(e=>t[e]).sort((e,t)=>(e.ordre||0)-(t.ordre||0)),s=String(e.value||"").toUpperCase();e.innerHTML=r.map(e=>'").join(""),e.value=t[s]?s:socDuPoste()||"NE"}function accSuccCellule(e,t){const r=entDef(),s=Object.keys(r).map(e=>r[e]).sort((e,t)=>(e.ordre||0)-(t.ordre||0)),i=String(t||"NE").toUpperCase();return""}function accesSucc(e,t){if(!estAdmin())return toast("Seul un administrateur peut changer la succursale."),void accesListe();const r=String(t||"").toUpperCase();ACC.users[e]&&entDef()[r]?(ACC.users[e].succ=r,accesSauver(),accesListe(),toast("Succursale de "+e+" : "+entNom(r)+" — active a la prochaine ouverture.")):accesListe()}function accSuccOptions(){const e=$("accNewSucc");if(!e)return;const t=entListe().filter(e=>entVue().indexOf(e.id)>=0),r=e.value;e.innerHTML=(t.length?t:entListe()).map(e=>'").join(""),e.value=t.some(e=>e.id===r)?r:accSucc(),e.disabled=t.length<2}function accesListe(){try{accSuccOptions()}catch(e){}const e=$("accListe");if(!e)return;const t=estAdmin(),r=Object.keys(ACC.users).sort();e.innerHTML=`
${offEsc(e.no||"—")}${e.id===OFF.courant?' à l’écran':""}
\n
${offEsc(e.chantier||"")}
${offEsc(e.client||"")}
\n
${e.np||0}
${fmt(+e.ht2||0)}
\n
${offEsc(offDate(e.envoi))}
${offEsc(offDate(e.accept))}
\n
${oui(e.adjug)}
${oui(e.refus)}
\n
${offEsc(e.comm||"")}
\n
\n \n
`}).join("")+"
":e.innerHTML='
'+(offVue().length?"Aucune offre ne correspond à cette recherche.":"Aucune offre enregistrée pour l'instant — elles s'ajoutent ici toutes seules dès la première position.")+"
";const i=$("offEtat");if(i){const e=Math.round(JSON.stringify(OFF).length/1024);i.innerHTML=OFF_OK?OFF_PLEIN?`⚠ La sauvegarde du navigateur est pleine (${e} Ko). Exporte tes offres, puis supprime les plus anciennes — sinon les nouvelles ne s'enregistrent plus.`:`${offVue().length} offre(s) · ${socGroupe()?"tout le groupe":offEsc(entNom(SOC_ACT))}`+(OFF.offres.length>offVue().length?` · ${OFF.offres.length-offVue().length} chez les autres sociétés`:"")+` · ${e} Ko`:"⚠ Ce navigateur refuse d'enregistrer les offres pour un fichier local. Utilise Exporter la sauvegarde avant de fermer."}}async function offDemarrer(){await offCharger(),["docClient","docChantier","docDate","docNo","docRef","docVotreRef","docAdr","docMandat","docNoteRab","docComm","docEnvoi","docAccept"].forEach(e=>{const t=$(e);t&&t.addEventListener("input",offAuto)});const e=$("docChantier");e&&e.addEventListener("change",()=>{try{manNoter(e.value,$("docClient")&&$("docClient").value||"",!0),manCondMaj()}catch(e){}}),["docClient","docChantier","docNo","docDate","docComm","docEnvoi","docAccept"].forEach(e=>{const t=$(e);t&&t.addEventListener("input",()=>{clearTimeout(OFF_TR),OFF_TR=setTimeout(offRender,350)})});const t=offMenage(),r=offReprendre();offMiroirRetour(),offRender(),r?setTimeout(()=>toast("Offre "+(r.no||"en cours")+" reprise là où tu l'avais laissée — "+(r.positions||[]).length+" position(s)."),900):t&&setTimeout(()=>toast(t+" fiche(s) vide(s) retirée(s) du registre — aucune offre numérotée n'a été touchée."),900)}const FACT_KEY="air-ventil.factures",FACT_DEP=100001;let FACT={ver:1,compteurs:{NE:100001,VD:100001},factures:[],courant:null},FACT_OK=!0,FACT_PLEIN=!1,FACT_TR=null;async function factCharger(){const e=await cfrLire(FACT_KEY);if(e)try{const t=JSON.parse(e);t&&"object"==typeof t&&(Array.isArray(t.factures)&&(FACT.factures=t.factures),FACT.compteurs={NE:100001,VD:100001,...t.compteurs||{}},FACT.courant=t.courant||null)}catch(e){}}function factSauver(){FACT_OK&&KDATA&&cfrEcrire(FACT_KEY,JSON.stringify(FACT)).then(e=>{FACT_PLEIN=!e})}function factNouvelId(){return"f"+Date.now().toString(36)+Math.random().toString(36).slice(2,6)}function factNumUtilises(e){const t=new Set;return FACT.factures.forEach(r=>{const s=/^F(NE|VD)\s*(\d+)$/.exec(String(r.no||"").trim());s&&s[1]===e&&t.add(+s[2])}),t}function factNumPrendre(e){const t=factNumUtilises(e="VD"===e?"VD":"NE"),r=Math.max(100001,Math.round(entite(e).serieDep)||0);let s=Math.max(r,Math.round(FACT.compteurs[e]||100001));for(;t.has(s);)s++;return FACT.compteurs[e]=s+1,"F"+e+s}function factPrefixeTR(){const e=entite("TR");return String(e.prefixe||"HKT").toUpperCase().replace(/[^A-Z]/g,"").padEnd(3,"X").slice(0,3)}function factAnneeTR(){return(new Date).getFullYear()}function factNumTR(e,t,r){return String(e)+String(t)+("000000000"+Math.max(1,Math.round(r)||1)).slice(-9)}function factNumUtilisesTR(e,t){const r=new Set,s=new RegExp("^"+e+t+"(\\d{9})$");return FACT.factures.forEach(e=>{const t=s.exec(String(e.no||"").trim().toUpperCase());t&&r.add(+t[1])}),r}function factNumPrendreTR(){const e=entite("TR"),t=factPrefixeTR(),r=factAnneeTR(),s="TR"+r,i=factNumUtilisesTR(t,r);let o=Math.max(1,Math.round(e.serieDep)||1,Math.round(FACT.compteurs[s])||0);for(;i.has(o);)o++;return FACT.compteurs[s]=o+1,factNumTR(t,r,o)}function factNumPrendreEnt(e){const t=String(null==e?"NE":e).toUpperCase();return"TR"===entPays(t)?factNumPrendreTR():factNumPrendre("VD"===t?"VD":"NE")}function factDevise(e){return entiteDe(e).devise||"CHF"}function devNom(e){return"TRY"===String(e||"CHF").toUpperCase()?"TL":"CHF"}function fmtDev(e,t){const r=+e||0;return"TRY"===String(t||"CHF").toUpperCase()?r.toLocaleString("tr-TR",{minimumFractionDigits:2,maximumFractionDigits:2}):fmt(r)}function factSomme(e,t){const r=factDevise(t);return fmtDev(e,r)+" "+devNom(r)}function flDevise(){const e=void 0!==FACT_LIB&&FACT_LIB&&FACT_LIB.id?FACT.factures.find(e=>e.id===FACT_LIB.id):null;if(e)return factDevise(e);try{return entite(flSucc()).devise||"CHF"}catch(e){return"CHF"}}function flDevNom(){return devNom(flDevise())}function factCalc(e){const t={},r=(e=e||{}).taux||{};(e.lignes||[]).forEach(e=>{const r=e.grp||"Standard";t[r]=(t[r]||0)+(+e.pu||0)*(+e.q||0)});const s=+e.tva||0,i=+e.rem||0,o=Object.keys(t).map(e=>{const s=r2(t[e]),i=+(r[e]||0),o=r2(s*i/100);return{g:e,r:i,ht:s,rab:o,ht2:r2(s-o)}}),l=r2(o.reduce((e,t)=>e+t.ht,0)),u=r2(o.reduce((e,t)=>e+t.rab,0)),d=r2((l-u)*i/100),m=r2(l-u-d);if(factEstTR(e))return factCalcTR(e,o,l,u,d,m);const h=r2(m*s/100),g=Math.round(20*(m+h))/20;return o.forEach(e=>{e.tva=r2(e.ht2*s/100),e.ttc=r2(e.ht2+e.tva)}),{grp:o,ht:l,rab:u,rem:d,ht2:m,tva:h,ttc:r2(g),du:r2(g)}}var TEVK_SEUIL=12e3,TEVK=[{c:"",n:0,d:10,l:"Aucune retenue"},{c:"601",n:4,d:10,l:"Yapım işleri — travaux de construction et d'installation, y compris les prestations d'ingénierie qui les accompagnent"},{c:"602",n:9,d:10,l:"Etüt, plan-proje, danışmanlık, denetim — études, plans, conseil, contrôle"},{c:"603",n:7,d:10,l:"Makine ve teçhizat bakım-onarım — entretien et réparation de machines et d'équipements"},{c:"604",n:5,d:10,l:"Yemek servisi — service de repas"},{c:"606",n:9,d:10,l:"İşgücü temini — mise à disposition de personnel"},{c:"607",n:9,d:10,l:"Özel güvenlik — sécurité privée"},{c:"612",n:9,d:10,l:"Temizlik — nettoyage"},{c:"613",n:9,d:10,l:"Çevre ve bahçe bakımı — entretien des espaces verts"},{c:"614",n:5,d:10,l:"Servis taşımacılığı — transport de personnel"},{c:"615",n:7,d:10,l:"Baskı ve basım — impression"},{c:"624",n:2,d:10,l:"Karayoluyla yük taşımacılığı — transport routier de marchandises"},{c:"625",n:3,d:10,l:"Ticari reklam — publicité commerciale"}];function tevkListe(){return TEVK}function tevkDe(e){const t=String(null==e?"":e).trim();return TEVK.find(e=>e.c===t)||TEVK[0]}function tevkOranTxt(e){return e&&e.c&&e.n?e.n+"/"+e.d:""}function tevkSeuil(){const e=+entite("TR").tevkSeuil;return e>0?e:TEVK_SEUIL}function factEstTR(e){return"TR"===entiteDe(e).pays}function factKdvLigne(e,t){const r=t&&void 0!==t.kdv&&null!==t.kdv&&""!==t.kdv?+t.kdv:+(e&&e.tva)||0;return Math.max(0,r)}function factTevkifat(e,t,r){const s=tevkDe(e&&e.tevk);return s.c&&s.n?r2(t){const r=factKdvLigne(e,t);d[r]=r2((d[r]||0)+r2(factLigneHT2(e,t)*(1-l/100)))});const m=Object.keys(d).map(Number).sort((e,t)=>e-t).map(e=>({pc:e,matrah:d[e],kdv:r2(d[e]*e/100)}));if(m.length){const e=r2(m.reduce((e,t)=>e+t.matrah,0)),t=r2(o-e);if(t){let e=0;m.forEach((t,r)=>{t.pc>m[e].pc&&(e=r)}),m[e].matrah=r2(m[e].matrah+t),m[e].kdv=r2(m[e].matrah*m[e].pc/100)}}const h=r2(m.reduce((e,t)=>e+t.kdv,0)),g=r2(o+h),b=factTevkifat(e,g,h);return t.forEach(e=>{e.tva=r2(e.ht2*u/100),e.ttc=r2(e.ht2+e.tva)}),{grp:t,ht:r,rab:s,rem:i,ht2:o,tva:h,ttc:g,kdvs:m,pays:"TR",tevkCode:b.code,tevkOran:b.oran,tevkSous:b.sous,tevkSeuil:b.seuil,tevkifat:b.montant,kdvNet:r2(h-b.montant),du:r2(g-b.montant)}}function factDu(e){const t=factCalc(e);return r2(void 0===t.du?t.ttc:t.du)}function factLigneHT2(e,t){const r=+((e.taux||{})[t.grp||"Standard"]||0);return r2((+t.pu||0)*(+t.q||0)*(1-r/100))}function factDejaFacture(e){const t={};return e?(FACT.factures.forEach(r=>{r.offreId===e&&"annulee"!==r.etat&&(r.lignes||[]).forEach(e=>{const r=void 0===e.pi||null===e.pi?"d"+(e.desc||""):"p"+e.pi;t[r]=r2((t[r]||0)+(+e.q||0))})}),t):t}function factClePos(e,t){return null==t?"d"+(e.desc||""):"p"+t}function factDeLOffre(e){return FACT.factures.filter(t=>t.offreId&&t.offreId===e)}function factAujourdhui(){const e=new Date,z=e=>("0"+e).slice(-2);return e.getFullYear()+"-"+z(e.getMonth()+1)+"-"+z(e.getDate())}function factMvtId(e){let t=1,r=new Set((e.mvts||[]).map(e=>e.id));for(;r.has("mv"+t);)t++;return"mv"+t}function factMvts(e){if(!e)return[];if(!Array.isArray(e.mvts)){e.mvts=[];const t=+e.regle||0;if(t>0||"payee"===e.etat){const r=t>0?t:factDu(e);r>0&&e.mvts.push({id:"mv1",date:e.paye||e.date||factAujourdhui(),montant:r,escompte:0,origine:"repris",ref:"",payeur:"",note:"encaissement repris de l'ancienne version",par:"",cree:e.maj||""})}}return e.mvts}function factRegle(e){return e?r2(factMvts(e).reduce((e,t)=>e+(+t.montant||0)+(+t.escompte||0),0)):0}function factRecu(e){return e?r2(factMvts(e).reduce((e,t)=>e+(+t.montant||0),0)):0}function factEscompteAccorde(e){return e?r2(factMvts(e).reduce((e,t)=>e+(+t.escompte||0),0)):0}function factDateCloture(e){const t=factMvts(e).map(e=>e.date).filter(Boolean).sort();return t.length?t[t.length-1]:""}function factFinaliser(e){if(!e)return;factMvts(e),e.regle=factRegle(e),"payee"===e.etat&&(e.etat="ouverte");const t=factSolde(e);"annulee"!==e.etat&&"brouillon"!==e.etat&&e.mvts.length&&t<=.005?(e.cloture=factDateCloture(e),e.paye=e.cloture):(e.cloture="",e.paye=""),e.maj=(new Date).toISOString()}function factEtat(e){return e?"annulee"===e.etat?"annulee":"brouillon"===e.etat?"brouillon":factSolde(e)<=.005&&(factMvts(e).length||"payee"===e.etat)?"payee":e.echeance&&e.echeance{const s=r.dataset.pays===t;r.hidden=!e&&!s,r.textContent=e?"Factures "+factPaysNom(r.dataset.pays):"Factures"})}function factPaysNom(e){return"TR"===String(e||FACT_PAYS).toUpperCase()?"Turquie":"Suisse"}function factEntitesPays(e){const t="TR"===String(e||FACT_PAYS).toUpperCase()?"TR":"CH";return entListe().filter(e=>e.pays===t)}function factDansLaVue(e){return socOk(e)&&entiteDe(e).pays===FACT_PAYS}function socEntitesPays(e){const t=factEntitesPays(e);if(socGroupe())return t;const r=t.filter(e=>e.id===SOC_ACT);return r.length?r:t}function factEncoursDe(e){const t="TR"===String(e||FACT_PAYS).toUpperCase()?"TR":"CH";return r2(factVue().filter(e=>entiteDe(e).pays===t).reduce((e,t)=>e+factSolde(t),0))}function factEncaisseDe(e){const t="TR"===String(e||FACT_PAYS).toUpperCase()?"TR":"CH";return r2(factVue().filter(e=>entiteDe(e).pays===t&&"annulee"!==e.etat).reduce((e,t)=>e+factRecu(t),0))}function factSommePays(e,t){const r="TR"===String(t||FACT_PAYS).toUpperCase()?"TRY":"CHF";return fmtDev(e,r)+" "+devNom(r)}function factPaysAppliquer(){const e="TR"===FACT_PAYS,t=socEntitesPays(),r=document.querySelector("#cardFactures h2");r&&(r.textContent="Factures "+factPaysNom());const s=$("factSoc"),i=$("factSocLab");if(s){const e=s.value;s.innerHTML=''+t.map(e=>'").join(""),s.value=t.some(t=>t.id===e)?e:""}i&&(i.hidden=t.length<2);const o=$("flSucc");if(o&&!o.disabled){const e=o.value;o.innerHTML=t.map(e=>'").join(""),o.value=t.some(t=>t.id===e)?e:t[0]?t[0].id:"NE"}const l=$("flNoteBr");return l&&(l.hidden=!1),e}function factEncours(){return r2(factVue().reduce((e,t)=>e+factSolde(t),0))}function factTotalEncaisse(){return r2(factVue().reduce((e,t)=>e+("annulee"===t.etat?0:factRecu(t)),0))}function factEncaisser(e,t){const r=FACT.factures.find(t=>t.id===e);if(!r)return null;if("annulee"===r.etat)return toast("Cette facture est annulée — elle ne peut plus être encaissée."),null;if(factBrouillon(r))return toast("Ce brouillon n'est pas encore une facture : comptabilise-le d'abord."),null;const s=r2(+t.montant||0),i=r2(+t.escompte||0);if(0===s&&0===i)return toast("Un encaissement sans montant, cela n'existe pas."),null;const o={id:factMvtId(r),date:t.date||factAujourdhui(),montant:s,escompte:i,origine:t.origine||"main",ref:String(t.ref||""),payeur:String(t.payeur||""),note:String(t.note||""),par:void 0!==ACC_USER&&ACC_USER?ACC_USER:"",cree:(new Date).toISOString()};return factMvts(r).push(o),factFinaliser(r),o}function factMvtSupprimer(e,t){const r=FACT.factures.find(t=>t.id===e);if(!r)return!1;const s=factMvts(r),i=s.findIndex(e=>e.id===t);return!(i<0)&&(s.splice(i,1),factFinaliser(r),!0)}function factPayer(e){const t=FACT.factures.find(t=>t.id===e);if(!t)return;if("annulee"===t.etat)return void toast("Cette facture est annulée — elle ne peut plus être encaissée.");if(factBrouillon(t))return void toast("Ce brouillon n'est pas encore une facture : comptabilise-le d'abord, il partira alors chez le client.");if("payee"===factEtat(t)){const e=factMvts(t).length;if(!confirm("Rouvrir la facture "+t.no+" ?\n\nSes "+e+" encaissement(s) seront effacés du registre et la facture repassera dans l'encours."))return;return t.mvts=[],factFinaliser(t),factSauver(),factRender(),void toast("Facture "+t.no+" remise en « ouverte ».")}const r=factSolde(t);factEncaisser(e,{montant:r,origine:"main",note:"soldé depuis la liste des factures"})&&(factSauver(),factRender(),toast("Facture "+t.no+" encaissée de "+factSomme(r,t)+" — solde à zéro, elle est clôturée."))}function factAnnuler(e){const t=FACT.factures.find(t=>t.id===e);if(t)return"annulee"===t.etat?(t.etat="ouverte",t.maj=(new Date).toISOString(),factSauver(),factRender(),void toast("Facture "+t.no+" réactivée.")):void(confirm("Annuler la facture "+t.no+" ?\n\nElle reste au registre avec son numéro, elle sort des totaux, et ses quantités redeviennent facturables sur l'offre.")&&(t.etat="annulee",t.paye="",t.cloture="",t.maj=(new Date).toISOString(),t.regle=factRegle(t),factSauver(),factRender(),toast("Facture "+t.no+" annulée — son numéro reste pris, comme le veut la comptabilité.")))}function factComptabiliser(e){const t=FACT.factures.find(t=>t.id===e);if(!t)return!1;if(!factBrouillon(t))return toast("La facture "+t.no+" est déjà comptabilisée."),!1;if(!(t.lignes||[]).length)return toast("Ce brouillon n'a aucune ligne — il n'y a rien à comptabiliser."),!1;if(!String(t.client||"").trim())return toast("Ce brouillon n'a pas de client — ouvre-le et complète-le."),!1;const r=String(t.date||"").trim()||factAujourdhui(),s=factJours(r,factPaiementDe(t).delai||30),i=factCalc(t);return!!confirm("Comptabiliser la facture "+t.no+" ?\n\nDate de facturation : "+offDate(r)+"\nÉchéance : "+offDate(s)+"\nTotal TTC : "+factSomme(i.ttc,t)+"\n\nElle devient une pièce comptable : elle entre dans l'encours, elle s'imprime avec son bulletin de versement, et elle ne se modifie plus — sauf à la décomptabiliser, ce que seul un administrateur peut faire.")&&(t.etat="ouverte",t.date=r,t.echeance=s,t.compta=factAujourdhui(),t.comptaPar=void 0!==ACC_USER&&ACC_USER?ACC_USER:"",t.maj=(new Date).toISOString(),factSauver(),factRender(),toast("Facture "+t.no+" comptabilisée au "+offDate(r)+" — échéance "+offDate(s)+"."),!0)}function factDecomptabiliser(e){const t=FACT.factures.find(t=>t.id===e);return!!t&&(factBrouillon(t)?(toast("La facture "+t.no+" est déjà un brouillon."),!1):"function"!=typeof estAdmin||estAdmin()?"annulee"===t.etat?(toast("Cette facture est annulée : réactive-la d'abord si tu veux la reprendre."),!1):factMvts(t).length||factRegle(t)>0?(toast("Facture déjà encaissée : on ne la réécrit pas. Annule-la et refais-en une."),!1):!!confirm("Décomptabiliser la facture "+t.no+" ?\n\nElle redevient un brouillon : modifiable, hors de l'encours, et elle ne s'imprimera plus avec son bulletin de versement.\n\nSon numéro "+t.no+" lui reste acquis — il ne resservira jamais ailleurs.")&&(t.etat="brouillon",t.paye="",t.regle=0,t.decompta=factAujourdhui(),t.decomptaPar=void 0!==ACC_USER&&ACC_USER?ACC_USER:"",t.maj=(new Date).toISOString(),factSauver(),factRender(),toast("Facture "+t.no+" décomptabilisée — c'est de nouveau un brouillon, corrige-la puis recomptabilise-la."),!0):(toast("Seul un administrateur peut décomptabiliser une facture."),!1))}function factSupprimer(e){return toast("Suppression interdite : une facture enregistrée ne s'efface pas, elle s'annule."),!1}function factPaiementDef(){return{iban:"CH7330767000S52136042",nom:"Air-Ventil SA",rue:"Chemin du Theyeret",no:"1",npa:"2017",loc:"Boudry",pays:"CH",tva:"CHE-114 816 586",ident:"102785",refScor:!1,delai:30,escPc:2,escJours:10,pied:"Merci d'effectuer le paiement au moyen du bulletin ci-dessous."}}function factPaiement(){const e=void 0!==STORE&&STORE&&STORE.paiement?STORE.paiement:{};return{...factPaiementDef(),...e}}function entDef(){const e=factPaiementDef();return{VD:{id:"VD",pays:"CH",devise:"CHF",ordre:1,nom:"Air-Ventil SA",site:"Romanel",court:"Romanel",rue:"",no:"",npa:"",loc:"Romanel-sur-Lausanne",ipays:"CH",iban:"",tva:e.tva,ident:"",refScor:!1,cpte:"",tvaPc:8.1,delai:30,escPc:2,escJours:10,pied:e.pied,serieDep:100001,offDep:24e4},NE:{id:"NE",pays:"CH",devise:"CHF",ordre:2,nom:e.nom,site:"Boudry",court:"Boudry",rue:e.rue,no:e.no,npa:e.npa,loc:e.loc,ipays:e.pays,iban:e.iban,tva:e.tva,ident:e.ident,refScor:e.refScor,cpte:"BCV S52 13 60 42",tvaPc:8.1,delai:e.delai,escPc:e.escPc,escJours:e.escJours,pied:e.pied,serieDep:100001,offDep:14e4},TR:{id:"TR",pays:"TR",devise:"TRY",ordre:3,nom:"HK Teknik A.Ş.",site:"Kırşehir",court:"Kırşehir",rue:"",no:"",npa:"",loc:"Kırşehir",ipays:"TR",iban:"",vd:"",vkn:"",mersis:"",sicil:"",prefixe:"HKT",serieDep:1,offDep:34e4,eArsiv:!1,profil:"TICARI",tvaPc:20,delai:30,escPc:0,escJours:0,pied:""}}}function entites(){const e=entDef(),t=void 0!==STORE&&STORE&&STORE.entites?STORE.entites:{},r=factPaiement();return e.NE={...e.NE,nom:r.nom,rue:r.rue,no:r.no,npa:r.npa,loc:r.loc,ipays:r.pays,iban:r.iban,tva:r.tva,ident:r.ident,refScor:r.refScor,delai:r.delai,escPc:r.escPc,escJours:r.escJours,pied:r.pied},Object.keys(e).forEach(r=>{t[r]&&(e[r]={...e[r],...t[r]})}),e}function entite(e){const t=entites();return t[String(null==e?"":e).toUpperCase()]||t.NE}function entIds(){const e=entites();return Object.keys(e).sort((t,r)=>(e[t].ordre||9)-(e[r].ordre||9))}function entListe(){const e=entites();return entIds().map(t=>e[t])}function entiteDe(e){return entite(e&&(e.ent||e.succ)||null)}function entDuNumero(e){const t=String(null==e?"":e).trim().toUpperCase().replace(/\s+/g,"");if(!t)return null;const r=entites(),s=Object.keys(r);for(let e=0;e
",factModMaj()}function factModTout(e){FACT_MOD&&(FACT_MOD.lignes.forEach((t,r)=>{const s=$("fmc"+r);s&&!s.disabled&&(s.checked=!!e)}),factModMaj())}function factModLignes(){if(!FACT_MOD)return[];const e=[];return FACT_MOD.lignes.forEach((t,r)=>{const s=$("fmc"+r),i=$("fmq"+r);if(!s||!s.checked||s.disabled)return;let o=i&&parseFloat(String(i.value).replace(",","."))||0;o>t.reste&&(o=t.reste,i&&(i.value=o)),o<0&&(o=0,i&&(i.value=0)),o<=0||e.push({pi:t.pi,desc:t.desc,grp:t.grp,pu:t.pu,q:r2(o)})}),e}function factModApercu(){const e=factModLignes(),t=factModEnt(),r=entite(t);return{lignes:e,taux:FACT_MOD?FACT_MOD.taux:{},ent:t,tva:"TR"===r.pays?r.tvaPc:FACT_MOD?FACT_MOD.tva:0,rem:FACT_MOD?FACT_MOD.rem:0}}function factModMaj(){if(!FACT_MOD)return;const e=new Set(factModLignes().map(e=>e.pi)),t=factModApercu(),r=factCalc(t);FACT_MOD.lignes.forEach((r,s)=>{const i=$("fmr"+s),o=$("fmh"+s),l=e.has(r.pi);if(i&&i.classList.toggle("fm-pris",l),o){const e=l&&parseFloat(String(($("fmq"+s)||{}).value||0).replace(",","."))||0;o.textContent=l?pfD(factLigneHT2(t,{grp:r.grp,pu:r.pu,q:e}),factDevise(t)):"—"}});const s=$("factModTot");s&&(s.innerHTML=t.lignes.length?`${t.lignes.length} position(s)HT 2 ${fmtDev(r.ht2,factDevise(t))}${entEstTR(t.ent||t.succ)?"KDV":"TVA"} ${fmtDev(r.tva,factDevise(t))}Total TTC ${factSomme(r.ttc,t)}`:'Aucune position cochée — rien à facturer.')}function factModValider(){if(!FACT_MOD)return;const e=OFF.offres.find(e=>e.id===FACT_MOD.offreId);if(!e)return toast("Offre introuvable — elle a peut-être été rechargée."),void factModFermer();const t=factModLignes();if(!t.length)return void toast("Coche au moins une position, avec une quantité au-dessus de zéro.");const r=factModEnt(),s=$("factModDate")&&$("factModDate").value||factAujourdhui(),i=$("factModEch")&&$("factModEch").value||factJours(s,factPaiementDe(r).delai||30),o=(new Date).toISOString(),l={id:factNouvelId(),no:factNumPrendreEnt(r),succ:r,ent:r,date:s,echeance:i,offreId:e.id,offreNo:e.no||"",client:e.client||"",adr:e.adr||"",chantier:e.chantier||"",ref:e.ref||"",vref:e.vref||"",mandat:e.mandat||"",mention:$("factModMention")?$("factModMention").value.trim():"",tva:factModApercu().tva,rem:FACT_MOD.rem,taux:FACT_MOD.taux,lignes:t,etat:"brouillon",paye:"",regle:0,par:void 0!==ACC_USER&&ACC_USER?ACC_USER:"",cree:o,maj:o};FACT.factures.push(l),FACT.courant=l.id,factSauver(),factModFermer(),erpPage("factures",entPays(r)),factRender(),toast("Brouillon "+l.no+" créé — "+t.length+" position(s), "+factSomme(factCalc(l).ttc,l)+" TTC. Relis-le, puis clique « Comptabiliser » : c'est ce geste qui en fait une facture.")}let FACT_LIB=null;function flSuccDef(){if(!socGroupe()&&entPays(SOC_ACT)===FACT_PAYS)return SOC_ACT;if("TR"===FACT_PAYS){const e=socEntitesPays("TR");return e[0]?e[0].id:"TR"}const e=$("docSucc");if(e&&e.value&&entDef()[e.value])return e.value;const t=factVue().filter(e=>"CH"===entiteDe(e).pays),r=t[t.length-1];return r&&"VD"===entiteDe(r).id?"VD":"NE"}function flSuccOptions(e){const t=$("flSucc");if(!t)return;const r=e?[entiteDe(e)]:socEntitesPays();t.innerHTML=r.map(e=>'").join("")}function flSucc(){const e=String($("flSucc")&&$("flSucc").value||"").toUpperCase();return entDef()[e]?e:flSuccDef()}function flSuccMaj(){if(!FACT_LIB||FACT_LIB.id||FL_RO)return void flMaj();const e=entite(flSucc()),t=$("flTva");t&&(t.value=e.tvaPc);const r=$("flEch"),s=$("flDate")&&$("flDate").value||factAujourdhui();r&&(r.value=factJours(s,factPaiementDe(e.id).delai||30)),flMaj()}function flTauxDef(e){try{return grpRab(e)||0}catch(e){return 0}}function flGroupes(){return FACT_LIB?[...new Set(FACT_LIB.lignes.map(e=>e.grp||GRP_QUA))]:[]}let FL_RO=!1,FL_ONG="det";const FL_CHAMPS=["flDate","flEch","flClient","flChantier","flAdr","flRef","flVref","flMandat","flMention","flTva","flRem","flQ"];function flOuvrir(e){if(!flQuitter())return;let t=null;if(e&&(t=FACT.factures.find(t=>t.id===e),!t))return void toast("Facture introuvable.");FL_RO=!(!t||factBrouillon(t));const r=factAujourdhui();FACT_LIB={id:t?t.id:"",lignes:t?(t.lignes||[]).map(e=>({desc:e.desc||"",grp:e.grp||GRP_QUA,pu:+e.pu||0,q:+e.q||0})):[],taux:t?{...t.taux||{}}:{}},$("flTitre").textContent=t?"Modifier le brouillon "+t.no:"Nouvelle facture",$("flBt").textContent=t?"Enregistrer le brouillon":"Créer le brouillon",$("flNo").innerHTML=t?"N° "+offEsc(t.no)+" — il ne change plus":"N° attribué à la validation";const s=$("flSucc"),i=t?entiteDe(t).id:flSuccDef();flSuccOptions(t),s.value=i,s.disabled=!!t,$("flDate").value=t&&t.date||r,$("flEch").value=t&&t.echeance||factJours(r,factPaiementDe(i).delai||30),$("flClient").value=t&&t.client||"",$("flAdr").value=t&&t.adr||"",$("flChantier").value=t&&t.chantier||"",$("flRef").value=t&&t.ref||"",$("flVref").value=t&&t.vref||"",$("flMandat").value=t&&t.mandat||"",$("flMention").value=t&&t.mention||"",$("flTva").value=t&&void 0!==t.tva&&""!==t.tva?t.tva:entEstTR(i)?entite(i).tvaPc:$("txTva")&&$("txTva").value||8.1,$("flRem").value=t&&void 0!==t.rem&&""!==t.rem?t.rem:$("txRem")&&$("txRem").value||0,$("flQ").value="";const o=$("flCatRes");o&&(o.hidden=!0);const l=$("flRes");l&&(l.hidden=!0),FACT.courant=t?t.id:null,["flSep","flBar","flOng"].forEach(e=>{const t=$(e);t&&(t.hidden=!1)});const u=$("flAide");u&&(u.hidden=!0),flChampsMaj(),flGrille(),flEntete(),flOnglet(t||"mvt"!==FL_ONG&&"rap"!==FL_ONG?FL_ONG:"det"),factRender(),t||setTimeout(()=>{const e=$("flClient");e&&e.focus()},30)}function flFermer(){if(!flQuitter())return;FACT_LIB=null,FL_RO=!1,FACT.courant=null,["flSep","flBar","flOng","flPanDet","flPanLig","flPanPcs","flPanMvt","flPanRap"].forEach(e=>{const t=$(e);t&&(t.hidden=!0)});const e=$("flAide");e&&(e.hidden=!1),factRender()}function flQuitter(){if(!FACT_LIB||FL_RO)return!0;if(FACT_LIB.id)return flValider(!0);return!(FACT_LIB.lignes.some(e=>(e.desc||"").trim()||+e.pu||+e.q)||String(($("flClient")?$("flClient").value:"")||"").trim())||confirm("La facture en cours de saisie n'a pas encore de numéro.\n\nSi tu continues, ce que tu viens de taper est perdu.")}function flChampsMaj(){FL_CHAMPS.forEach(e=>{const t=$(e);t&&(t.disabled=FL_RO)});const e=$("flSucc");e&&(e.disabled=FL_RO||!(!FACT_LIB||!FACT_LIB.id));const t=$("flLibre");t&&(t.disabled=FL_RO);const r=$("flSrc");r&&(r.hidden=FL_RO);const s=$("flNoteBr");s&&(s.hidden=FL_RO);const i=$("flRo");i&&(i.hidden=!FL_RO,FL_RO&&(i.innerHTML="🔒 Facture comptabilisée : c'est une pièce comptable, elle se consulte et s'encaisse, elle ne se corrige plus. Pour la reprendre, il faut la décomptabiliser — réservé à l'administrateur, et refusé dès qu'un franc est encaissé."))}function flDevLabels(){const e=flDevNom();document.querySelectorAll(".fl-dev").forEach(t=>{t.textContent=e})}function flOnglet(e){const t={det:"flOngDet",lig:"flOngLig",pcs:"flOngPcs",mvt:"flOngMvt",rap:"flOngRap"},r={det:"flPanDet",lig:"flPanLig",pcs:"flPanPcs",mvt:"flPanMvt",rap:"flPanRap"};r[e]||(e="det"),FL_ONG=e,Object.keys(t).forEach(s=>{const i=$(t[s]),o=$(r[s]);i&&i.classList.toggle("on",s===e),o&&(o.hidden=s!==e||!FACT_LIB)}),flDevLabels(),"lig"===e&&flMaj(),"pcs"===e&&flResume(),"mvt"===e&&flMvtRender(),"rap"===e&&flRapRender()}function flEntete(){if(!FACT_LIB)return;const e=FACT_LIB.id?FACT.factures.find(e=>e.id===FACT_LIB.id):null,t=e?factEtat(e):"brouillon",r=$("flTitre");r&&(r.textContent=e?("brouillon"===t?"Brouillon ":"Facture ")+e.no:"Nouvelle facture");const s=$("flEt");s&&(s.hidden=!e,e&&(s.className="fact-et fact-et-"+t,s.textContent=FACT_ETAT_TXT[t]));const i=$("flNo");i&&(i.innerHTML=e?offEsc(e.client||"—")+(e.chantier?" — "+offEsc(e.chantier):"")+("brouillon"===t||"annulee"===t?"":" · solde "+factSomme(factSolde(e),e)+""):"N° attribué à la validation");const o=$("flBt");o&&(o.hidden=FL_RO,o.textContent=e?"Enregistrer le brouillon":"Créer le brouillon");const l=$("flAct");l&&(l.innerHTML=e?flActions(e,t):"")}function flActions(e,t){const r=offEsc(e.id);let s=``;return"brouillon"===t?s+=``:(s+=``,"payee"===t||"annulee"===t||factMvts(e).length||"function"==typeof estAdmin&&!estAdmin()||(s+=``)),s+=``,s}function flAgir(e,t){!FL_RO&&FACT_LIB&&FACT_LIB.id&&!flValider(!0)||("imp"===e?factImprimer(t):"compta"===e?factComptabiliser(t):"decompta"===e?factDecomptabiliser(t):"payer"===e?factPayer(t):"annuler"===e&&factAnnuler(t))}function flPanSync(){if(!FACT_LIB)return;if(!FACT_LIB.id)return void flEntete();const e=FACT.factures.find(e=>e.id===FACT_LIB.id);if(e){if(!factBrouillon(e)!==FL_RO)return FACT_LIB=null,void flOuvrir(e.id);flEntete(),"mvt"===FL_ONG&&flMvtRender()}}let FL_TIRE=null;function flSepPrendre(e){const t=$("factBox");t&&(FL_TIRE={y:e.clientY,h:t.getBoundingClientRect().height},e.preventDefault(),document.addEventListener("mousemove",flSepBouger),document.addEventListener("mouseup",flSepLacher))}function flSepBouger(e){const t=$("factBox");if(!FL_TIRE||!t)return;const r=Math.max(90,Math.min(window.innerHeight-200,FL_TIRE.h+e.clientY-FL_TIRE.y));t.style.maxHeight=r+"px"}function flSepLacher(){FL_TIRE=null,document.removeEventListener("mousemove",flSepBouger),document.removeEventListener("mouseup",flSepLacher)}function flEcheanceAuto(){const e=$("flEch");e&&(e.value=factJours($("flDate")?$("flDate").value:"",factPaiement().delai||30))}function flGrille(){const e=$("flBox");if(!e||!FACT_LIB)return;const t=[...new Set([...grpTous(),...FACT_LIB.lignes.map(e=>e.grp||GRP_QUA)])];FACT_LIB.lignes.length?e.innerHTML=FL_RO?`
Désignation
Groupe
\n
Qté
P.U. ${flDevNom()}
\n
Montant
`+FACT_LIB.lignes.map((e,t)=>`
\n
${offEsc(e.desc||"")}
\n
${offEsc(e.grp||GRP_QUA)}
\n
${fmt(+e.q||0)}
\n
${fmt(+e.pu||0)}
\n
—
\n
`).join("")+"
":`
Désignation
Groupe
\n
Qté
P.U. ${flDevNom()}
\n
Montant
`+FACT_LIB.lignes.map((e,r)=>`
\n
\n
\n
\n
\n
—
\n
\n
`).join("")+"
":e.innerHTML='
'+(FL_RO?"Cette facture n'a aucune ligne.":"Aucune ligne pour l'instant — clique « + Ligne libre » pour taper une prestation, ou cherche un article du catalogue juste au-dessus.")+"
${r.length} références correspondent — les 25 plus proches sont affichées.
`:""):'
Aucun article trouvé — tu peux toujours taper une ligne libre.
',e.hidden=!1}function flArtPrendre(e){const t=ART.a[e];if(!t||!FACT_LIB)return;flSync(),FACT_LIB.lignes.push({desc:t[0]+" — "+t[1],grp:ART.g[t[3]]||GRP_QUA,pu:r2(+t[2]||0),q:1});const r=$("flQ");r&&(r.value="");const s=$("flCatRes");s&&(s.hidden=!0),flGrille();const i=$("flq"+(FACT_LIB.lignes.length-1));i&&(i.focus(),i.select()),toast(t[0]+" ajouté — indique la quantité.")}function flValider(e){if(!FACT_LIB)return!1;if(FL_RO)return!0;flSync();const t=FACT_LIB.lignes.map(e=>({desc:String(e.desc||"").trim(),grp:e.grp||GRP_QUA,pu:r2(+e.pu||0),q:r2(+e.q||0)})).filter(e=>e.desc||e.pu||e.q);if(!t.length)return toast("Une facture sans ligne n'existe pas — ajoute au moins une prestation."),!1;if(t.some(e=>!e.desc))return toast("Une ligne n'a pas de désignation : le client doit pouvoir lire ce qu'il paie."),!1;const r=String(($("flClient")?$("flClient").value:"")||"").trim();if(!r)return toast("Indique le client — pris au registre, ou écrit à la main."),!1;const s=flDoc(),i=FACT_LIB.id?entiteDe(FACT.factures.find(e=>e.id===FACT_LIB.id)||{}).id:flSucc(),o=$("flDate")&&$("flDate").value||factAujourdhui(),l=$("flEch")&&$("flEch").value||factJours(o,factPaiementDe(i).delai||30),val=e=>String(($(e)?$(e).value:"")||"").trim(),u=(new Date).toISOString(),d={date:o,echeance:l,client:r,adr:val("flAdr"),chantier:val("flChantier"),ref:val("flRef"),vref:val("flVref"),mandat:val("flMandat"),mention:val("flMention"),tva:s.tva,rem:s.rem,taux:s.taux,lignes:t,maj:u};let m;if(FACT_LIB.id){if(m=FACT.factures.find(e=>e.id===FACT_LIB.id),!m)return toast("Ce brouillon n'existe plus."),FACT_LIB=null,flFermer(),!1;if(!factBrouillon(m))return toast("La facture "+m.no+" vient d'être comptabilisée — rien n'a été modifié."),FACT_LIB=null,flOuvrir(m.id),!1;Object.assign(m,d)}else{const e=i;m=Object.assign({id:factNouvelId(),no:factNumPrendreEnt(e),succ:e,ent:e,offreId:"",offreNo:"",etat:"brouillon",paye:"",regle:0,par:void 0!==ACC_USER&&ACC_USER?ACC_USER:"",cree:u},d),FACT.factures.push(m)}FACT.courant=m.id,factSauver();const h=factSomme(factCalc(m).ttc,m);return FACT_LIB=null,erpPage("factures",entPays(m.ent||m.succ)),flOuvrir(m.id),e||toast("Brouillon "+m.no+" enregistré — "+t.length+" ligne(s), "+h+" TTC. Relis-le, puis clique « Comptabiliser » : c'est ce geste qui en fait une facture."),!0}function flResume(){const e=$("flPcs");if(!e||!FACT_LIB)return;flSync();const t=flDoc(),r=factCalc(t);if(!t.lignes.length)return void(e.innerHTML='
Sous-total ${offEsc(grpAbr(e))}`+(d.rab?" — après rabais":"")+`
${fmt(o)}
`+`
${fmt(d.rab?d.ht2:u)}
`}),l+=`
Total ${t.lignes.length} ligne(s), ${i.length} désignation(s)
${fmt(r2(t.lignes.reduce((e,t)=>e+(+t.q||0),0)))}
${fmt(r.ht)}
`,l+="
",e.innerHTML=l}function flMvtRender(){const e=$("flMvt"),t=$("flMvNote"),r=$("flMvForm");if(!e)return;const s=FACT_LIB&&FACT_LIB.id?FACT.factures.find(e=>e.id===FACT_LIB.id):null;if(!s)return r&&(r.hidden=!0),t&&(t.textContent="Cette facture n'est pas encore enregistrée : il n'y a rien à encaisser. Enregistre le brouillon, comptabilise-le, et son registre s'ouvrira ici."),void(e.innerHTML='
Aucun mouvement.
');const i=factEtat(s),o="brouillon"!==i&&"annulee"!==i;r&&(r.hidden=!o),t&&(t.innerHTML="brouillon"===i?"Un brouillon n'est pas encore une vente : il ne pèse pas dans l'encours et ne s'encaisse pas. Comptabilise-le d'abord — c'est ce geste qui inscrit la créance.":"annulee"===i?"Facture annulée : elle est sortie des totaux. Les encaissements déjà reçus restent inscrits ci-dessous, car l'argent est bien tombé sur le compte — une annulation ne le fait pas disparaître.":"Total TTC "+factSomme(factCalc(s).ttc,s)+" · reçu "+factSomme(factRecu(s),s)+""+(factEscompteAccorde(s)?" · escompte accordé "+factSomme(factEscompteAccorde(s),s):"")+" · solde "+factSomme(factSolde(s),s)+""+(s.cloture?" · clôturée le "+offEsc(offDate(s.cloture)):"")+".");const l=factMvts(s);if(l.length){const t=r2(l.reduce((e,t)=>e+(+t.montant||0),0)),r=r2(l.reduce((e,t)=>e+(+t.escompte||0),0));e.innerHTML='
';const u=$("flMvDate");u&&!u.value&&(u.value=factAujourdhui());const d=$("flMvM");if(d&&o&&!d.value){const e=factSolde(s);e>0&&(d.value=e)}}function flEncaisser(){if(!FACT_LIB||!FACT_LIB.id)return;const num=e=>parseFloat(String(null==e?"":e).replace(",","."))||0,e=factEncaisser(FACT_LIB.id,{date:$("flMvDate")&&$("flMvDate").value||factAujourdhui(),montant:num($("flMvM")?$("flMvM").value:0),escompte:num($("flMvE")?$("flMvE").value:0),origine:"main",note:String(($("flMvN")?$("flMvN").value:"")||"").trim()});if(!e)return;factSauver(),["flMvM","flMvN"].forEach(e=>{const t=$(e);t&&(t.value="")});const t=$("flMvE");t&&(t.value=0);const r=FACT.factures.find(e=>e.id===FACT_LIB.id);flMvtRender(),flEntete(),factRender(),toast(factSomme(+e.montant||0,r)+" encaissés sur "+r.no+(factSolde(r)<=.005?" — solde à zéro, la facture est clôturée.":" — il reste "+factSomme(factSolde(r),r)+"."))}function flSolder(){if(!FACT_LIB||!FACT_LIB.id)return;const e=FACT.factures.find(e=>e.id===FACT_LIB.id);if(!e)return;const t=factSolde(e);if(t<=.005)return void toast("Cette facture est déjà soldée.");const r=$("flMvM");r&&(r.value=t);const s=$("flMvE");s&&!s.value&&(s.value=0),flEncaisser()}function flMvtOter(e){if(!FACT_LIB||!FACT_LIB.id)return;const t=FACT.factures.find(e=>e.id===FACT_LIB.id);if(!t)return;const r=factMvts(t).find(t=>t.id===e);r&&confirm("Retirer cet encaissement de "+factSomme(+r.montant||0,t)+" du "+offDate(r.date)+" ?\n\nLa facture "+t.no+" se rouvrira toute seule si son solde redevient positif.")&&factMvtSupprimer(FACT_LIB.id,e)&&(factSauver(),flMvtRender(),flEntete(),factRender(),toast("Mouvement retiré — solde de "+t.no+" : "+factSomme(factSolde(t),t)+"."))}const BANK_KEY="air-ventil.banque";var BANQUE={ver:1,imports:[]},BQ=null;async function bqCharger(){const e=await cfrLire(BANK_KEY);if(e)try{const t=JSON.parse(e);t&&Array.isArray(t.imports)&&(BANQUE.imports=t.imports)}catch(e){}}function bqSauver(){KDATA&&cfrEcrire(BANK_KEY,JSON.stringify(BANQUE))}function bqNom(e){return e.localName||String(e.nodeName||"").replace(/^.*:/,"")}function bqTxt(e){return e?String(e.textContent||"").trim():""}function bqEnf(e,t){const r=[];if(!e)return r;for(let s=e.firstElementChild;s;s=s.nextElementSibling)bqNom(s)===t&&r.push(s);return r}function bqDesc(e,t){const r=[];if(!e)return r;const s=e.getElementsByTagName("*");for(let e=0;e{const t=[];r.forEach(r=>bqEnf(r,e).forEach(e=>t.push(e))),r=t}),r.length?r[0]:null}function bqDate(e){const t=bqTxt(e),r=/^(\d{4}-\d{2}-\d{2})/.exec(t);return r?r[1]:""}function camtLire(e,t){const r={ok:!1,msg:"",type:"",compte:"",lignes:[],debits:0,attente:0,ecritures:0};let s=null;try{s=(new DOMParser).parseFromString(String(e||""),"text/xml")}catch(e){s=null}if(!s||!s.documentElement||s.getElementsByTagName("parsererror").length)return r.msg="Ce fichier n'est pas un XML lisible.",r;const i=s.documentElement,o=bqDesc(i,"BkToCstmrDbtCdtNtfctn")[0],l=bqDesc(i,"BkToCstmrStmt")[0],u=o||l;if(!u)return r.msg="Ce XML n'est ni un camt.054 (avis de crédit) ni un camt.053 (relevé de compte).",r;r.type=o?"camt.054":"camt.053";return bqEnf(u,o?"Ntfctn":"Stmt").forEach(e=>{r.compte||(r.compte=bqTxt(bqChem(e,"Acct/Id/IBAN"))),bqEnf(e,"Ntry").forEach(e=>{r.ecritures++;const s=bqTxt(bqEnf(e,"CdtDbtInd")[0])||"CRDT",i=(bqTxt(bqEnf(e,"Sts")[0])||"BOOK").replace(/\s+/g,"")||"BOOK",o=bqDate(bqChem(e,"ValDt/Dt"))||bqDate(bqChem(e,"ValDt/DtTm"))||bqDate(bqChem(e,"BookgDt/Dt"))||bqDate(bqChem(e,"BookgDt/DtTm")),l=bqTxt(bqEnf(e,"AddtlNtryInf")[0]),u=/^true$/i.test(bqTxt(bqEnf(e,"RvslInd")[0])),d=bqDesc(e,"TxDtls");(d.length?d:[e]).forEach(e=>{const d=bqEnf(e,"Amt")[0],m=parseFloat(String(bqTxt(d)).replace(",","."))||0,h=d&&d.getAttribute("Ccy")||"CHF",g=bqTxt(bqEnf(e,"CdtDbtInd")[0])||s,b=bqChem(e,"RltdPties/Dbtr"),A=bqTxt(bqChem(e,"RltdPties/Dbtr/Nm"))||(b?bqTxt(bqDesc(b,"Nm")[0]):"")||bqTxt(bqDesc(e,"Nm")[0]),C=bqDesc(e,"CdtrRefInf")[0];let R=C?bqTxt(bqDesc(C,"Ref")[0]):"",E=C?bqTxt(bqDesc(C,"Prtry")[0])||bqTxt(bqDesc(C,"Cd")[0]):"";if(!R){const t=bqDesc(e,"Ustrd").map(e=>bqTxt(e)).join(" "),r=/\b(\d{27})\b/.exec(t.replace(/\s+/g," ").replace(/(\d)\s(?=\d)/g,"$1")),s=/\b(RF\d{2}[0-9A-Za-z]{1,21})\b/.exec(t);r?(R=r[1],E=E||"QRR"):s&&(R=s[1].toUpperCase(),E=E||"SCOR")}if(!R){const t=bqTxt(bqChem(e,"Refs/EndToEndId"));/^\d{27}$/.test(t.replace(/\s+/g,""))&&(R=t.replace(/\s+/g,""),E=E||"QRR")}const S=bqDate(bqChem(e,"RltdDts/IntrBkSttlmDt"))||bqDate(bqChem(e,"RltdDts/AccptncDtTm"))||o;"CRDT"!==g||u?r.debits++:i&&i.toUpperCase().indexOf("BOOK")<0?r.attente++:m<=0||r.lignes.push({date:S||factAujourdhui(),montant:r2(m),ccy:String(h).toUpperCase(),payeur:A,ref:String(R||"").replace(/\s+/g,""),refTp:E,lib:l,fichier:String(t||"")})})})}),r.ok=!0,r.lignes.length||r.msg||(r.msg=r.ecritures?"Ce fichier ne contient aucun paiement reçu — que des écritures au débit ou en attente.":"Ce fichier ne contient aucune écriture."),r}function bqNoDeRef(e){const t=String(e||"").replace(/\s+/g,"").toUpperCase();if(/^\d{27}$/.test(t)){if(!qrRefQRRValide(t))return{no:"",cle:!1,typ:"QRR"};const e=String(factPaiement().ident||"").replace(/\D/g,"");let r=t.slice(0,26);return e&&0===r.indexOf(e)&&(r=r.slice(e.length)),{no:r.replace(/^0+/,""),cle:!0,typ:"QRR"}}if(/^RF\d{2}[0-9A-Z]{1,21}$/.test(t)&&qrRefSCORValide(t))return{no:t.slice(4).replace(/^0+/,""),cle:!0,typ:"SCOR"};return{no:t.replace(/\D/g,"").replace(/^0+/,""),cle:!1,typ:"?"}}function bqCandidats(e){if(!e)return[];return(void 0!==FACT&&FACT&&FACT.factures?FACT.factures:[]).filter(t=>!(!t||"annulee"===t.etat||factBrouillon(t))&&String(t.no||"").replace(/\D/g,"").replace(/^0+/,"")===e)}function bqDejaPose(e,t){return factMvts(e).some(e=>String(e.ref||"").replace(/\s+/g,"")===t.ref&&e.date===t.date&&Math.abs((+e.montant||0)-t.montant)<.005)}function bqQualifier(e){if(e.etat="ok",e.motif="",e.f=null,"CHF"!==e.ccy)return e.etat="refus",e.motif="devise "+e.ccy,e;const t=bqNoDeRef(e.ref);if(e.no=t.no,e.refTyp=t.typ,!e.ref)return e.etat="inconnue",e.motif="aucune référence de paiement",e;if("QRR"===t.typ&&!t.cle)return e.etat="inconnue",e.motif="référence QRR fausse",e;const r=bqCandidats(t.no);if(!r.length)return e.etat="inconnue",e.motif="aucune facture n° "+(t.no||"?"),e;if(r.length>1)return e.etat="ambigu",e.motif=r.map(e=>e.no).join(" ou "),e;const s=r[0];if("function"==typeof socOk&&!socOk(s))return e.etat="inconnue",e.motif="facture n° "+(t.no||"?")+" de "+entNom(entiteDe(s).id)+" — à encaisser depuis cette société",e;if(e.f=s,bqDejaPose(s,e))return e.etat="double",e.motif="déjà au registre de "+s.no,e;const i=factSolde(s);return i<=.005?(e.etat="soldee",e.motif=s.no+" est déjà soldée",e):(e.montant-i>.005?e.motif="dépasse le solde de "+fmt(r2(e.montant-i))+" CHF":i-e.montant>.005&&(e.motif="il restera "+fmt(r2(i-e.montant))+" CHF"),e)}async function bqDezip(e){const t=new DataView(e),r=new Uint8Array(e);let s=-1;for(let e=r.length-22;e>=0&&e>=r.length-66e3;e--)if(101010256===t.getUint32(e,!0)){s=e;break}if(s<0)throw new Error("archive illisible");const i=t.getUint16(s+10,!0),o=[];let l=t.getUint32(s+16,!0);for(let e=0;e3&&80===i[0]&&75===i[1]){let e;try{e=await bqDezip(s)}catch(e){return void toast("Cette archive ne s'ouvre pas ici — décompresse-la et dépose le .xml qu'elle contient.")}if(r=e.filter(e=>/\.xml$/i.test(e.nom)),!r.length)return void toast("Cette archive ne contient aucun fichier .xml.")}else r=[{nom:t,texte:new TextDecoder("utf-8").decode(i)}]}catch(e){return void toast("Fichier illisible.")}const s=[];let i="",o="",l=0,u=0,d="";r.forEach(e=>{const t=camtLire(e.texte,e.nom);t.ok?(i=i||t.type,o=o||t.compte,l+=t.debits,u+=t.attente,!t.lignes.length&&t.msg&&(d=d||t.msg),t.lignes.forEach(e=>s.push(e))):d=d||t.msg}),s.length||toast(d||"Aucun paiement trouvé dans ce fichier."),s.forEach(bqQualifier),s.sort((e,t)=>String(e.date).localeCompare(String(t.date))||(e.payeur||"").localeCompare(t.payeur||"")),BQ={nom:t,type:i,compte:o,lignes:s,debits:l,attente:u},bqRender(),window.scrollTo(0,0)}function bqVider(){BQ=null,bqRender()}function bqAImporter(){return BQ?BQ.lignes.filter(e=>"ok"===e.etat):[]}function bqRender(){const e=$("bqBox"),t=$("bqPied"),r=$("bqAide"),s=$("bqSrc"),i=$("bqVider"),o=$("bqZone");if(!e)return;if(bqJournal(),!BQ)return e.innerHTML="",e.hidden=!0,t&&(t.hidden=!0),r&&(r.hidden=!1),o&&(o.hidden=!1),s&&(s.textContent=""),void(i&&(i.hidden=!0));e.hidden=!1,r&&(r.hidden=!0),o&&(o.hidden=!0),i&&(i.hidden=!1),s&&(s.textContent=BQ.nom+(BQ.type?" · "+BQ.type:"")+(BQ.compte?" · compte "+qrRefJolie(BQ.compte):""));const l={ok:"à imputer",double:"déjà importé",inconnue:"facture inconnue",ambigu:"plusieurs factures",soldee:"déjà soldée",refus:"écarté"},u=r2(BQ.lignes.reduce((e,t)=>e+t.montant,0)),d=bqAImporter(),m=r2(d.reduce((e,t)=>e+t.montant,0));if(e.innerHTML='
\n
Date de valeur
Payeur
Référence
Montant
\n
Facture
Client
Solde avant
État
'+BQ.lignes.map(e=>{const t=e.f;return`
\n
${offEsc(offDate(e.date))}
\n
${offEsc(e.payeur||"—")}
\n
${offEsc(e.ref?qrRefJolie(e.ref):"—")}
\n
${fmt(e.montant)}
\n
${t?offEsc(t.no):"—"}
\n
${t?offEsc(t.client||""):""}
\n
${t?fmt(factSolde(t)):"—"}
\n
${l[e.etat]||e.etat}`+(e.motif?`
${offEsc(e.motif)}
`:"")+"
"}).join("")+`
${BQ.lignes.length} paiement(s) lu(s)
`+`
${fmt(u)}
`,t){t.hidden=!1;const e=$("bqResume");e&&(e.innerHTML=d.length?""+d.length+" encaissement(s) pour "+fmt(m)+" CHF sont prêts à être imputés. Les autres lignes resteront affichées sans rien changer."+(BQ.debits?" · "+BQ.debits+" ligne(s) au débit écartée(s)":"")+(BQ.attente?" · "+BQ.attente+" en attente de comptabilisation":""):"Aucune ligne à imputer dans ce fichier — regarde la colonne État pour savoir pourquoi.");const r=$("bqBt");r&&(r.disabled=!d.length,r.textContent=d.length?"Importer "+d.length+" encaissement(s)":"Rien à importer")}}function bqJournal(){const e=$("bqJour");if(!e)return;const t=(BANQUE.imports||[]).slice().reverse();t.length?e.innerHTML='
Le
Fichier
Paiements
\n
Total
Par
'+t.map(e=>`
${offEsc(offDate(e.date))}
${offEsc(e.fichier||"")}
\n
${e.n}
${fmt(+e.total||0)}
\n
${offEsc(e.par||"")}
`).join("")+"
":e.innerHTML='
Aucun fichier n\'a encore été importé.
'}function bqImporter(){const e=bqAImporter();if(!e.length)return;const t=r2(e.reduce((e,t)=>e+t.montant,0)),r=new Set(e.map(e=>e.f.id)).size;if(!confirm("Importer "+e.length+" encaissement(s) pour "+fmt(t)+" CHF ?\n\n"+r+" facture(s) seront créditées. Celles dont le solde tombe à zéro se clôtureront toutes seules.\n\nUn encaissement posé par erreur se retire ligne par ligne depuis l'onglet Mouvements monétaires de la facture."))return;let s=0,i=0;e.forEach(e=>{factEncaisser(e.f.id,{date:e.date,montant:e.montant,escompte:0,origine:"banque",ref:e.ref,payeur:e.payeur,note:"fichier "+(e.fichier||BQ.nom||"banque")})&&(s++,factSolde(e.f)<=.005&&i++)}),BANQUE.imports.push({id:"b"+Date.now().toString(36),date:factAujourdhui(),fichier:BQ.nom,n:s,total:t,par:void 0!==ACC_USER&&ACC_USER?ACC_USER:""}),factSauver(),bqSauver(),BQ.lignes.forEach(bqQualifier),bqRender(),factRender(),toast(s+" encaissement(s) importé(s) pour "+fmt(t)+" CHF"+(i?" — "+i+" facture(s) clôturée(s).":"."))}async function bqDemarrer(){await bqCharger(),bqRender()}var RAP_NIV={1:{t:"Premier rappel",c:"Rappel"},2:{t:"Deuxième rappel",c:"2e rappel"},3:{t:"Mise en demeure",c:"Mise en demeure"}};function rapNiv(e){return e=Math.max(1,Math.min(3,parseInt(e,10)||1))}function factRappels(e){return e?(Array.isArray(e.rappels)||(e.rappels=[]),e.rappels.slice().sort((e,t)=>String(e.date).localeCompare(String(t.date))||rapNiv(e.niveau)-rapNiv(t.niveau))):[]}function factRappelDernier(e){const t=factRappels(e);return t.length?t.reduce((e,t)=>rapNiv(t.niveau)>=rapNiv(e.niveau)?t:e,t[0]):null}function factRappelSuivant(e){const t=factRappelDernier(e);return t?Math.min(3,rapNiv(t.niveau)+1):1}function factRappelPuce(e){const t=factRappelDernier(e);if(!t||"annulee"===factEtat(e))return"";const r=rapNiv(t.niveau);return`${offEsc(RAP_NIV[r].c)}`}function factJoursRetard(e,t){if(!e||!e.echeance)return 0;const r=new Date(String(t||factAujourdhui())+"T00:00:00"),s=new Date(String(e.echeance)+"T00:00:00");return isNaN(r)||isNaN(s)?0:Math.max(0,Math.round((r-s)/864e5))}function factRappelAjouter(e,t){const r=FACT.factures.find(t=>t.id===e);if(!r)return null;if(factBrouillon(r))return toast("Un brouillon ne se rappelle pas : comptabilise-le d'abord."),null;if("annulee"===r.etat)return toast("Cette facture est annulée — elle ne se rappelle plus."),null;Array.isArray(r.rappels)||(r.rappels=[]);const s={id:"r"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),niveau:rapNiv(t.niveau),date:t.date||factAujourdhui(),frais:r2(+t.frais||0),note:String(t.note||"").trim(),par:void 0!==ACC_USER&&ACC_USER?ACC_USER:"",cree:(new Date).toISOString()};return r.rappels.push(s),r.maj=s.cree,factSauver(),s}function factRappelSupprimer(e,t){const r=FACT.factures.find(t=>t.id===e);if(!r||!Array.isArray(r.rappels))return;const s=r.rappels.find(e=>e.id===t);s&&confirm("Retirer le "+RAP_NIV[rapNiv(s.niveau)].t.toLowerCase()+" du "+offDate(s.date)+" ?\n\nLe courrier deja parti, lui, ne revient pas : ne retire cette ligne que si elle a ete inscrite par erreur.")&&(r.rappels=r.rappels.filter(e=>e.id!==t),r.maj=(new Date).toISOString(),factSauver(),flRapRender(),factRender(),toast("Rappel retiré du registre."))}function flRapRender(){const e=$("flRp"),t=$("flRpNote"),r=$("flRpForm");if(!e)return;const s=FACT_LIB&&FACT_LIB.id?FACT.factures.find(e=>e.id===FACT_LIB.id):null;if(!s)return r&&(r.hidden=!0),t&&(t.textContent="Cette facture n'est pas encore enregistrée : il n'y a rien à rappeler. Enregistre le brouillon, comptabilise-le, et son registre de rappels s'ouvrira ici."),void(e.innerHTML='
Aucun rappel.
');const i=factEtat(s),o="brouillon"!==i&&"annulee"!==i;r&&(r.hidden=!o);const l=factJoursRetard(s),u=factSolde(s);t&&(t.innerHTML="brouillon"===i?"Un brouillon n'est pas encore une facture : il ne se rappelle pas. Comptabilise-le d'abord.":"annulee"===i?"Facture annulée : elle est sortie des totaux et ne se rappelle plus. Les rappels déjà partis restent inscrits ci-dessous.":u<=.005?"Cette facture est soldée : plus rien à réclamer. Les rappels déjà partis restent inscrits ci-dessous, c'est l'histoire du dossier.":"Échéance "+offEsc(factDateJolie(s.echeance))+" · "+(l?''+l+" jour(s) de retard":"pas encore échue")+" · solde "+factSomme(u,s)+".");const d=$("flRpNiv"),m=$("flRpDate");d&&!d.dataset.tenu&&(d.value=String(factRappelSuivant(s))),m&&!m.value&&(m.value=factAujourdhui());const h=factRappels(s);if(!h.length)return void(e.innerHTML='
`}function flRapAjouter(){const e=FACT_LIB&&FACT_LIB.id?FACT.factures.find(e=>e.id===FACT_LIB.id):null;if(!e)return void toast("Enregistre d'abord la facture.");const t=rapNiv($("flRpNiv")?$("flRpNiv").value:1),r=$("flRpDate")&&$("flRpDate").value||factAujourdhui(),s=factRappelDernier(e);if(s&&String(r)Sauf erreur de notre part, la facture ci-dessous n'a pas encore été réglée à ce jour.\n Il s'agit probablement d'un oubli : nous vous prions de bien vouloir en effectuer le\n paiement à l'aide du bulletin de versement joint.\n
Si votre paiement s'est croisé avec ce courrier, nous vous remercions de ne pas en\n tenir compte.
":2===o?"
Malgré notre premier rappel, la facture ci-dessous demeure impayée. Nous vous prions\n de procéder à son règlement dans les dix jours, à l'aide du bulletin de versement\n joint.
\n
Passé ce délai, nous nous verrons contraints d'engager la procédure de recouvrement,\n dont les frais seraient à votre charge.
":"
Nos rappels sont restés sans effet. Par la présente, nous vous mettons formellement\n en demeure de régler le montant ci-dessous dans les dix jours dès réception de\n ce courrier.
\n
À défaut de paiement dans ce délai, le dossier sera transmis sans autre avis pour\n recouvrement, avec les frais et intérêts moratoires que cela entraîne.
";$("printDoc").innerHTML=`\n
\n ${entEnteteHtml(entiteDe(e).id)}\n
${entFiscHtml(entiteDe(e).id)}
\n
\n
${s(l)}
\n
\n
${s(g)}
\n
Date :
${s(factDateJolie(t.date))}
\n
Facture n° :
${s(e.no||"—")}
\n
Du :
${s(factDateJolie(e.date))}
\n ${e.ref?`
Notre réf. :
${s(e.ref)}
`:""}\n ${e.vref?`
Votre réf. :
${s(e.vref)}
`:""}
\n
\n ${e.chantier?`
Chantier : ${s(e.chantier)}
`:""}\n
${s(g)} — facture ${s(e.no||"")}
\n
${b}
\n
\n
Facture ${s(e.no||"")} du ${s(factDateJolie(e.date))}
\n
${pfD(r.ttc,h)}
\n ${m?`
Déjà reçu
− ${pfD(m,h)}
`:""}\n ${factEscompteAccorde(e)?`
Escompte accordé
\n
− ${pfD(factEscompteAccorde(e),h)}
`:""}\n
Échéance du ${s(factDateJolie(e.echeance))}${u?" — "+u+" jour(s) de retard":""}
\n
\n
Montant encore dû (${s(devNom(h))})
${pfD(d,h)}
\n
\n ${+t.frais?`
Des frais de rappel de\n ${s(factSomme(+t.frais,e))} pourront vous être facturés séparément.
`:""}\n
Nous vous remercions de votre règlement et vous adressons,\n Madame, Monsieur, nos salutations distinguées.
\n
${s(entiteDe(e).nom)}
\n ${factConditions(e)}\n ${factSectionQr(e,d)}`,document.title=g+" "+(e.no||"")+" — "+entiteDe(e).nom}function factRappelImprimer(e,t){const r=FACT.factures.find(t=>t.id===(e||FACT.courant));if(!r)return void toast("Choisis d'abord une facture.");const s=(r.rappels||[]).find(e=>e.id===t);s?(FACT.courant=r.id,PRINTMODE="rappel",construireRappel(r,s),factSolde(r)<=.005&&toast("Attention : cette facture est soldée — le rappel sort quand même, mais il n'y a plus rien à réclamer."),window.print()):toast("Ce rappel n'est plus au registre.")}var RAP_DELAI=10;function rapAge(e){const t=factRappelDernier(e);if(!t)return null;const r=new Date(factAujourdhui()+"T00:00:00"),s=new Date(String(t.date)+"T00:00:00");return isNaN(r)||isNaN(s)?null:Math.max(0,Math.round((r-s)/864e5))}function rapEnDemeure(e){const t=factRappelDernier(e);return!!(t&&rapNiv(t.niveau)>=3)}function rapAFaire(e,t){if(!e)return!1;const r=factEtat(e);if("brouillon"===r||"annulee"===r||"payee"===r)return!1;if(factSolde(e)<=.005)return!1;if(factJoursRetard(e)<(null==t?RAP_DELAI:t))return!1;if(rapEnDemeure(e))return!1;const s=rapAge(e);return null===s||s>=RAP_DELAI}function rapJmin(){const e=$("rapJ"),t=e?parseInt(e.value,10):RAP_DELAI;return isNaN(t)?RAP_DELAI:t}function rapListe(){const e=norm(($("rapQ")?$("rapQ").value:"").trim()),t=$("rapVue")?$("rapVue").value:"afaire",r=rapJmin();return factVue().slice().filter(e=>{const s=factEtat(e);return"afaire"===t?rapAFaire(e,r):"ouvertes"===t?"brouillon"!==s&&"annulee"!==s&&factSolde(e)>.005:"relancees"!==t||factRappels(e).length>0}).filter(t=>!e||norm([t.no,t.client,t.chantier,t.offreNo,t.succ].join(" ")).includes(e)).sort((e,t)=>factJoursRetard(t)-factJoursRetard(e)||factSolde(t)-factSolde(e)||String(e.no||"").localeCompare(String(t.no||"")))}function rapRender(){const e=$("rapBox");if(!e)return;const t=$("rapVue")?$("rapVue").value:"afaire",r=rapListe();r.length?e.innerHTML='
N° de facture
Client
Chantier
\n
Échéance
Retard
Solde
\n
Dernier rappel
À envoyer
'+r.map(e=>{const t=factEtat(e),r=factJoursRetard(e),s=factSolde(e),i=factRappelDernier(e),o=factRappelSuivant(e),l=rapEnDemeure(e),u=s<=.005||"annulee"===t||"brouillon"===t,d=i?offEsc(offDate(i.date))+` `+offEsc(RAP_NIV[rapNiv(i.niveau)].c)+"":'jamais',m=u?'plus rien à réclamer':l?'au recouvrement':offEsc(RAP_NIV[o].t);return`
\n
${offEsc(e.no||"—")}
\n
${offEsc(e.client||"")}
\n
${offEsc(e.chantier||"")}
\n
${offEsc(offDate(e.echeance))}
\n
${r?r+" j":"—"}
\n
${s>.005?factSomme(s,e):"—"}
\n
${d}
${m}
\n
\n \n \n
`}).join("")+"
":e.innerHTML='
'+("afaire"===t?"Rien à relancer : aucune facture ne dépasse le délai choisi, ou toutes celles qui le dépassent ont déjà été relancées il y a moins de "+RAP_DELAI+" jours.":"Aucune facture ne correspond à cette recherche.")+"
";const s=$("rapEtat");if(s){const e=rapJmin(),t=factVue().filter(t=>rapAFaire(t,e)).length,i=factVue().filter(e=>rapEnDemeure(e)&&factSolde(e)>.005&&"annulee"!==factEtat(e)).length,o={};factVue().filter(e=>"retard"===factEtat(e)).forEach(e=>{const t=factDevise(e);o[t]=r2((o[t]||0)+factSolde(e))});const l=Object.keys(o).map(e=>fmtDev(o[e],e)+" "+devNom(e)).join(" + ");s.innerHTML=`${r.length} ligne(s) affichée(s) · `+(t?`${t} facture(s) à relancer`:"rien à relancer")+` · en retard ${l||"0.00 CHF"}`+(i?` · ${i} dossier(s) en mise en demeure — le logiciel s'arrête là, la suite part au recouvrement`:"")}}function rapFacture(e){erpPage("factures");try{flOuvrir(e),flOnglet("rap")}catch(e){}}function rapRelancer(e,t){const r=FACT.factures.find(t=>t.id===e);if(!r)return void toast("Facture introuvable.");if(factSolde(r)<=.005)return void toast("Cette facture est soldée — il n'y a plus rien à réclamer.");const s=factRappelSuivant(r),i=$("rapF")&&+String($("rapF").value).replace(",",".")||0,o=factRappelAjouter(r.id,{niveau:s,date:factAujourdhui(),frais:i,note:""});if(o){rapRender(),rapRegistre();try{factRender()}catch(e){}if(void 0!==FACT_LIB&&FACT_LIB&&FACT_LIB.id===r.id)try{flRapRender()}catch(e){}t?factRappelImprimer(r.id,o.id):toast(RAP_NIV[s].t+" inscrit pour la facture "+(r.no||"")+" — le 🖨 du registre sortira la lettre quand tu voudras.")}}function rapTout(){const e=rapJmin(),t=factVue().filter(t=>rapAFaire(t,e));if(!t.length)return void toast("Rien à relancer avec le délai choisi.");const r={1:0,2:0,3:0};t.forEach(e=>r[factRappelSuivant(e)]++);const s=[1,2,3].filter(e=>r[e]).map(e=>r[e]+" × "+RAP_NIV[e].t.toLowerCase()).join(", ");if(!confirm("Inscrire un rappel pour les "+t.length+" facture(s) de la liste ?\n\n"+s+"\n\nLes lettres ne sortiront pas maintenant : elles s'impriment ensuite une par une, depuis le registre. Rien n'est envoyé au client par le logiciel."))return;let i=0;t.forEach(e=>{factRappelAjouter(e.id,{niveau:factRappelSuivant(e),date:factAujourdhui(),frais:$("rapF")&&+String($("rapF").value).replace(",",".")||0,note:""})&&i++}),rapRender(),rapRegistre();try{factRender()}catch(e){}toast(i+" rappel(s) inscrit(s) — sors les lettres depuis le registre ci-dessous.")}function rapRegistre(){const e=$("rapRegBox");if(!e)return;const t=[];if(factVue().forEach(e=>factRappels(e).forEach(r=>t.push({f:e,r:r}))),t.sort((e,t)=>String(t.r.date).localeCompare(String(e.r.date))||String(t.r.cree||"").localeCompare(String(e.r.cree||""))),!t.length)return void(e.innerHTML='
`}function rapRetirer(e,t){factRappelSupprimer(e,t),rapRender(),rapRegistre()}function rapCsv(){const e=[["Date du rappel","Degré","N° facture","Date facture","Échéance","Client","Chantier","Jours de retard","Frais","Note","Par","Solde de la facture aujourd'hui"]],t=[];factVue().forEach(e=>factRappels(e).forEach(r=>t.push({f:e,r:r}))),t.sort((e,t)=>String(t.r.date).localeCompare(String(e.r.date))),t.forEach(t=>e.push([t.r.date,RAP_NIV[rapNiv(t.r.niveau)].t,t.f.no||"",t.f.date||"",t.f.echeance||"",t.f.client||"",t.f.chantier||"",factJoursRetard(t.f,t.r.date),r2(+t.r.frais||0),t.r.note||"",t.r.par||"",factSolde(t.f)]));const r="\ufeff"+e.map(e=>e.map(e=>'"'+String(null==e?"":e).replace(/"/g,'""')+'"').join(";")).join("\r\n"),s=document.createElement("a");s.href=URL.createObjectURL(new Blob([r],{type:"text/csv;charset=utf-8"})),s.download="rappels-air-ventil.csv",s.click(),URL.revokeObjectURL(s.href),toast(t.length+" rappel(s) exporté(s).")}const PAY_CH={iban:"payIban",nom:"payNom",rue:"payRue",no:"payNo",npa:"payNpa",loc:"payLoc",pays:"payPays",tva:"payTva",pied:"payPied",delai:"payDelai",ident:"payIdent",escPc:"payEscPc",escJours:"payEscJours"};function payRemplir(){const e=factPaiement();Object.keys(PAY_CH).forEach(t=>{const r=$(PAY_CH[t]);r&&(r.value=void 0===e[t]?"":e[t])});const t=$("payScor");t&&(t.checked=!!e.refScor),payMaj()}function payLire(){const e={};return Object.keys(PAY_CH).forEach(t=>{const r=$(PAY_CH[t]);e[t]=r?String(r.value||"").trim():""}),e.delai=Math.max(0,parseInt(e.delai,10)||0),e.escPc=Math.max(0,Math.min(10,+String(e.escPc).replace(",",".")||0)),e.escJours=Math.max(0,parseInt(e.escJours,10)||0),e.ident=String(e.ident||"").replace(/\D/g,"").slice(0,10),e.pays=(e.pays||"CH").toUpperCase().slice(0,2),e.refScor=!(!$("payScor")||!$("payScor").checked),e}function payMaj(){const e=payLire(),t={...factPaiementDef(),...e},r=$("payType");r&&(qrIbanNet(t.iban)?qrIbanValide(t.iban)?qrEstQrIban(t.iban)?r.innerHTML="✔ QR-IBAN reconnu ("+offEsc(qrIbanJoli(t.iban))+"). Chaque facture portera une référence QRR à 27 chiffres — par exemple "+offEsc(qrRefJolie(qrRefQRR(factRefQRRBase(t.ident,"100001"))))+" pour la facture n° 100001.":r.innerHTML="✔ IBAN ordinaire reconnu ("+offEsc(qrIbanJoli(t.iban))+"). Référence "+(t.refScor?"SCOR (RF…).":"aucune — le numéro de facture est repris dans les informations supplémentaires."):r.innerHTML='⚠ Cet IBAN est faux — le contrôle modulo 97 ne tombe pas juste. Vérifie les chiffres.':r.innerHTML="Aucun compte saisi pour l'instant.");const s=$("payEtat");if(s){const e=qrControle({iban:t.iban,crNom:t.nom,crRue:t.rue,crNo:t.no,crNpa:t.npa,crLoc:t.loc,crPays:t.pays||"CH",montant:"",devise:"CHF",refType:qrEstQrIban(t.iban)?"QRR":t.refScor&&qrIbanValide(t.iban)?"SCOR":"NON",ref:qrEstQrIban(t.iban)?qrRefQRR(factRefQRRBase(t.ident,"1")):t.refScor&&qrIbanValide(t.iban)?qrRefSCOR("1"):""});s.innerHTML=e.length?'⚠ Les factures sortiront sans QR de paiement tant que ceci n\'est pas réglé : '+e.map(offEsc).join(" · ")+".":"✔ Tout est là. Le bulletin de versement s'imprimera au bas de chaque facture."}}function payApercu(){const e=$("payQr");if(!e)return;const t=payLire(),r={...factPaiementDef(),...t},s={iban:r.iban,crNom:r.nom,crRue:r.rue,crNo:r.no,crNpa:r.npa,crLoc:r.loc,crPays:r.pays||"CH",montant:1234.55,devise:"CHF",dbNom:"",dbRue:"",dbNo:"",dbNpa:"",dbLoc:"",dbPays:"CH",refType:qrEstQrIban(r.iban)?"QRR":r.refScor&&qrIbanValide(r.iban)?"SCOR":"NON",ref:qrEstQrIban(r.iban)?qrRefQRR(factRefQRRBase(r.ident,"100001")):r.refScor&&qrIbanValide(r.iban)?qrRefSCOR("100001"):"",message:"Exemple — facture de démonstration"},i=qrControle(s);i.length?e.innerHTML='
Aperçu pour un montant de 1 234.55 CHF. C\'est exactement ce qui sortira au bas de la facture : 46 mm de côté, croix suisse au centre, niveau de correction M — les trois exigences de la norme. Ce code est calculé sur ce poste, hors ligne.
'}function payEnregistrer(){if("function"==typeof estAdmin&&!estAdmin())return void toast("Seul un administrateur peut modifier les coordonnées de paiement.");const e=payLire();if(!qrIbanNet(e.iban)||qrIbanValide(e.iban)){STORE.paiement={...factPaiementDef(),...e},storeSave(),payMaj();try{factRender()}catch(e){}toast(factPaiementPret()?"Coordonnées enregistrées — les factures sortiront avec leur QR de paiement.":"Coordonnées enregistrées, mais incomplètes : le QR ne s'imprimera pas encore.")}else toast("IBAN refusé : le contrôle modulo 97 ne tombe pas juste. Rien n'a été enregistré.")}function payRoleAppliquer(){const e="function"!=typeof estAdmin||estAdmin(),t=$("cardPaiement");if(!t)return;t.querySelectorAll("input, button").forEach(t=>{"payBt"!==t.id&&"INPUT"!==t.tagName||(t.disabled=!e)});const r=$("payLock");r&&(r.hidden=e)}function payDemarrer(){payRemplir(),entDemarrer(),payRoleAppliquer()}var ENT_SEL="NE";function entSections(e){const t={s:"La société",r:[{g:"2fr 1fr 1fr",f:[{c:"nom",l:"Raison sociale",t:"text"},{c:"site",l:"Site",t:"text"},{c:"court",l:"Nom court (listes et filtres)",t:"text"}]}]},r={s:"En-tête imprimé",r:[{g:"1fr",f:[{c:"logo",l:"Logo de cette société (PNG ou JPEG, en haut à gauche des documents)",t:"logo"}]}],n:"TR"===e.pays?"Tant qu'aucun logo n'est choisi, les documents de cette société sortent avec son nom et son adresse en toutes lettres — jamais avec l'en-tête d'Air-Ventil SA. À droite s'impriment le vergi dairesi, le VKN, le MERSİS et le sicil.":"Sans logo propre, cette société imprime l'en-tête Air-Ventil SA d'origine. À droite s'impriment le compte et le numéro de TVA saisis plus bas."};return"TR"===e.pays?[t,r,{s:"Adresse et mentions légales (Türkiye)",r:[{g:"2fr .5fr .7fr 1.4fr",f:[{c:"rue",l:"Adres (cadde / sokak)",t:"text"},{c:"no",l:"N°",t:"text"},{c:"npa",l:"Posta kodu",t:"text"},{c:"loc",l:"İlçe / İl",t:"text"}]},{g:"1fr 1fr 1fr",f:[{c:"vd",l:"Vergi dairesi (bureau des impôts)",t:"text"},{c:"vkn",l:"VKN — n° fiscal (10 chiffres)",t:"text"},{c:"mersis",l:"MERSİS",t:"text"}]},{g:"1fr 1.6fr",f:[{c:"sicil",l:"Ticaret sicil no",t:"text"},{c:"iban",l:"IBAN (TR…)",t:"text",ph:"TR.. .... .... .... .... .... .."}]}],n:"Le VKN et le vergi dairesi figurent obligatoirement sur toute facture turque, et servent d'identifiant de l'émetteur dans le fichier e-Fatura. Tant qu'ils manquent, l'application le dit ici plutôt que de sortir une pièce fausse."},{s:"Facturation",r:[{g:".9fr .9fr .7fr .8fr",f:[{c:"prefixe",l:"Préfixe des numéros (3 lettres)",t:"text",ph:"HKT"},{c:"serieDep",l:"Premier numéro de la série",t:"num",pas:1},{c:"tvaPc",l:"KDV par défaut (%)",t:"num",pas:1},{c:"delai",l:"Délai de paiement (jours)",t:"num",pas:1}]},{g:".8fr 1fr 1.4fr",f:[{c:"offDep",l:"Premier n° d'offre (teklif)",t:"num",pas:1},{c:"profil",l:"Profil e-Fatura",t:"liste",o:["TEMEL","TICARI"]},{c:"eArsiv",l:"Client non inscrit au système : émettre en e-Arşiv",t:"case"}]},{g:"1fr",f:[{c:"pied",l:"Mention imprimée au bas de la facture",t:"text"}]}],n:"Le numéro turc fait 16 caractères : 3 lettres, l'année sur 4 chiffres, puis 9 chiffres de séquence, sans jamais de trou. Le profil TEMEL n'attend pas de réponse du client, TİCARİ lui laisse 8 jours pour accepter ou refuser."}]:[t,r,{s:"Adresse (structurée, comme l'exige la norme QR)",r:[{g:"2fr .5fr .7fr 1.4fr .5fr",f:[{c:"rue",l:"Rue",t:"text"},{c:"no",l:"N°",t:"text"},{c:"npa",l:"NPA",t:"text"},{c:"loc",l:"Localité",t:"text"},{c:"ipays",l:"Pays",t:"text"}]}]},{s:"Facturation",r:[{g:"1.6fr .8fr 1.2fr",f:[{c:"iban",l:"IBAN ou QR-IBAN de la société",t:"text",ph:"CH.. .... .... .... .... ."},{c:"ident",l:"N° d'identification (préfixe QRR)",t:"text",ph:"ex. 102785"},{c:"cpte",l:"Compte imprimé dans l'en-tête",t:"text",ph:"ex. BCV S52 13 60 42"}]},{g:"1.2fr .6fr .6fr .6fr .6fr",f:[{c:"tva",l:"N° de TVA",t:"text",ph:"ex. CHE-114 816 586"},{c:"tvaPc",l:"TVA (%)",t:"num",pas:.1},{c:"delai",l:"Délai (jours)",t:"num",pas:1},{c:"escPc",l:"Escompte (%)",t:"num",pas:.05},{c:"escJours",l:"Escompte (jours)",t:"num",pas:1}]},{g:"1.4fr .8fr .8fr",f:[{c:"refScor",l:"Référence structurée SCOR (RF…) sur un IBAN ordinaire",t:"case"},{c:"serieDep",l:"Premier n° de facture",t:"num",pas:1},{c:"offDep",l:"Premier n° d'offre",t:"num",pas:1}]},{g:"1fr",f:[{c:"pied",l:"Mention imprimée au bas de la facture",t:"text"}]}],n:"Tant qu'une société n'a pas son propre IBAN valable, ses factures sortent avec les coordonnées de paiement historiques — celles de la carte ci-dessous. C'est tout ou rien : jamais l'adresse d'un atelier avec le compte de l'autre."}]}function entLogoCharger(e){const t=e&&e.files&&e.files[0];if(!t)return;if(0!==String(t.type||"").indexOf("image/"))return toast("Ce fichier n'est pas une image."),void(e.value="");if(t.size>6e5)return toast("Image trop lourde ("+Math.round(t.size/1024)+" ko). Un logo de 300 ko suffit largement : réduis-la, puis reprends."),void(e.value="");const r=new FileReader;r.onload=()=>{const e=String(r.result||""),t=$("entLogoV"),s=$("entLogoApercu");t&&(t.value=e),s&&(s.innerHTML=''),entMaj(),toast("Logo chargé — il reste à enregistrer la société pour le conserver.")},r.readAsDataURL(t)}function entLogoRetirer(){const e=$("entLogoV"),t=$("entLogoApercu"),r=$("entLogoF");e&&(e.value=""),r&&(r.value=""),t&&(t.innerHTML="Aucun logo propre."),entMaj()}function entChampHtml(e,t){const r='data-ec="'+e.c+'" data-et="'+e.t+'"';if("logo"===e.t){const r=String(null==t?"":t).trim();return"
'+(r?'':"Aucun logo propre.")+'
'}return"case"===e.t?'":"liste"===e.t?"":"num"===e.t?"':"'}function entVue(){let e=!1;try{e=socGroupe()}catch(t){e=!0}if(e)return entIds();const t=entIds().filter(e=>e===SOC_ACT);return t.length?t:entIds().slice(0,1)}function entOuvrir(e){const t=String(e).toUpperCase();entDef()[t]&&(entVue().indexOf(t)<0?toast("Cette société se règle depuis sa propre session."):(ENT_SEL=t,entRender()))}function entRender(){const e=$("entBox");if(!e)return;const t=entites(),r=entVue();(!t[ENT_SEL]||r.indexOf(ENT_SEL)<0)&&(ENT_SEL=r[0]||"NE");const s=$("entOng");s&&(s.innerHTML=r.map(e=>"").join(""));const i=$("entPortee");i&&(i.innerHTML=r.length>1?"Vue d'ensemble — les "+r.length+" fiches du groupe sont modifiables ici.":"Cette session règle "+offEsc(entNom(ENT_SEL))+". Les autres sociétés du groupe gardent leurs propres réglages : elles se modifient depuis leur session.");const o=t[ENT_SEL];e.innerHTML=entSections(o).map(e=>'
'+e.s+"
"+e.r.map((e,t)=>'
'+e.f.map(e=>entChampHtml(e,o[e.c])).join("")+"
").join("")+(e.n?'
'+e.n+"
":"")+"
").join(""),entRoleAppliquer(),entMaj()}function entLire(){const e={},t=$("entBox");return t?(t.querySelectorAll("[data-ec]").forEach(t=>{const r=t.dataset.ec,s=t.dataset.et;e[r]="case"===s?!!t.checked:"num"===s?Math.max(0,+String(t.value||"").replace(",",".")||0):String(t.value||"").trim()}),void 0!==e.prefixe&&(e.prefixe=e.prefixe.toUpperCase().slice(0,3)),void 0!==e.vkn&&(e.vkn=e.vkn.replace(/\D/g,"").slice(0,10)),void 0!==e.ident&&(e.ident=e.ident.replace(/\D/g,"").slice(0,10)),void 0!==e.ipays&&(e.ipays=(e.ipays||"CH").toUpperCase().slice(0,2)),void 0!==e.serieDep&&(e.serieDep=Math.max(1,Math.round(e.serieDep)||1)),void 0!==e.offDep&&(e.offDep=Math.max(1,Math.round(e.offDep)||1)),e):e}function entNumExemple(e){if("TR"===e.pays){const t=String(e.prefixe||"").toUpperCase().padEnd(3,"X").slice(0,3),r=String(Math.max(1,Math.round(e.serieDep)||1));return t+(new Date).getFullYear()+("000000000"+r).slice(-9)}return"F"+(e.id||ENT_SEL)+Math.max(1,Math.round(e.serieDep)||100001)}function entOffExemple(e){return(e.id||ENT_SEL)+Math.max(14e4,Math.round(e.offDep)||0)}function entMaj(){const e=$("entEtat");if(!e)return;const t={...entite(ENT_SEL),...entLire()},r=entControle(t),s="Ses factures porteront des numéros du type "+offEsc(entNumExemple(t))+", en "+offEsc(t.devise||"CHF")+", et ses offres du type "+offEsc(entOffExemple(t))+".";e.innerHTML=r.length?'⚠ Fiche incomplète — il manque encore '+r.map(offEsc).join(", ")+". On peut préparer des brouillons, mais pas sortir de facture définitive. "+s:"✔ Fiche complète. "+s}function entValider(){if("function"==typeof estAdmin&&!estAdmin())return void toast("Seul un administrateur peut modifier les sociétés du groupe.");const e=entLire();if("TR"==={...entite(ENT_SEL),...e}.pays||!qrIbanNet(e.iban||"")||qrIbanValide(e.iban)){entEnregistrer(ENT_SEL,e);try{payRemplir()}catch(e){}try{factRender()}catch(e){}entRender(),toast(entControle(ENT_SEL).length?"Société enregistrée — sa fiche reste incomplète.":"Société enregistrée.")}else toast("IBAN refusé : le contrôle modulo 97 ne tombe pas juste. Rien n'a été enregistré.")}function entRoleAppliquer(){const e="function"!=typeof estAdmin||estAdmin(),t=$("cardEntites");if(!t)return;t.querySelectorAll("#entBox input, #entBox select, #entBox button, #entBt").forEach(t=>{t.disabled=!e});const r=$("entBt");r&&(r.disabled=!e);const s=$("entLock");s&&(s.hidden=e)}function entDemarrer(){entRender()}function rondMaj(){const e=$("rondEtat");if(!e)return;let t=null;try{t=rcatIdx()}catch(e){t=null}if(!t||!t.sr)return void(e.textContent="—");const c=e=>Object.keys(e||{}).length,r=c(t.sr)+c(t.srl)+c(t.b)+c(t.bse)+c(t.bselm)+c(t.tc)+c(t.tr)+c(t.rc)+c(t.rcl)+c(t.re)+c(t.rel);e.innerHTML="Le chiffrage des pièces rondes retrouve "+r+" références galva du catalogue vivant, sans passer par le tarif gelé du dessin : tuyau spiralé SR "+c(t.sr)+" · tube lisse SRL "+c(t.srl)+" · coudes B "+c(t.b)+", BSE "+c(t.bse)+", BSELM "+c(t.bselm)+" · tés TC "+c(t.tc)+", T Roulé "+c(t.tr)+" · réductions RC "+c(t.rc)+", RCL "+c(t.rcl)+", RE "+c(t.re)+", REL "+c(t.rel)+"."}function rondEnregistrer(){if("function"==typeof estAdmin&&!estAdmin())return void toast("Seul un administrateur change la façon de commander les pièces rondes.");const e=$("rondLisse"),t=$("rondExc");STORE.rond={lisse:!!e&&!!e.checked,exc:!!t&&!!t.checked},storeSave(),rondMaj(),toast("Enregistré — les pièces rondes chiffrées à partir de maintenant suivent ce choix.")}function rondRoleAppliquer(){const e="function"!=typeof estAdmin||estAdmin();["rondLisse","rondExc","rondBt"].forEach(t=>{const r=$(t);r&&(r.disabled=!e)})}function rondDemarrer(){const e=rondReglage(),t=$("rondLisse"),r=$("rondExc");t&&(t.checked=!!e.lisse),r&&(r.checked=!!e.exc),rondRoleAppliquer(),rondMaj()}function factMontantJoli(e){const t=qrMontant(e);if(!t)return"";const[r,s]=t.split(".");return r.replace(/\B(?=(\d{3})+(?!\d))/g," ")+"."+s}function factDateJolie(e){const t=new Date(String(e||"")+"T12:00:00");if(isNaN(t.getTime()))return"";const z=e=>("0"+e).slice(-2);return z(t.getDate())+"."+z(t.getMonth()+1)+"."+t.getFullYear()}function factCadre(e,t){return''}function factSectionQr(e,t){if(factBrouillon(e))return"";const r=factQrDonnees(e);if(null!=t&&r2(+t)>0&&(r.montant=r2(+t)),qrControle(r).length)return"";const s=offEsc,bloc=(e,t,r,i,o)=>{const l=[t,r].filter(e=>String(e||"").trim()).join(" ");return[s(e),l?s(l):"",s((i+" "+o).trim())].filter(Boolean).join(" ")},i=bloc(r.crNom,r.crRue,r.crNo,r.crNpa,r.crLoc),o=!!String(r.dbNom||"").trim(),l=o?bloc(r.dbNom,r.dbRue,r.dbNo,r.dbNpa,r.dbLoc):"",u="NON"===r.refType?"":qrRefJolie(r.ref),d=factMontantJoli(r.montant),m=qrSvgSuisse(qrTexte(r),46),h=qrIbanJoli(r.iban);return`
\n\n
\n
Récépissé
\n
Compte / Payable à
\n
${s(h)} ${i}
\n ${u?`
Référence
${s(u)}
`:""}\n
Payable par${o?"":" (nom/adresse)"}
\n ${o?`
${l}
`:factCadre(52,20)}\n
\n
Monnaie
${s(r.devise)}
\n
Montant
${s(d)}
\n
\n
Point de dépôt
\n
\n\n
\n
\n
Section paiement
\n ${m}\n
\n
Monnaie
${s(r.devise)}
\n
Montant
${s(d)}
\n
\n
\n
\n
Compte / Payable à
\n
${s(h)} ${i}
\n ${u?`
Référence
${s(u)}
`:""}\n ${r.message?`
Informations supplémentaires
${s(qrCoupe(qrNettoyer(r.message),140))}
`:""}\n
Payable par${o?"":" (nom/adresse)"}
\n ${o?`
${l}
`:factCadre(65,25)}\n
\n
\n\n
`}function factEscompte(e){const t=factPaiementDe(e),r=+t.escPc||0,s=Math.max(0,parseInt(t.escJours,10)||0);if(r<=0||s<=0)return null;const i=factDu(e),o=Math.round(i*(1-r/100)*20)/20;return{pc:r,jours:s,date:factJours(e.date,s),montant:r2(o),remise:r2(i-o)}}function factConditions(e){const t=factPaiementDe(e),r=offEsc,s=[],i=factEscompte(e);s.push("Payable jusqu'au "+r(factDateJolie(e.echeance))+""+(i?", net.":", net et sans escompte.")),i&&s.push("Escompte "+r(fmt(i.pc,i.pc===Math.round(i.pc)?0:2))+" % pour un paiement sous "+i.jours+" jours, soit jusqu'au "+r(factDateJolie(i.date))+" : "+r(factSomme(i.montant,e))+".");const o=factRefPaiement(e);return"NON"!==o.refType&&s.push("Merci d'indiquer la référence "+r(qrRefJolie(o.ref))+" lors du paiement."),qrIbanNet(t.iban)&&s.push("IBAN "+r(qrIbanJoli(t.iban))+"."),e.mention&&s.push(r(e.mention)+"."),t.pied&&s.push(r(t.pied)),'
Section de paiement QR non imprimée :\n ${r(qrControle(factQrDonnees(e)).join(" · "))}. À compléter dans Configuration →\n Coordonnées de paiement, puis réimprimer.
`,g=m?"
BROUILLON — document de travail.\n Ce n'est pas une facture : elle n'a pas encore été comptabilisée, sa date n'est pas\n définitive, et elle sort sans bulletin de versement. À ne pas envoyer au client.
":"";$("printDoc").innerHTML=`\n
\n ${entEnteteHtml(entiteDe(e).id)}\n
${entFiscHtml(entiteDe(e).id)}
\n
\n
${r(u)}
\n
\n
${m?"BROUILLON DE FACTURE":"FACTURE"} N° : ${r(e.no||"—")}
\n ${g}\n ${factConditions(e)}\n ${h}\n ${d}`,document.title=(e.no?e.no+" ":"Facture — ")+entiteDe(e).nom}function factImprimer(e){const t=FACT.factures.find(t=>t.id===(e||FACT.courant));t?(FACT.courant=t.id,PRINTMODE="facture",construireFacture(t),factBrouillon(t)?toast("Brouillon imprimé : barré BROUILLON et sans bulletin de versement. Comptabilise-le pour sortir la vraie facture."):factSectionQr(t)||toast("Facture imprimée SANS le QR de paiement — complète Configuration → Coordonnées de paiement."),window.print()):toast("Choisis d'abord une facture à imprimer (bouton 🖨 de la ligne).")}function factActions(e,t){const r=offEsc(e.id);if("brouillon"===t)return``;let s=``;return"payee"===t||"annulee"===t||factMvts(e).length||"function"==typeof estAdmin&&!estAdmin()||(s+=``),s+=``,s}function factRender(){const e=$("factBox");if(!e)return;const t=norm(($("factQ")?$("factQ").value:"").trim()),r=$("factFiltre")?$("factFiltre").value:"",factNoTri=e=>{const t=/^([A-Za-z]*)[ -]?(\d+)/.exec(String(e.no||"").trim());return t?[t[1].toUpperCase(),+t[2]]:["",-1]},s=$("factSoc")?$("factSoc").value:"",i="TR"===FACT_PAYS,o=i?"TRY":"CHF",l=factVue().filter(e=>factDansLaVue(e)),u=l.slice().sort((e,t)=>{const r=factNoTri(e),s=factNoTri(t);return s[1]-r[1]||r[0].localeCompare(s[0])||String(t.cree||t.maj||"").localeCompare(String(e.cree||e.maj||""))}).filter(e=>!s||entiteDe(e).id===s).filter(e=>!r||factEtat(e)===r).filter(e=>!t||norm([e.no,e.client,e.chantier,e.offreNo,e.succ,entNom(entiteDe(e).id)].join(" ")).includes(t)),d=factEntitesPays().length>1;u.length?e.innerHTML=`
'+(l.length?"Aucune facture ne correspond à cette recherche.":i?"Aucune facture turque pour l'instant — clique « Nouvelle facture turque », ou pars d'une offre avec « Facturer une offre » en choisissant HK Teknik A.Ş.":"Aucune facture pour l'instant — clique « Nouvelle facture » pour en saisir une librement, ou pars d'une offre avec « Facturer une offre ».")+"
";const m=$("factEtat");if(m){const e=Math.round(JSON.stringify(FACT).length/1024),t=factVue().filter(e=>factDansLaVue(e)),r=t.filter(e=>"ouverte"===factEtat(e)).length,s=t.filter(e=>"retard"===factEtat(e)).length,i=t.filter(e=>"brouillon"===factEtat(e)).length;m.innerHTML=FACT_OK?FACT_PLEIN?`⚠ La sauvegarde du navigateur est pleine (${e} Ko). Exporte tes factures avant d'en créer d'autres.`:`${t.length} facture(s) ${factPaysNom()} · ${r} ouverte(s)`+(i?` · ${i} brouillon(s)`:"")+(s?` · ${s} en retard`:"")+` · encours ${factSommePays(factEncoursDe())}`+` · encaissé ${factSommePays(factEncaisseDe())}`+(factVue().length>t.length?` · ${factVue().length-t.length} pièce(s) dans l'autre carnet`:"")+(!socGroupe()&&FACT.factures.length>factVue().length?` · ${FACT.factures.length-factVue().length} chez les autres sociétés`:"")+` · ${e} Ko`:"⚠ Ce navigateur refuse d'enregistrer les factures pour un fichier local. Utilise Exporter avant de fermer."}const h=$("factAvert");if(h){const e=qrControle(factQrDonnees(null));h.hidden=!e.length,e.length&&(h.innerHTML="⚠ Les factures sortiront sans QR de paiement tant que ceci n'est pas réglé dans Configuration → Coordonnées de paiement : "+e.map(offEsc).join(" · ")+".")}flPanSync()}function factExporter(){const e=new Blob([JSON.stringify(FACT,null,1)],{type:"application/json"}),t=document.createElement("a");t.href=URL.createObjectURL(e);const r=new Date,z=e=>("0"+e).slice(-2);t.download=`factures-air-ventil-${r.getFullYear()}${z(r.getMonth()+1)}${z(r.getDate())}.json`,t.click(),URL.revokeObjectURL(t.href),toast(FACT.factures.length+" facture(s) exportée(s) — garde ce fichier hors du PC.")}function factImporter(e){const t=e.files&&e.files[0];if(!t)return;const r=new FileReader;r.onload=()=>{let t;try{t=JSON.parse(r.result)}catch(t){return toast("Fichier illisible — ce n'est pas une sauvegarde de factures."),void(e.value="")}if(!t||!Array.isArray(t.factures))return toast("Ce fichier ne contient pas de factures."),void(e.value="");const s=new Set(FACT.factures.map(e=>e.id));let i=0;t.factures.forEach(e=>{e&&e.id&&!s.has(e.id)&&(FACT.factures.push(e),s.add(e.id),i++)}),["NE","VD"].forEach(e=>{const r=(t.compteurs||{})[e];r&&r>(FACT.compteurs[e]||0)&&(FACT.compteurs[e]=r)}),factSauver(),factRender(),toast(i+" facture(s) reprise(s) — les doublons ont été ignorés."),e.value=""},r.readAsText(t)}const CLS_FICHIER="classeur-air-ventil.json",CLS_V=1,CLS_SOUS=["mvts","rappels"],CLS_ADMIN=["reglages","articles","paiement","entites"];function clsNo(e){return String(e&&e.no||"").trim().toUpperCase().replace(/\s+/g,"")}function clsSign(e){if(e&&e.id)return"#"+e.id;try{return JSON.stringify(e)}catch(t){return String(e)}}function clsMot(e){return{reglages:"les tarifs",articles:"le catalogue maison",paiement:"les coordonnées de paiement",entites:"les fiches des sociétés"}[e]||e}function clsDiffere(e,t){const r=entDuNumero(e&&e.no||t&&t.no),s=["maj"].concat(CLS_SOUS,r?["ent","succ"]:[]),net=e=>{const t={};Object.keys(e||{}).forEach(r=>{s.indexOf(r)<0&&(t[r]=e[r])});try{return JSON.stringify(t,Object.keys(t).sort())}catch(e){return""}};return net(e)!==net(t)}function clsFondre(e,t,r){const s=Array.isArray(t[r])?t[r]:null;if(!s||!s.length)return 0;const i=Array.isArray(e[r])?e[r]:[],o=new Set(i.map(clsSign));let l=0;return s.forEach(e=>{const t=clsSign(e);o.has(t)||(i.push(e),o.add(t),l++)}),e[r]=i,l}function clsCompteurs(e,t){let r=0;return Object.keys(t||{}).forEach(s=>{const i=+t[s];i&&i>(+e[s]||0)&&(e[s]=i,r++)}),r}function clsFusionRegistre(e,t,r,s){const i={ajoutes:0,majs:0,fondus:0,doubles:0};if(!Array.isArray(e)||!Array.isArray(t))return i;const o=new Map,l=new Map;return e.forEach(e=>{if(!e||!e.id)return;o.set(e.id,e);const t=clsNo(e);t&&!l.has(t)&&l.set(t,e)}),t.forEach(t=>{if(!t||!t.id)return;const u=o.get(t.id);if(!u){const u=clsNo(t);return u&&l.has(u)&&(i.doubles++,r.push({quoi:s,no:u,cause:"Deux documents différents portent ce numéro : celui d'ici et celui du classeur. Les deux sont conservés ; il faut en renuméroter un à la main."})),e.push(t),o.set(t.id,t),u&&!l.has(u)&&l.set(u,t),void i.ajoutes++}CLS_SOUS.forEach(e=>{i.fondus+=clsFondre(u,t,e)});const d=String(u.maj||""),m=String(t.maj||"");if(m&&m>d){const r={};CLS_SOUS.forEach(e=>{Array.isArray(u[e])&&(r[e]=u[e])});const s=Object.assign({},t);Object.keys(r).forEach(e=>{s[e]=r[e]}),e[e.indexOf(u)]=s,o.set(t.id,s),i.majs++}else d===m&&clsDiffere(u,t)&&r.push({quoi:s,no:clsNo(u)||clsNo(t),cause:"Les deux versions portent le même horodatage et ne disent pas la même chose. C'est celle d'ici qui a été gardée."})}),i}function clsStoreReprise(e,t){if(!e||"object"!=typeof e)return{ecartes:[]};const r=estAdmin(),s={},i=[];Object.keys(e).forEach(t=>{!r&&CLS_ADMIN.indexOf(t)>=0?e[t]&&Object.keys(e[t]).length&&i.push(t):s[t]=e[t]});try{storeFusion(s)}catch(e){}return i.length&&t.push({quoi:"réglages",no:"",cause:"Repris sans "+i.map(clsMot).join(", ")+" : seul un administrateur peut rapatrier cela."}),{ecartes:i}}function clsBanque(e){if(!e||!Array.isArray(e.imports))return 0;BANQUE.imports=BANQUE.imports||[];const t=BANQUE.imports,r=new Set(t.map(clsSign));let s=0;return e.imports.forEach(e=>{const i=clsSign(e);r.has(i)||(t.push(e),r.add(i),s++)}),s}function clsPaquet(){return{v:1,poste:cfrPoste(),ecrit:(new Date).toISOString(),off:{compteurs:OFF&&OFF.compteurs||{},offres:OFF&&OFF.offres||[]},fact:{compteurs:FACT&&FACT.compteurs||{},factures:FACT&&FACT.factures||[]},store:STORE,banque:{ver:1,imports:BANQUE&&BANQUE.imports||[]}}}async function clsSceller(e){const t=await cfrSceller(KCAT,cfrTE.encode(JSON.stringify(e)));return JSON.stringify({av:"classeur-air-ventil",v:1,i:t.i,c:t.c},null,1)}async function clsDesceller(e){const t=JSON.parse(String(e));if(!t||"classeur-air-ventil"!==t.av)throw new Error("pas un classeur");return JSON.parse(cfrTD.decode(await cfrDesceller(KCAT,{i:t.i,c:t.c})))}function clsCompte(){return(OFF&&OFF.offres?OFF.offres.length:0)+" offre(s), "+(FACT&&FACT.factures?FACT.factures.length:0)+" facture(s)"}async function clsEnvoyer(){if(!KCAT)return void toast("Ouvre d'abord une session : le classeur est scellé avec la clé du catalogue.");try{offEcrire()}catch(e){}try{storeSave()}catch(e){}let e="";try{e=await clsSceller(clsPaquet())}catch(e){return void toast("Le classeur n'a pas pu être scellé.")}try{if(window.showSaveFilePicker){const t=await showSaveFilePicker({suggestedName:CLS_FICHIER,types:[{description:"Classeur Air-Ventil",accept:{"application/json":[".json"]}}]}),r=await t.createWritable();return await r.write(e),await r.close(),void toast("Classeur envoyé — "+clsCompte()+".")}}catch(e){if(e&&"AbortError"===e.name)return}const t=new Blob([e],{type:"application/json"}),r=document.createElement("a");r.href=URL.createObjectURL(t),r.download=CLS_FICHIER,r.click(),URL.revokeObjectURL(r.href),toast("Classeur enregistré — "+clsCompte()+". Dépose-le dans I:\\app celil.")}function clsReprendre(e){const t=e&&e.files&&e.files[0];if(!t)return;const r=new FileReader;r.onload=async()=>{e.value="";let t=null;try{t=await clsDesceller(r.result)}catch(e){return void toast("Ce fichier ne s'ouvre pas ici : ce n'est pas un classeur, ou il vient d'une autre installation.")}clsAppliquer(t)},r.readAsText(t)}function clsAppliquer(e){const t=[],r=clsFusionRegistre(OFF.offres,(e.off||{}).offres,t,"offre"),s=clsFusionRegistre(FACT.factures,(e.fact||{}).factures,t,"facture"),i=clsCompteurs(OFF.compteurs,(e.off||{}).compteurs),o=clsCompteurs(FACT.compteurs,(e.fact||{}).compteurs),l=clsStoreReprise(e.store,t),u=clsBanque(e.banque),d="function"==typeof docRanger?docRanger(!1):{bouges:0};try{offSauver()}catch(e){}try{factSauver()}catch(e){}try{bqSauver()}catch(e){}try{storeAppliquer(),storeMaj()}catch(e){}STORE.maj=(new Date).toISOString();try{cfrEcrire(STORE_KEY,JSON.stringify(STORE))}catch(e){}try{offRender()}catch(e){}try{factRender()}catch(e){}return clsRapport(e,r,s,i+o,u,d,t),{off:r,fact:s,compteurs:i+o,banque:u,rang:d,vus:t,store:l}}function clsRapport(e,t,r,s,i,o,l){const u=$("clsSlot");if(!u)return;const d=String(e&&e.ecrit||"").slice(0,16).replace("T"," à "),m=[];m.push("
Classeur écrit le "+offEsc(d)+" par le poste "+offEsc(e&&e.poste||"?")+".
"),m.push("
");const li=(e,t)=>m.push("
"+e+"
"+t+"
");li("Offres ajoutées",t.ajoutes),li("Offres mises à jour",t.majs),li("Factures ajoutées",r.ajoutes),li("Factures mises à jour",r.majs),li("Encaissements et rappels repris",t.fondus+r.fondus),li("Compteurs relevés",s),li("Lectures bancaires reprises",i),li("Documents rangés dans leur société",o&&o.bouges||0),m.push("
"),l.length?(m.push("
À regarder
"),m.push("
"),l.forEach(e=>m.push("
"+offEsc(e.quoi)+(e.no?" "+offEsc(e.no):"")+"
"+offEsc(e.cause)+"
")),m.push("
")):m.push("
Rien à trancher : tout s'est rangé de soi-même.
"),u.innerHTML=m.join("");const h=$("clsModal");h&&(h.hidden=!1)}function clsFermer(){const e=$("clsModal");e&&(e.hidden=!0)}function clsFond(e){e&&e.target&&"clsModal"===e.target.id&&clsFermer()}function factCsv(){const e=[["N° facture","Date","Échéance","Offre","Client","Chantier","HT","HT2","TVA","TTC","État","Encaissé","Escompte accordé","Solde","Date de clôture"]];FACT.factures.forEach(t=>{const r=factCalc(t);e.push([t.no,t.date,t.echeance,t.offreNo||"",t.client||"",t.chantier||"",r.ht,r.ht2,r.tva,r.ttc,FACT_ETAT_TXT[factEtat(t)],factRecu(t),factEscompteAccorde(t),factSolde(t),t.cloture||""])});const t="\ufeff"+e.map(e=>e.map(e=>'"'+String(null==e?"":e).replace(/"/g,'""')+'"').join(";")).join("\r\n"),r=document.createElement("a");r.href=URL.createObjectURL(new Blob([t],{type:"text/csv;charset=utf-8"})),r.download="factures-air-ventil.csv",r.click(),URL.revokeObjectURL(r.href),toast(FACT.factures.length+" ligne(s) exportée(s) pour la fiduciaire.")}async function factDemarrer(){await factCharger(),factRender();const e=factVue().filter(e=>"retard"===factEtat(e)).length,t=factVue().filter(e=>rapAFaire(e,RAP_DELAI)).length;e&&setTimeout(()=>toast(e+" facture(s) dépassent leur échéance"+(t?" — "+t+" à relancer, onglet Rappels.":" — voir l'onglet Rappels.")),1600)}let OFF_TR=null;function offOnglet(e){const t={det:"ongDet",lig:"ongLig",res:"ongRes",imp:"ongImp",ia:"ongIA"},r={det:"panDet",lig:"panLig",res:"panRes",imp:"panImp",ia:"panIA"};if(r[e]||(e="det"),"imp"===e&&void 0!==IMPMODAL&&IMPMODAL&&impFermer(),Object.keys(t).forEach(s=>{const i=$(t[s]),o=$(r[s]);i&&i.classList.toggle("on",s===e),o&&(o.hidden=s!==e)}),"lig"===e&&ligRender(),"res"===e&&resRender(),"ia"===e)try{iaMaj()}catch(e){}}function offDate(e){return e?/^\d{4}-\d{2}-\d{2}$/.test(e)?e.slice(8,10)+"."+e.slice(5,7)+"."+e.slice(0,4):String(e):""}function offMiroir(e,t){const r=$(e),s=$(t);r&&s&&(s.value=r.value,totaux(),offAuto())}function offMiroirRetour(){[["docTva","txTva"],["docRem","txRem"]].forEach(([e,t])=>{const r=$(e),s=$(t);r&&s&&document.activeElement!==r&&r.value!==s.value&&(r.value=s.value)})}function sugOuverte(){if("object"!=typeof SUG_BOITES||!SUG_BOITES)return!1;for(const e in SUG_BOITES){const t=$(SUG_BOITES[e]);if(t&&!t.hidden)return!0}return!1}var POSMODAL=!1,POS_HOME={};function posAncrer(e){const t=$(e);return!t||POS_HOME[e]||(POS_HOME[e]={p:t.parentNode,n:t.nextSibling}),t}function posRendre(e){const t=$(e),r=POS_HOME[e];t&&r&&r.p&&r.p.insertBefore(t,r.n&&r.n.parentNode===r.p?r.n:null)}function posOuvrir(e){const t=$("posModal");if(!t||!$("cardSaisie")||!$("fCat"))return offAllerPage("cardSaisie",e);posAncrer("fCat"),posAncrer("cardSaisie"),$("posSlotCat").appendChild($("fCat")),$("posSlotCalc").appendChild($("cardSaisie")),POSMODAL=!0,e&&$("pType")&&($("pType").value=e),typeChange();const r=$("posTitre");r&&(r.textContent="catalogue"===e?"Ajouter une référence du catalogue":"gaine"===e?"Ajouter une gaine":"manuel"===e?"Ajouter un article libre":"Nouvelle position"),t.hidden=!1,setTimeout(()=>{const t=$("catalogue"===e?"catQ":"pA");if(t)try{t.focus()}catch(e){}},60)}function posFermer(){const e=$("posModal");e&&(e.hidden=!0,POSMODAL=!1,posRendre("fCat"),posRendre("cardSaisie"),typeChange())}function posFond(e){e&&e.target&&"posModal"===e.target.id&&posFermer()}var IMPMODAL=!1;function impFenetre(){const e=$("impModal");if(!e||!$("cardImport"))return erpPage("import");posAncrer("cardImport"),$("impSlot").appendChild($("cardImport")),IMPMODAL=!0,e.hidden=!1,setTimeout(()=>{const e=$("impTxt");if(e)try{e.focus()}catch(e){}},60)}function impFermer(){const e=$("impModal");e&&IMPMODAL&&(e.hidden=!0,IMPMODAL=!1,posRendre("cardImport"))}function impFond(e){e&&e.target&&"impModal"===e.target.id&&impFermer()}document.addEventListener("keydown",e=>{"Escape"===e.key&&IMPMODAL&&!sugOuverte()&&impFermer()}),document.addEventListener("keydown",e=>{"Escape"===e.key&&POSMODAL&&!sugOuverte()&&posFermer()});var CRQ_I=-1,CRQ_OUTIL="dep",CRQ_PILE=[],CRQ_PTS=[],CRQ_ATT=null,CRQ_DRAG=null,CRQ_SVGNS="http://www.w3.org/2000/svg",CRQ_AIDE={dep:"Tire un point bleu pour déplacer un trait de section. Les cotes n'ont pas de point bleu : une cote fausse se gomme et se repose. Le coude est tracé d'une seule courbe : il ne se tire pas non plus — gomme-le et redessine par-dessus.",trait:"Clique le départ, puis l'arrivée.",cote:"Tape la valeur, puis clique les deux extrémités à coter. Trait fin, deux petits traits aux bouts, pas de flèche — comme sur tes fiches.",txt:"Tape le texte, puis clique où le poser.",gom:"Clique un trait, une cote ou un texte pour l'effacer."};function crqBouton(e,t){if(!e||!e.fab&&!e.croq)return"";const r=e.croq?"Croquis retouché à la main — cliquer pour le reprendre":"Dessiner ou corriger le croquis de la pièce";return''}function crqSvg(){const e=$("crqPlan");return e?e.querySelector("svg.dsn"):null}function crqEl(e,t){const r=document.createElementNS(CRQ_SVGNS,e);return Object.keys(t).forEach(e=>r.setAttribute(e,t[e])),r}function crqFragment(e){const t=document.createElement("div");return t.innerHTML='",t.querySelector("svg")}function crqRemplir(e,t){for(;e.firstChild;)e.removeChild(e.firstChild);const r=crqFragment(t);for(;r.firstChild;)e.appendChild(r.firstChild)}function crqNu(e){const t=e.cloneNode(!0);return[...t.querySelectorAll("g.crq-hs, .crq-p")].forEach(e=>e.remove()),t}function crqBlanche(){return dsnSvg(dsnLeg("Croquis à la main"))}function crqValeur(){const e=$("crqVal");return e?String(e.value||"").trim():""}function crqOuvrir(e){const t=positions[e];if(!t)return void toast("Position introuvable.");if(!t.fab&&!t.croq)return void toast("Cette ligne n'est pas une pièce à fabriquer — rien à dessiner.");const r=$("crqModal");if(!r)return;CRQ_I=e,CRQ_PILE=[],CRQ_ATT=null,CRQ_DRAG=null;const s=$("crqTitre"),i=String(t.desc||"");s&&(s.textContent="Croquis — position "+(e+1)+" · "+(i.length>62?i.slice(0,62).trim()+"…":i)),crqPoser(dsnCroqDe(t)||crqBlanche()),crqOutil("dep"),r.hidden=!1}function crqFermer(){const e=$("crqModal");if(!e)return;e.hidden=!0,CRQ_I=-1,CRQ_PILE=[],CRQ_PTS=[],CRQ_ATT=null,CRQ_DRAG=null;const t=$("crqPlan");t&&(t.innerHTML="")}function crqFond(e){e&&e.target&&"crqModal"===e.target.id&&crqFermer()}function crqPoser(e){const t=$("crqPlan");if(!t)return;t.innerHTML=e;const r=crqSvg();r&&([...r.querySelectorAll("rect.d-p")].forEach(e=>{const t=+e.getAttribute("x")||0,r=+e.getAttribute("y")||0,s=+e.getAttribute("width")||0,i=+e.getAttribute("height")||0;e.parentNode.replaceChild(crqEl("polygon",{class:"d-p",points:[[t,r],[t+s,r],[t+s,r+i],[t,r+i]].map(e=>dsnR(e[0])+","+dsnR(e[1])).join(" ")}),e)}),r.addEventListener("pointerdown",crqBas),r.addEventListener("pointermove",crqBouge),r.addEventListener("pointerup",crqHaut),r.addEventListener("pointercancel",crqHaut),crqPoignees(),crqMaj())}function crqMaj(){const e=positions[CRQ_I],t=$("crqEtat"),r=$("crqAide");t&&(t.textContent=e&&e.croq?"Croquis retouché à la main : c'est lui qui part à l'atelier.":"Croquis automatique — enregistre pour garder tes retouches."),r&&(r.textContent=CRQ_AIDE[CRQ_OUTIL]||"")}function crqOutil(e){CRQ_OUTIL=e,CRQ_ATT=null,crqMarque(null),["dep","trait","cote","txt","gom"].forEach(t=>{const r=$("crqT-"+t);r&&r.classList.toggle("on",t===e)}),crqPoignees(),crqMaj()}function crqLirePts(e){return(e.getAttribute("points")||"").trim().split(/\s+/).filter(Boolean).map(e=>e.split(",").map(Number))}function crqTraits(e){return[...e.children].filter(e=>"g"!==e.tagName.toLowerCase()&&!(e.classList&&(e.classList.contains("crq-p")||e.classList.contains("crq-h"))))}function crqTirable(e){const t=e.classList;return!(t&&(t.contains("d-c")||t.contains("d-x")||t.contains("d-x2")))}function crqTousPoints(){const e=crqSvg();if(!e)return[];const t=[];return crqTraits(e).filter(crqTirable).forEach(e=>{const r=e.tagName.toLowerCase();"polygon"===r||"polyline"===r?crqLirePts(e).forEach((r,s)=>t.push({el:e,x:r[0],y:r[1],k:s,quoi:"pts"})):"line"===r?(t.push({el:e,x:+e.getAttribute("x1")||0,y:+e.getAttribute("y1")||0,k:1,quoi:"lig"}),t.push({el:e,x:+e.getAttribute("x2")||0,y:+e.getAttribute("y2")||0,k:2,quoi:"lig"})):"text"===r?t.push({el:e,x:+e.getAttribute("x")||0,y:+e.getAttribute("y")||0,k:0,quoi:"txt"}):"ellipse"!==r&&"circle"!==r||t.push({el:e,x:+e.getAttribute("cx")||0,y:+e.getAttribute("cy")||0,k:0,quoi:"cen"})}),t}function crqPoserPoint(e,t,r){const s=e.el;if("pts"===e.quoi){const i=crqLirePts(s);i[e.k]=[t,r],s.setAttribute("points",i.map(e=>dsnR(e[0])+","+dsnR(e[1])).join(" "))}else if("lig"===e.quoi)s.setAttribute("x"+e.k,dsnR(t)),s.setAttribute("y"+e.k,dsnR(r));else if("txt"===e.quoi){const e=(s.getAttribute("transform")||"").match(/rotate\(\s*(-?[\d.]+)/);s.setAttribute("x",dsnR(t)),s.setAttribute("y",dsnR(r)),e&&s.setAttribute("transform","rotate("+e[1]+" "+dsnR(t)+" "+dsnR(r)+")")}else s.setAttribute("cx",dsnR(t)),s.setAttribute("cy",dsnR(r));e.x=t,e.y=r}function crqPoignees(){const e=crqSvg();if(!e)return;const t=e.querySelector("g.crq-hs");if(t&&t.remove(),CRQ_PTS=[],"dep"!==CRQ_OUTIL)return;const r=crqEl("g",{class:"crq-hs"});(CRQ_PTS=crqTousPoints()).forEach(e=>{e.rond=crqEl("circle",{class:"txt"===e.quoi?"crq-h crq-ht":"crq-h",cx:dsnR(e.x),cy:dsnR(e.y),r:3.2}),r.appendChild(e.rond)}),e.appendChild(r)}function crqPlusPres(e,t){let r=null,s=t*t;return CRQ_PTS.forEach(t=>{const i=t.x-e.x,o=t.y-e.y,l=i*i+o*o;l<=s&&(s=l,r=t)}),r}function crqDseg(e,t,r){const s=r[0]-t[0],i=r[1]-t[1],o=s*s+i*i;let l=o?((e.x-t[0])*s+(e.y-t[1])*i)/o:0;l=Math.max(0,Math.min(1,l));const u=t[0]+l*s-e.x,d=t[1]+l*i-e.y;return Math.sqrt(u*u+d*d)}function crqDist(e,t){const r=e.tagName.toLowerCase();if("polygon"===r||"polyline"===r){const s=crqLirePts(e);if(!s.length)return 1e9;let i=1e9,o="polygon"===r?s.length:s.length-1;for(let e=0;e{const i=crqDist(t,e);i<=s&&(s=i,r=t)}),r}function crqPt(e){const t=crqSvg();if(!t||!t.getScreenCTM)return null;const r=t.getScreenCTM();if(!r)return null;const s=t.createSVGPoint();s.x=e.clientX,s.y=e.clientY;const i=s.matrixTransform(r.inverse());return{x:i.x,y:i.y}}function crqMarque(e){const t=crqSvg();if(!t)return;const r=t.querySelector("circle.crq-p");r&&r.remove(),e&&t.appendChild(crqEl("circle",{class:"crq-p",cx:dsnR(e.x),cy:dsnR(e.y),r:2.4}))}function crqEmpiler(){const e=crqSvg();e&&(CRQ_PILE.length>=40&&CRQ_PILE.shift(),CRQ_PILE.push(crqNu(e).innerHTML))}function crqAjouter(e){const t=crqSvg();if(!t)return;const r=t.querySelector("g.crq-hs"),s=crqFragment(e);for(;s.firstChild;)t.insertBefore(s.firstChild,r||null);crqPoignees()}function crqCote(e,t,r){const s=t.x-e.x,i=t.y-e.y,o=Math.sqrt(s*s+i*i)||1,l=-i/o*3,u=s/o*3,d=(e.x+t.x)/2,m=(e.y+t.y)/2;return dsnLg(e.x,e.y,t.x,t.y)+dsnLg(e.x-l,e.y-u,e.x+l,e.y+u)+dsnLg(t.x-l,t.y-u,t.x+l,t.y+u)+(Math.abs(i)>Math.abs(s)?dsnTxt(d-4,m,r,"d-t",-90):dsnTxt(d,m-4,r))}function crqBas(e){if(CRQ_I<0)return;const t=crqPt(e);if(!t)return;if(e.preventDefault&&e.preventDefault(),"dep"===CRQ_OUTIL){const r=crqPlusPres(t,9);if(!r)return void toast("Vise un point bleu pour le déplacer.");crqEmpiler(),CRQ_DRAG=r;try{crqSvg().setPointerCapture(e.pointerId)}catch(e){}return}if("gom"===CRQ_OUTIL){const e=crqCible(t);return e?(crqEmpiler(),e.remove(),void crqPoignees()):void toast("Rien à effacer à cet endroit.")}if("txt"===CRQ_OUTIL){const e=crqValeur();return e?(crqEmpiler(),void crqAjouter(dsnTxt(t.x,t.y,e))):void toast("Tape d'abord le texte dans la case, puis clique où le poser.")}if("cote"===CRQ_OUTIL&&!crqValeur())return void toast("Tape d'abord la valeur de la cote dans la case.");if(!CRQ_ATT)return CRQ_ATT=t,void crqMarque(t);const r=CRQ_ATT;CRQ_ATT=null,crqMarque(null),crqEmpiler(),crqAjouter("trait"===CRQ_OUTIL?dsnLg(r.x,r.y,t.x,t.y,"d-m"):crqCote(r,t,crqValeur()))}function crqBouge(e){if(!CRQ_DRAG)return;const t=crqPt(e);t&&(crqPoserPoint(CRQ_DRAG,t.x,t.y),CRQ_DRAG.rond&&(CRQ_DRAG.rond.setAttribute("cx",dsnR(t.x)),CRQ_DRAG.rond.setAttribute("cy",dsnR(t.y))))}function crqHaut(e){if(CRQ_DRAG){CRQ_DRAG=null;try{crqSvg().releasePointerCapture(e.pointerId)}catch(e){}crqPoignees()}}function crqAnnuler(){const e=crqSvg();e&&(CRQ_PILE.length?(crqRemplir(e,CRQ_PILE.pop()),CRQ_ATT=null,crqPoignees()):toast("Rien à annuler."))}function crqAuto(){const e=positions[CRQ_I];if(!e)return;const t=dsnPiece(e.fab);t?(crqEmpiler(),crqPoser(t)):toast("Cette pièce n'a pas de croquis automatique — page blanche.")}function crqVide(){crqSvg()&&(crqEmpiler(),crqPoser(crqBlanche()))}function crqOublier(){const e=positions[CRQ_I];if(e){if(!e.croq)return toast("Cette position n'a aucune retouche enregistrée."),void crqAuto();delete e.croq,crqFermer(),render(),toast("Retouches oubliées — le croquis automatique reprend la main.")}}function crqEnregistrer(){const e=positions[CRQ_I],t=crqSvg();if(!e||!t)return;const r=crqNu(t);r.setAttribute("class","dsn"),e.croq=r.outerHTML,crqFermer(),render(),toast("Croquis enregistré — il part tel quel sur le bon de préparation.")}function offAller(e,t){return"cardSaisie"===e||"catQ"===e?posOuvrir(t):offAllerPage(e,t)}function offAllerPage(e,t){t&&$("pType")&&($("pType").value=t,typeChange());const r=$(e);if(!r)return;const s=erpPageDe(r);s&&s!==ERP_PAGE&&erpPage(s);(r.closest(".card")||r).scrollIntoView({behavior:"smooth",block:"start"}),setTimeout(()=>{try{r.focus({preventScroll:!0})}catch(e){}},320)}document.addEventListener("keydown",e=>{"Escape"===e.key&&CRQ_I>=0&&crqFermer()});var LIG_EDIT=null,LIG_SEL=new Set,LIG_ANCRE=null;function ligRender(){const e=$("ligBody");if(!e)return;if(null==LIG_EDIT||positions[LIG_EDIT]||(LIG_EDIT=null),!positions.length)return LIG_EDIT=null,LIG_SEL=new Set,LIG_ANCRE=null,e.innerHTML='
'+(t?"Aucun client ne correspond a cette recherche.":"Aucun client dans le carnet.")+"
";const i=$("cliEtat");if(i){const e=cliListe(),t=e.filter(e=>e.actif&&e.nom).length,r=e.filter(e=>"loc"===e.src).length,o=e.filter(e=>e.corr).length;i.innerHTML=`${t} client(s) actif(s) sur ${e.length} fiche(s) · ${r} ajoute(s) ici · ${o} corrige(s) ici`+(s.length>CLI_AFF.length?` · ${s.length} correspondent, les 400 premiers sont affiches`:"")}}function cliRemplir(e){$("cliNom")&&($("cliNom").value=e?e.nom:""),$("cliAdr")&&($("cliAdr").value=e?e.adr:""),$("cliVille")&&($("cliVille").value=e?e.ville:""),$("cliTel")&&($("cliTel").value=e?e.tel:""),$("cliMail")&&($("cliMail").value=e?e.mail:""),cliRabRemplir(e);const t=$("cliTitre");t&&(t.textContent=e?"Fiche : "+e.nom:"Nouveau client");const r=$("cliOrigine");r&&(r.innerHTML=e?"loc"===e.src?"Fiche creee sur ce poste"+(e.par?" par "+offEsc(e.par):"")+(e.maj?", le "+offEsc(e.maj):"")+".":e.corr?"Contact du carnet livre, corrige sur ce poste"+(e.par?" par "+offEsc(e.par):"")+(e.maj?", le "+offEsc(e.maj):"")+".":"Contact du carnet livre avec l'application. Le corriger ici ne touche pas le carnet d'origine : la correction se pose par-dessus.":"La fiche sera enregistree sur ce poste, dans la memoire chiffree. Elle rejoint aussitot la recherche de clients de l'en-tete d'offre.")}function cliNouveau(){CLI_EDIT="",cliRemplir(null),erpPage("clients");const e=$("cliNom");e&&e.focus()}function cliEditer(e){const t=CLI_AFF[e];if(!t)return;CLI_EDIT=t.cle,cliRemplir(t);const r=$("cliNom");r&&r.focus()}function cliAnnuler(){CLI_EDIT="",cliRemplir(null)}function cliEnregistrer(){const e=($("cliNom")&&$("cliNom").value||"").trim();if(!e)return toast("Le nom du client est obligatoire."),void($("cliNom")&&$("cliNom").focus());const t=CLI_EDIT||cliCle(e),r=cliTrouver(t);STORE.clients=STORE.clients||{};const s={...STORE.clients[t]||{}};if(s.nom=e,s.adr=($("cliAdr")&&$("cliAdr").value||"").trim(),s.ville=($("cliVille")&&$("cliVille").value||"").trim(),s.tel=($("cliTel")&&$("cliTel").value||"").trim(),s.mail=($("cliMail")&&$("cliMail").value||"").trim(),estAdmin()){s.rem=String($("cliRem")&&$("cliRem").value||"").trim();const e={},t=$("cliRabBox");t&&t.querySelectorAll("input[type=number][id]").forEach(t=>{const r=String(t.value).trim();""!==r&&(e[t.id.slice(4)]=r)}),s.rab=e}void 0===s.actif&&(s.actif=!0),s.maj=ficDate(),s.par=ficQui(),STORE.clients[t]=s,CLI_EDIT=t,cliInvalider(),storeSave(),cliRender(),cliRemplir(cliTrouver(t)),storeMaj(),toast(r&&"cat"===r.src?"Contact « "+e+" » corrige sur ce poste — le carnet livre avec l'application reste intact.":"Client « "+e+" » enregistre.")}function cliBascule(e){const t=CLI_AFF[e];if(!t)return;if(t.actif&&!confirm("Desactiver le client « "+t.nom+" » ?\n\nRien n'est efface : il disparait des recherches, sa fiche reste consultable ici en cochant « Montrer les fiches desactivees », et les offres qui le citent ne bougent pas."))return;STORE.clients=STORE.clients||{};const r={...STORE.clients[t.cle]||{}};void 0===r.nom&&(r.nom=t.nom),void 0===r.adr&&(r.adr=t.adr),void 0===r.ville&&(r.ville=t.ville),void 0===r.tel&&(r.tel=t.tel),void 0===r.mail&&(r.mail=t.mail),r.actif=!t.actif,r.maj=ficDate(),r.par=ficQui(),STORE.clients[t.cle]=r,cliInvalider(),storeSave(),cliRender(),storeMaj(),toast(r.actif?"Client reactive.":"Client desactive — rien n'a ete efface.")}function cliVersOffre(e){const t=CLI_AFF[e];if(!t)return;$("docClient")&&($("docClient").value=t.nom),$("docAdr")&&($("docAdr").value=[t.nom,t.adr,t.ville].filter(Boolean).join("\n"));const r=condPoser();try{offAuto(),offRender()}catch(e){}erpPage("offres");const s=cliCondPour(t.nom);toast(r&&cliAConditions(s)?"Client « "+t.nom+" » repris dans l'offre a l'ecran — "+cliResume(s)+".":"Client « "+t.nom+" » repris dans l'offre a l'ecran.")}function cliDemarrer(){cliBuildRab(),cliInvalider(),cliRemplir(null),cliRender(),cliRoleAppliquer()}function cliCleD(e){return norm(String(e||"")).replace(/[‘’ʼ]/g,"'").replace(/\s+/g," ").trim()}function cliLivre(e){const t=cliCleD(e);return RABCLI&&Object.prototype.hasOwnProperty.call(RABCLI,t)?RABCLI[t]:null}function cliCondPour(e){const t=String(e||"").trim();if(!t)return null;const r=cliCle(t),s=cliLivre(t),i={};s&&Object.keys(s).forEach(e=>{i[grpId(e)]=s[e]});const o=(STORE&&STORE.clients||{})[r]||{},l=o.rab||{};return Object.keys(l).forEach(e=>{manEcrit(l[e])&&(i[e]=l[e])}),{cle:r,nom:t,rem:manEcrit(o.rem)?o.rem:"",rab:i,liv:!!s,loc:!(!o.rab&&!manEcrit(o.rem))}}function cliAConditions(e){if(!e)return!1;if(manEcrit(e.rem))return!0;const t=e.rab||{};return Object.keys(t).some(e=>manEcrit(t[e]))}function cliResume(e){if(!e)return"";const t=[];manEcrit(e.rem)&&t.push("rabais global "+e.rem+" %");let r=[];try{r=grpTous()}catch(e){r=[]}const s={};return r.forEach(r=>{const i=grpId(r);s[i]=1;const o=(e.rab||{})[i];manEcrit(o)&&t.push(grpAbr(r)+" "+o+" %")}),Object.keys(e.rab||{}).forEach(r=>{!s[r]&&manEcrit(e.rab[r])&&t.push(r+" "+e.rab[r]+" %")}),t.join(" · ")}var CLI_POSE=null;function cliDeposer(){if(!CLI_POSE)return;const e=CLI_POSE.av||{};if(Object.keys(e).forEach(t=>{const r=$(t);r&&(r.value=e[t])}),void 0!==CLI_POSE.remAv){const e=$("txRem");e&&(e.value=CLI_POSE.remAv);const t=$("docRem");t&&(t.value=CLI_POSE.remAv)}CLI_POSE=null}function cliAppliquer(e){if(!cliAConditions(e))return 0;if((!CLI_POSE||CLI_POSE.cle!==e.cle)&&(cliDeposer(),CLI_POSE={cle:e.cle,av:{}},Object.keys(e.rab).forEach(t=>{if(!manEcrit(e.rab[t]))return;const r=$("rab_"+t);r&&(CLI_POSE.av["rab_"+t]=r.value)}),manEcrit(e.rem))){const e=$("txRem");e&&(CLI_POSE.remAv=e.value)}let t=0;if(manEcrit(e.rem)){const r=$("txRem");r&&(r.value=e.rem,t++);const s=$("docRem");s&&(s.value=e.rem)}Object.keys(e.rab).forEach(r=>{if(!manEcrit(e.rab[r]))return;const s=$("rab_"+r);s&&(s.value=e.rab[r],t++)});try{render()}catch(e){try{totaux()}catch(e){}}return t}function condPoser(){let e=0;const t=cliCondPour($("docClient")?$("docClient").value:"");t&&cliAConditions(t)?e+=cliAppliquer(t):cliDeposer();let r=null;try{r=manParNom($("docChantier")?$("docChantier").value:"")}catch(e){}if(r&&manAConditions(r))try{e+=manAppliquer(r)}catch(e){}try{cliCondMaj(),manCondMaj()}catch(e){}return e}function cliCondMaj(){const e=$("cliCond");if(!e)return;const t=$("docClient"),r=t?cliCondPour(t.value):null;if(!r||!cliAConditions(r))return e.hidden=!0,void(e.innerHTML="");let s=null;try{s=manParNom($("docChantier")?$("docChantier").value:"")}catch(e){}const i=!(!s||!manAConditions(s));e.innerHTML="Conditions de ce client : "+offEsc(cliResume(r))+""+(i?' — le chantier passe devant':"")+' ',e.hidden=!1}function cliReprendre(){const e=cliCondPour($("docClient")?$("docClient").value:"");if(!e||!cliAConditions(e))return void toast("Ce client n'a pas de conditions particulières.");const t=condPoser();try{offAuto()}catch(e){}toast(t?"Conditions reprises : "+cliResume(e)+".":"Rien à reprendre.")}function cliBuildRab(){const e=$("cliRabBox");if(!e)return;let t=[];try{t=grpTous()}catch(e){t=[]}e.innerHTML=t.map(e=>'
'+offEsc(e)+" ("+offEsc(grpAbr(e))+')
').join("")}function cliRabRemplir(e){const t=$("cliRabBox"),r=e?cliLivre(e.nom):null,s=e&&(STORE&&STORE.clients||{})[e.cle]||{},i=s.rab||{};let o=[];try{o=grpTous()}catch(e){o=[]}t&&o.forEach(e=>{const t=grpId(e),s=$("cli_"+t);if(!s)return;const o=i[t];s.value=manEcrit(o)?o:"";const l=r&&void 0!==r[e]&&null!==r[e]?r[e]:null;s.placeholder=null!==l?l+" % — tarif ERP de ce client":(GRP_DEF[e]||0).toFixed(2)});const l=$("cliRem");l&&(l.value=e&&manEcrit(s.rem)?s.rem:"");const u=$("cliCondEtat");if(u)if(e){const t=cliCondPour(e.nom);u.innerHTML=cliAConditions(t)?"Conditions appliquées : "+offEsc(cliResume(t))+" — "+(r?"tarif de l'ERP":"saisi ici")+(r&&cliCondPour(e.nom).loc?", recouvert sur ce poste":"")+".":"Aucune condition connue pour ce client : le tarif habituel de la carte Tarifs s'applique."}else u.textContent="Choisis une fiche dans la liste pour voir ses conditions."}function cliDepuisEcran(){if(!estAdmin())return void toast("Seul un administrateur fixe les conditions d'un client.");const e=$("txRem");e&&$("cliRem")&&($("cliRem").value=e.value);let t=[];try{t=grpTous()}catch(e){t=[]}t.forEach(e=>{const t=$("rab_"+grpId(e)),r=$("cli_"+grpId(e));t&&r&&(r.value=t.value)}),toast("Rabais de l'écran recopiés ici. Ils ne recouvriront le tarif du client qu'une fois enregistrés.")}function cliRoleAppliquer(){const e=estAdmin(),t=$("cliRabBox");t&&t.querySelectorAll("input").forEach(t=>{t.disabled=!e,t.title=e?"":"Seul un administrateur fixe les rabais d'un client."});const r=$("cliRem");r&&(r.disabled=!e);const s=$("cliEcran");s&&(s.disabled=!e);const i=$("cliLock");i&&(i.hidden=e)}var MAN_LISTE=null,MAN_AFF=[],MAN_EDIT="";function manCle(e){return"h:"+norm(String(e||"")).replace(/\s+/g," ").trim()}function manInvalider(){MAN_LISTE=null}function manListe(){if(MAN_LISTE)return MAN_LISTE;const e=STORE&&STORE.chantiers||{},t=[];return Object.keys(e).forEach(r=>{const s=e[r];s&&"object"==typeof s&&t.push({cle:r,nom:s.nom||"",client:s.client||"",note:s.note||"",rem:void 0===s.rem||null===s.rem?"":String(s.rem),rab:{...s.rab||{}},actif:!1!==s.actif,cree:s.cree||"",vu:s.vu||"",maj:s.maj||"",par:s.par||"",parMaj:s.parMaj||""})}),MAN_LISTE=t,t}function manActifs(){return manListe().filter(e=>e.actif&&e.nom)}function manTrouver(e){const t=manListe();for(let r=0;rmanEcrit(t[e]))}function manResume(e){const t=[];manEcrit(e.rem)&&t.push("rabais global "+e.rem+" %");let r=[];try{r=grpTous()}catch(e){r=[]}r.forEach(r=>{const s=(e.rab||{})[grpId(r)];manEcrit(s)&&t.push(grpAbr(r)+" "+s+" %")});const s=new Set(r.map(e=>grpId(e)));return Object.keys(e.rab||{}).forEach(r=>{!s.has(r)&&manEcrit(e.rab[r])&&t.push(r+" "+e.rab[r]+" %")}),t.join(" · ")}function manNbOffres(e){const t=manCle(e);try{return offVue().filter(e=>e&&String(e.chantier||"").trim()&&manCle(e.chantier)===t).length}catch(e){return 0}}function manNoter(e,t,r){const s=String(e||"").trim();if(!s)return null;const i=manCle(s),o=ficDate();STORE.chantiers=STORE.chantiers||{};const l=STORE.chantiers[i];if(!l&&!r)return null;const u={...l||{}};let d=!1;l||(u.nom=s.replace(/\s+/g," "),u.cree=o,u.par=ficQui(),u.actif=!0,d=!0);const m=String(t||"").trim();if(m&&!u.client&&(u.client=m,d=!0),u.vu!==o&&(u.vu=o,d=!0),!d)return l;if(STORE.chantiers[i]=u,manInvalider(),storeSave(),!l){try{manRender(),storeMaj()}catch(e){}toast("Chantier « "+s+" » enregistré. Ses conditions de rabais se saisissent dans l'onglet Chantiers.")}return u}function manSearch(){const e=$("docChantier"),t=$("manRes");if(manCondMaj(),!e||!t)return;const r=manActifs(),s=norm(e.value.trim());if(s.length<2)return void(t.hidden=!0);const i=s.split(/\s+/),o=[];for(let e=0;et.includes(e))&&o.push(e)}t.innerHTML=o.length?o.map(e=>{const t=r[e],s=manNbOffres(t.nom),i=[t.client||"sans client",manAConditions(t)?manResume(t):"pas de conditions particulières",s?s+" offre(s)":""].filter(Boolean).join(" — ");return'
'+offEsc(t.nom)+''+offEsc(i)+"
"}).join(""):'
Aucun chantier connu sous ce titre — il s\'enregistrera tout seul en quittant le champ.
',t.hidden=!1}function manPick(e){const t=manActifs()[e];if(!t)return;const r=$("docChantier");r&&(r.value=t.nom);const s=$("manRes");s&&(s.hidden=!0);const i=manAppliquer(t);manCondMaj();try{offAuto(),offRender()}catch(e){}toast(i?"Chantier « "+t.nom+" » — conditions reprises : "+manResume(t)+".":"Chantier « "+t.nom+" » repris (aucune condition particulière).")}function manAppliquer(e){if(!manAConditions(e))return 0;let t=0;if(manEcrit(e.rem)){const r=$("txRem");r&&(r.value=e.rem,t++);const s=$("docRem");s&&(s.value=e.rem)}const r=e.rab||{};Object.keys(r).forEach(e=>{if(!manEcrit(r[e]))return;const s=$("rab_"+e);s&&(s.value=r[e],t++)});try{render()}catch(e){try{totaux()}catch(e){}}return t}function manCondMaj(){const e=$("manCond");if(!e)return;const t=$("docChantier"),r=t?manParNom(t.value):null;if(!r||!manAConditions(r))return e.hidden=!0,void(e.innerHTML="");e.innerHTML="Conditions de ce chantier : "+offEsc(manResume(r))+' ',e.hidden=!1}function manReprendre(){const e=$("docChantier"),t=e?manParNom(e.value):null;if(!t)return void toast("Ce titre ne correspond à aucun chantier enregistré.");const r=manAppliquer(t);try{offAuto()}catch(e){}toast(r?"Conditions reprises : "+manResume(t)+".":"Ce chantier n'a pas de conditions particulières.")}function manBuildRab(){const e=$("manRabBox");if(!e)return;let t=[];try{t=grpTous()}catch(e){t=[]}e.innerHTML=t.map(e=>'
"+offEsc([e.cree?"ouvert le "+e.cree:"",e.vu&&e.vu!==e.cree?"vu le "+e.vu:""].filter(Boolean).join(" · "))+'
").join("")+"
":'
'+(t?"Aucun chantier ne correspond à cette recherche.":"Aucun chantier encore. Le premier s'inscrira tout seul : écris un titre dans l'en-tête d'une offre.")+"
";const i=$("manEtat");if(i){const e=manListe(),t=e.filter(e=>e.actif&&e.nom).length,r=e.filter(e=>manAConditions(e)).length;i.innerHTML=t+" chantier(s) ouvert(s) sur "+e.length+" · "+r+" avec des conditions particulières"+(s.length>MAN_AFF.length?" · "+s.length+" correspondent, les 400 premiers sont affichés":"")}}function manRemplir(e){const set=(e,t)=>{const r=$(e);r&&(r.value=t||"")};set("manNom",e?e.nom:""),set("manClient",e?e.client:""),set("manNote",e?e.note:""),set("manRem",e?e.rem:"");const t=$("manRabBox");t&&t.querySelectorAll("input[type=number][id]").forEach(t=>{const r=e?(e.rab||{})[t.id.slice(4)]:"";t.value=manEcrit(r)?r:""});const r=$("manTitre");r&&(r.textContent=e?"Chantier : "+e.nom:"Nouveau chantier");const s=$("manOrigine");s&&(s.innerHTML=e?["Ouvert"+(e.cree?" le "+offEsc(e.cree):"")+(e.par?" par "+offEsc(e.par):"")+".",e.maj?"Conditions revues le "+offEsc(e.maj)+(e.parMaj?" par "+offEsc(e.parMaj):"")+".":"",manNbOffres(e.nom)+" offre(s) portent ce titre."].filter(Boolean).join(" "):"Un chantier s'inscrit déjà tout seul quand tu écris son titre dans une offre. Cette fiche sert à lui donner ses conditions.")}function manNouveau(){MAN_EDIT="",manRemplir(null),erpPage("chantiers");const e=$("manNom");e&&e.focus()}function manEditer(e){const t=MAN_AFF[e];if(!t)return;MAN_EDIT=t.cle,manRemplir(t);const r=$("manNom");r&&r.focus()}function manAnnuler(){MAN_EDIT="",manRemplir(null)}function manEnregistrer(){const e=($("manNom")&&$("manNom").value||"").trim();if(!e)return toast("Le titre du chantier est obligatoire."),void($("manNom")&&$("manNom").focus());const t=MAN_EDIT||manCle(e),r=estAdmin();STORE.chantiers=STORE.chantiers||{};const s={...STORE.chantiers[t]||{}};manTrouver(t);if(s.nom=e,s.client=($("manClient")&&$("manClient").value||"").trim(),s.note=($("manNote")&&$("manNote").value||"").trim(),r){const e=String($("manRem")&&$("manRem").value||"").trim();s.rem=e;const t={},r=$("manRabBox");r&&r.querySelectorAll("input[type=number][id]").forEach(e=>{const r=String(e.value).trim();""!==r&&(t[e.id.slice(4)]=r)}),s.rab=t,s.maj=ficDate(),s.parMaj=ficQui()}void 0===s.actif&&(s.actif=!0),s.cree||(s.cree=ficDate(),s.par=ficQui()),s.vu=s.vu||s.cree,STORE.chantiers[t]=s,MAN_EDIT=t,manInvalider(),storeSave(),manRender(),manRemplir(manTrouver(t)),storeMaj(),manCondMaj();const i=manTrouver(t);toast(r?manAConditions(i)?"Chantier « "+e+" » : "+manResume(i)+".":"Chantier « "+e+" » enregistré — tarif habituel, aucune condition particulière.":"Chantier « "+e+" » enregistré — seul un administrateur en change les rabais.")}function manBascule(e){const t=MAN_AFF[e];if(!t)return;if(t.actif&&!confirm("Clore le chantier « "+t.nom+" » ?\n\nRien n'est effacé : il sort de la recherche du titre d'offre, sa fiche et ses conditions restent ici en cochant « Montrer les chantiers clos », et les offres qui le citent ne bougent pas."))return;STORE.chantiers=STORE.chantiers||{};const r={...STORE.chantiers[t.cle]||{}};void 0===r.nom&&(r.nom=t.nom),r.actif=!t.actif,STORE.chantiers[t.cle]=r,manInvalider(),storeSave(),manRender(),storeMaj(),toast(r.actif?"Chantier rouvert.":"Chantier clos — rien n'a été effacé.")}function manVersOffre(e){const t=MAN_AFF[e];if(!t)return;$("docChantier")&&($("docChantier").value=t.nom),t.client&&$("docClient")&&!$("docClient").value.trim()&&($("docClient").value=t.client);const r=manAppliquer(t);manCondMaj();try{offAuto(),offRender()}catch(e){}erpPage("offres"),toast(r?"Chantier « "+t.nom+" » repris dans l'offre à l'écran — "+manResume(t)+".":"Chantier « "+t.nom+" » repris dans l'offre à l'écran.")}function manDepuisEcran(){if(!estAdmin())return void toast("Seul un administrateur fixe les conditions d'un chantier.");const e=$("txRem");e&&$("manRem")&&($("manRem").value=e.value);let t=[];try{t=grpTous()}catch(e){t=[]}t.forEach(e=>{const t=$("rab_"+grpId(e)),r=$("man_"+grpId(e));t&&r&&(r.value=t.value)}),toast("Rabais de l'écran recopiés ici. Ils ne seront la condition du chantier qu'une fois enregistrés.")}function manRoleAppliquer(){const e=estAdmin(),t=$("manRabBox");t&&t.querySelectorAll("input").forEach(t=>{t.disabled=!e,t.title=e?"":"Seul un administrateur fixe les rabais d'un chantier."});const r=$("manRem");r&&(r.disabled=!e);const s=$("manEcran");s&&(s.disabled=!e);const i=$("manLock");i&&(i.hidden=e)}function manDemarrer(){manBuildRab(),manInvalider(),manRemplir(null),manRender(),manRoleAppliquer()}let ART_CODE=null,ART_MASQ=null,ARF_I={},ARF_AFF=[],ARF_EDIT="";function arfCle(e){return norm(String(e||"")).trim()}function arfCodeIdx(){return ART_CODE||(ART_CODE={},ART&&ART.a&&ART.a.forEach((e,t)=>{const r=arfCle(e[0]);Object.prototype.hasOwnProperty.call(ART_CODE,r)||(ART_CODE[r]=t)})),ART_CODE}function arfGrpIdx(e){if(!ART||!ART.g)return 0;const t=ART.g.indexOf(e);if(t>=0)return t;const r=ART.g.indexOf("Standard");return r>=0?r:0}function arfInvalider(){artIndex=null,artIndexC=null,NORMREFS=null,FIRSTIDX=null,REFIDX=null,SLQ_IDX=null,DFHC_IDX=null,KGLIST=null,FZ=null}function arfAppliquer(){if(!ART||!ART.a)return 0;const e=arfCodeIdx(),t=STORE.articles||{};ART_MASQ=new Set;let r=0;return Object.keys(t).forEach(s=>{const i=t[s];if(!i||"object"!=typeof i)return;let o=e[s];if(void 0===o)o=ART.a.length,ART.a.push([i.code||s,i.des||"",+i.prix||0,arfGrpIdx(i.grp)]),e[s]=o;else{const e=ART.a[o];Array.isArray(i.ref0)||(i.ref0=[e[1],e[2],e[3]]),"string"==typeof i.des&&""!==i.des&&(e[1]=i.des),void 0!==i.prix&&null!==i.prix&&""!==i.prix&&isFinite(+i.prix)&&(e[2]=+i.prix),i.grp&&(e[3]=arfGrpIdx(i.grp))}ARF_I[s]=o,!1===i.actif&&ART_MASQ.add(o),r++}),arfInvalider(),r}function arfRender(){const e=$("arfBox");if(!e||!ART||!ART.a)return;const t=norm(($("arfQ")?$("arfQ").value:"").trim()),r=$("arfG")?$("arfG").value:"",s=STORE.articles||{};let i=[],o=0,l=!1;if(t.length>=2){const e=artRanger(t,r);o=e.length,l=!0,i=e.slice(0,60).map(e=>e[1])}else i=Object.keys(s).map(e=>ARF_I[e]).filter(e=>void 0!==e),""!==r&&(i=i.filter(e=>ART.a[e][3]===+r)),o=i.length;ARF_AFF=i,e.innerHTML=i.length?'
\n
Code
Designation
Groupe
Prix CHF
\n
Image
Fiche
'+i.map((e,t)=>{const r=ART.a[e],i=arfCle(r[0]),o=s[i],l=!(!o||!1!==o.actif),u=ARF_SEL.has(i),d=!!(o&&Array.isArray(o.ref0)&&Math.abs((+o.ref0[1]||0)-(+r[2]||0))>.004),m=[];o&&o.nouveau?m.push('ajoute ici'):d?m.push('prix corrige'):o&&(o.des||void 0!==o.prix)&&m.push('fiche du poste'),o&&o.img&&m.push('image'),l&&m.push('desactive');const h=m.length?m.join(" "):"catalogue",g=d?`${fmt(+o.ref0[1]||0)}`:"",b=o&&o.img?``:"",A=o?o.nouveau?``:``:"";return`
\n
\n
${offEsc(r[0])}
${offEsc(r[1]||"")}
\n
${offEsc(ART.g[r[3]]||"Standard")}
\n
${g}${fmt(+r[2]||0)}
\n
${b}
\n
${h}
\n
${A}
`}).join("")+"
":'
'+(l?"Aucun article ne correspond a cette recherche.":"Aucun article ajoute ni corrige sur ce poste. Tape au moins deux lettres pour chercher dans le catalogue.")+"
";const u=$("arfEtat");if(u){const e=Object.keys(s).filter(e=>s[e]&&s[e].nouveau).length,t=Object.keys(s).filter(e=>s[e]&&s[e].img).length,r=Object.keys(s).filter(e=>{const t=s[e],r=ARF_I[e];return t&&!t.nouveau&&Array.isArray(t.ref0)&&void 0!==r&&ART.a[r]&&Math.abs((+t.ref0[1]||0)-(+ART.a[r][2]||0))>.004}).length;u.innerHTML=(l?`${o} reference(s) trouvee(s)`+(o>i.length?` — les ${i.length} plus proches sont affichees`:""):`${o} fiche(s) de ce poste affichee(s)`)+` · catalogue : ${ART.a.length} reference(s) · ${e} article(s) cree(s) ici · ${r} prix corrige(s) ici`+` · ${t} image(s) de reference`}const d=$("arfImgNote");d&&(d.hidden=!i.length),arfSelMaj()}function arfRemplir(e){const t=STORE.articles||{};$("arfCode")&&($("arfCode").value=e?e[0]:""),$("arfDes")&&($("arfDes").value=e&&e[1]||""),$("arfPrix")&&($("arfPrix").value=e?(+e[2]||0).toFixed(2):"");const r=$("arfGrp");r&&(r.value=e?String(ART&&ART.g&&ART.g[e[3]]||"Standard"):"Standard");const s=e?t[arfCle(e[0])]:null,i=$("arfTitre");i&&(i.textContent=e?"Fiche : "+e[0]:"Nouvel article");const o=$("arfOrigine");o&&(o.innerHTML=e?s?s.nouveau?"Article cree sur ce poste"+(s.par?" par "+offEsc(s.par):"")+(s.maj?", le "+offEsc(s.maj):"")+".":"Reference du catalogue, corrigee sur ce poste"+(s.par?" par "+offEsc(s.par):"")+(s.maj?", le "+offEsc(s.maj):"")+(Array.isArray(s.ref0)?" — prix d'origine "+fmt(+s.ref0[1]||0)+" CHF.":"."):"Reference du catalogue livre avec l'application — prix d'origine "+fmt(+e[2]||0)+" CHF. La corriger ici pose une correction par-dessus, sans reecrire le catalogue.":"Nouvel article : il s'ajoute au catalogue de ce poste. Le fichier livre avec l'application n'est pas touche."),arfRoleAppliquer()}function arfNouveau(){ARF_EDIT="",arfRemplir(null),erpPage("articles");const e=$("arfCode");e&&!e.disabled&&e.focus()}function arfEditer(e){const t=ARF_AFF[e];void 0!==t&&ART.a[t]&&(ARF_EDIT=arfCle(ART.a[t][0]),arfRemplir(ART.a[t]))}function arfAnnulerForm(){ARF_EDIT="",arfRemplir(null)}function arfEnregistrer(){if(!estAdmin())return void toast("Seul un administrateur peut creer un article ou corriger un prix.");const e=($("arfCode")&&$("arfCode").value||"").trim();if(!e)return toast("Le code de l'article est obligatoire."),void($("arfCode")&&$("arfCode").focus());const t=($("arfPrix")&&$("arfPrix").value||"").trim().replace(",",".");if(""===t||!isFinite(+t))return toast("Le prix doit etre un nombre."),void($("arfPrix")&&$("arfPrix").focus());const r=r2(+t),s=ARF_EDIT||arfCle(e),i=arfCodeIdx()[s],o=void 0!==i?+ART.a[i][2]||0:null,l=STORE.articles=STORE.articles||{},u={...l[s]||{}};u.code=e,u.des=($("arfDes")&&$("arfDes").value||"").trim(),u.grp=$("arfGrp")&&$("arfGrp").value||"Standard",u.prix=r,void 0===u.actif&&(u.actif=!0),void 0===i&&void 0===u.nouveau&&(u.nouveau=!0),u.maj=ficDate(),u.par=ficQui(),l[s]=u,null!==o&&Math.abs(o-r)>.004&&(STORE.histo=STORE.histo||[],STORE.histo.push([u.maj,u.par,e,r2(o),r])),arfAppliquer(),storeSave(),storeMaj(),ARF_EDIT=s,arfRender(),arfHisto();const d=ARF_I[s];void 0!==d&&arfRemplir(ART.a[d]),toast(void 0===i?"Article « "+e+" » cree sur ce poste.":"Prix corrige : « "+e+" » a "+fmt(r)+" CHF — le catalogue d'origine n'est pas touche.")}function arfAnnuler(e){if(!estAdmin())return void toast("Seul un administrateur peut annuler une correction de prix.");const t=ARF_AFF[e];if(void 0===t||!ART.a[t])return;const r=arfCle(ART.a[t][0]),s=STORE.articles||{},i=s[r];if(!i)return;if(!Array.isArray(i.ref0))return void toast("Cet article a ete cree ici : il se desactive, il ne revient pas au catalogue.");if(!confirm("Revenir au prix et a la designation d'origine pour « "+ART.a[t][0]+" » ?\n\nPrix du catalogue : "+fmt(+i.ref0[1]||0)+" CHF. Le journal des corrections garde la trace de l'ancienne valeur."))return;const o=+ART.a[t][2]||0,l=+i.ref0[1]||0;ART.a[t][1]=i.ref0[0],ART.a[t][2]=i.ref0[1],ART.a[t][3]=i.ref0[2],delete s[r],delete ARF_I[r],i.img&&(s[r]={code:ART.a[t][0],img:i.img,maj:ficDate(),par:ficQui()}),Math.abs(o-l)>.004&&(STORE.histo=STORE.histo||[],STORE.histo.push([ficDate(),ficQui(),ART.a[t][0],r2(o),r2(l)])),arfAppliquer(),storeSave(),storeMaj(),ARF_EDIT="",arfRender(),arfHisto(),arfRemplir(ART.a[t]),toast("Reference revenue au prix du catalogue.")}function arfBascule(e){if(!estAdmin())return void toast("Seul un administrateur peut desactiver un article.");const t=ARF_AFF[e];if(void 0===t||!ART.a[t])return;const r=arfCle(ART.a[t][0]),s=(STORE.articles||{})[r];if(!s)return;const i=!1===s.actif;(i||confirm("Desactiver l'article « "+ART.a[t][0]+" » ?\n\nRien n'est efface : il sort des recherches du catalogue, sa fiche reste ici et les offres qui le citent restent lisibles."))&&(s.actif=i,s.maj=ficDate(),s.par=ficQui(),arfAppliquer(),storeSave(),arfRender(),toast(s.actif?"Article reactive.":"Article desactive — rien n'a ete efface."))}function arfHisto(){const e=$("arfHistoBox");if(!e)return;const t=(STORE.histo||[]).slice().reverse().slice(0,40);e.innerHTML=t.length?'
Date
Par
Article
\n
Ancien prix
Nouveau prix
'+t.map(e=>`
${offEsc(e[0])}
${offEsc(e[1]||"")}
${offEsc(e[2])}
\n
${fmt(+e[3]||0)}
${fmt(+e[4]||0)}
`).join("")+`
Journal complet :\n ${(STORE.histo||[]).length} ligne(s). On n'en efface jamais une — c'est la memoire des prix.
`:'
Aucune correction de prix enregistree sur ce poste.
'}var ARF_SEL=new Set,ARF_ANC=-1;function arfImgUrl(e){const t=String(e||"").trim();return t?/^(https?:|file:|data:|blob:)/i.test(t)?t:/^\\\\/.test(t)?encodeURI("file:"+t.replace(/\\/g,"/")):/^[A-Za-z]:[\\/]/.test(t)?encodeURI("file:///"+t.replace(/\\/g,"/")):t.replace(/\\/g,"/"):""}function arfCleDe(e){const t=ARF_AFF[e];return void 0!==t&&ART&&ART.a[t]?arfCle(ART.a[t][0]):""}function arfMarquer(e,t){const r=arfCleDe(e);r&&(t?ARF_SEL.add(r):ARF_SEL.delete(r))}function arfSelCodes(){return Array.from(ARF_SEL)}function arfSelVider(){ARF_SEL.clear(),ARF_ANC=-1,arfRender()}function arfCoche(e,t){e&&e.stopPropagation&&e.stopPropagation();const r=!e||!e.target||!!e.target.checked;if(e&&e.shiftKey&&ARF_ANC>=0){const e=Math.min(ARF_ANC,t),s=Math.max(ARF_ANC,t);for(let t=e;t<=s;t++)arfMarquer(t,r)}else arfMarquer(t,r);ARF_ANC=t,arfRender()}function arfLigneClic(e,t){if(!e)return;if(e.target&&e.target.closest&&e.target.closest("button, input, select, textarea, a"))return;if(!e.ctrlKey&&!e.metaKey&&!e.shiftKey)return;e.preventDefault&&e.preventDefault();const r=arfCleDe(t);if(r){if(e.shiftKey&&ARF_ANC>=0){const e=Math.min(ARF_ANC,t),r=Math.max(ARF_ANC,t);for(let t=e;t<=r;t++)arfMarquer(t,!0)}else arfMarquer(t,!ARF_SEL.has(r));ARF_ANC=t,arfRender()}}function arfCocheTout(e){ARF_AFF.forEach((t,r)=>arfMarquer(r,!!e)),ARF_ANC=-1,arfRender()}function arfSelMaj(){const e=ARF_SEL.size,t=$("arfSelBar");t&&(t.hidden=0===e);const r=ARF_AFF.filter((e,t)=>ARF_SEL.has(arfCleDe(t))).length,s=$("arfTout");s&&(s.checked=ARF_AFF.length>0&&r===ARF_AFF.length,s.indeterminate=r>0&&r1?"s":"")+" cochée"+(e>1?"s":"")+(r{s.textContent="Image trouvée ("+r.naturalWidth+"×"+r.naturalHeight+") — elle sera imprimée sur le bon de préparation."},r.onerror=()=>{r.style.display="none",s.textContent="Cette image ne s'ouvre pas depuis ce poste. Le chemin est gardé tel quel : il sera lu depuis le poste qui imprime, si le serveur y est monté."},r.src=arfImgUrl(t),e.appendChild(r),e.appendChild(s)}function arfImgPoser(){if(!estAdmin())return void toast("Seul un administrateur attache une image à une référence.");const e=($("arfImg")&&$("arfImg").value||"").trim();if(!e)return toast("Indique d'abord le chemin de l'image sur le serveur."),void($("arfImg")&&$("arfImg").focus());arfImgEcrire(e)}function arfImgOter(){estAdmin()?arfImgEcrire(""):toast("Seul un administrateur retire l'image d'une référence.")}function arfImgEcrire(e){const t=arfSelCodes();if(!t.length)return void toast("Coche d'abord au moins une référence.");const r=STORE.articles=STORE.articles||{},s=arfCodeIdx();let i=0;t.forEach(t=>{const o=void 0!==ARF_I[t]?ARF_I[t]:s[t];if(void 0===o||!ART.a[o])return;const l={...r[t]||{}};l.code||(l.code=ART.a[o][0]),e?l.img=e:delete l.img,l.maj=ficDate(),l.par=ficQui(),e||void 0!==l.nouveau||void 0!==l.prix||l.des||void 0!==l.actif?r[t]=l:(delete r[t],delete ARF_I[t]),i++}),arfAppliquer(),storeSave(),storeMaj(),arfRender(),toast(e?"Image de référence posée sur "+i+" référence(s) — aucun prix n'a bougé.":"Image retirée de "+i+" référence(s).")}function arfRoleAppliquer(){const e="function"!=typeof estAdmin||estAdmin(),t=$("arfForm");t&&t.querySelectorAll("input, select, textarea, button").forEach(t=>{t.disabled=!e}),["arfImg","arfImgOk","arfImgNon"].forEach(t=>{const r=$(t);r&&(r.disabled=!e)});const r=$("arfLock");r&&(r.hidden=e)}function arfDemarrer(){const e=$("arfGrp");e&&ART&&ART.g&&!e.options.length&&ART.g.forEach(t=>{const r=document.createElement("option");r.value=t,r.textContent=t,e.appendChild(r)});const t=$("arfG");t&&ART&&ART.g&&t.options.length<=1&&ART.g.forEach((e,r)=>{const s=document.createElement("option");s.value=r,s.textContent=e,t.appendChild(s)}),arfAppliquer(),arfRemplir(null),arfRender(),arfHisto()}document.addEventListener("keydown",e=>{if(e.altKey||!e.ctrlKey&&!e.metaKey)return;if("a"!==e.key&&"A"!==e.key)return;if(void 0===ERP_PAGE||"articles"!==ERP_PAGE)return;if(!ARF_AFF||!ARF_AFF.length)return;const t=document.activeElement;if(t&&"arfQ"===t.id){const e=t.value||"";if(e&&(0!==t.selectionStart||t.selectionEnd!==e.length))return}else if(t&&/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)&&"arfTout"!==t.id)return;e.preventDefault(),arfCocheTout(!0)});let CTIDX=null;function ctSearch(e,t){if("docClient"===(e||"docClient"))try{cliCondMaj()}catch(e){}const r=cliActifs();CTIDX&&CTIDX.length===r.length||(CTIDX=r.map(e=>norm(e.nom+" "+e.ville)));const s=e||"docClient",i=t||"ctRes",o=norm($(s).value.trim()),l=$(i);if(o.length<2)return void(l.hidden=!0);const u=o.split(/\s+/),d=[];for(let e=0;eCTIDX[e].includes(t))&&d.push(e);l.innerHTML=d.length?d.map(e=>`