Files
ASPIC/ecran_fond.html

1491 lines
80 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASPIC — Écran de fond (essai)</title>
<!-- Icône d'onglet (3 septembre 2026, voir journal) : logo Mévélia Solutions,
fourni par Jean-Marc, reprise à l'identique sur tous les écrans. -->
<link rel="icon" href="mevelia-icone.png">
<style>
/* ecran_fond.html — 2 septembre 2026
Premier essai de l'écran de fond "bureau à l'ancienne" (voir journal de
développement, section 28, et réflexions stratégiques, section 11).
Volontairement minimal pour ce test : affichage plein écran de l'image
de référence, sans aucun objet cliquable/fonctionnel — juste de quoi
voir le rendu réel après identification, avant d'aller plus loin.
Image mise à jour le 2 septembre 2026 (soir) — nouvelle version sans
tableau en liège, recadrée par Jean-Marc lui-même après un premier essai
où le "cover" rognait le bas de l'image sur son écran (proportions
d'écran différentes du 16:9 de l'image). Passage de background-size
"cover" à "contain" à cette occasion : l'image entière reste désormais
toujours visible, quelle que soit la proportion de l'écran, quitte à
laisser une bande de la couleur du mur de part et d'autre plutôt que de
rogner — plus robuste qu'un recadrage qui ne marche que pour une
proportion d'écran précise.
Image recadrée une 2ᵉ fois (toujours le 2 septembre 2026) : Jean-Marc a
utilisé une lecture de fenêtre temporaire (affichée le temps du recadrage
puis retirée) pour recadrer directement à ses proportions d'écran
réelles — les bandes "contain" restent le filet de sécurité pour tout
autre écran, mais ne devraient plus apparaître chez lui. Nouvelle image
1024×457 : toutes les coordonnées ci-dessous ont été remesurées sur
cette version (celles mesurées sur la précédente, 1280×650, ne
s'appliquent plus).
8 septembre 2026 — passage à plusieurs fonds interchangeables (voir
réflexions stratégiques). Jean-Marc a fourni un jeu d'images séparant
enfin ce qui était jusqu'ici une seule photo plate (ecran_fond.jpg,
bureau + classeurs + micro + carnet + téléphone fondus ensemble) en
deux calques réutilisables :
- avant_plan_bureau.png : classeurs, micro, carnet, téléphone et
plateau du bureau, en PNG transparent (fond réellement transparent,
vérifié) — IDENTIQUE quel que soit l'écran affiché.
- fond_bureau_1.jpg / fond_bureau_2.jpg / fond_bureau_3.jpg : les 3
décors interchangeables (liège actuel, tasseaux gris pour l'écran
assistants, tableau blanc pour les futurs tableaux de bord),
affichés SEULS derrière l'avant-plan, sans classeurs dessus.
Toutes les images font 1024×457 comme l'ancienne photo — même logique
de "contain" ci-dessous, appliquée à l'avant-plan ET au conteneur des
fonds ensemble (voir #fonds-container/#avant-plan plus bas), pour que
les deux calques restent alignés au pixel près quelle que soit la
proportion d'écran.
L'avant-plan étant un nouveau visuel (classeurs redessinés, pas la même
photo), toutes les coordonnées de classeurs/micro/pastille ci-dessous
ont dû être entièrement remesurées dessus (mesure par analyse pixel,
même exigence que d'habitude) — un premier passage, à affiner comme
toujours une fois vu en conditions réelles chez Jean-Marc.
Navigation : 2 flèches posées sur le bureau à droite (voir
#fleche-precedente/#fleche-suivante), 3 fonds codés en dur (décision de
Jean-Marc), pas de mémorisation du fond choisi d'une session à l'autre
(toujours le fond 1 par défaut), sens du glissement lié à la flèche
pressée (le fond n+1 entre par la droite en poussant l'actuel vers la
gauche, et inversement). */
@font-face{
font-family:"Metro DF";
src:url('MetroDF.woff2') format('woff2'),url('MetroDF.ttf') format('truetype');
font-weight:normal;font-style:normal;font-display:swap;
}
*{box-sizing:border-box;}
html,body{
margin:0;padding:0;border:0;width:100%;height:100%;overflow:hidden;
font-family:"Segoe UI","Arial",sans-serif;
}
body{
/* Couleur neutre pour les bandes laissées par "contain" quand la
proportion de l'écran diffère de celle des images (1024×457) — plus
de couleur unique "sampled sur le mur" possible depuis le 8 septembre
2026, chaque fond ayant sa propre teinte de mur. */
background:#2b2b2b;
position:relative;
}
/* Conteneur des fonds interchangeables (8 septembre 2026) : positionné et
dimensionné exactement comme l'était l'image unique auparavant (même
calcul "contain" dans positionner(), voir plus bas), mais désormais un
élément réel plutôt qu'un simple background CSS du body — nécessaire
pour pouvoir faire glisser un fond hors champ pendant qu'un autre entre
(voir changerFond()). overflow:hidden empêche le fond en cours de
transition de déborder pendant son glissement. */
#fonds-container{
position:fixed;
overflow:hidden;
}
/* Un fond = une image plein cadre du conteneur (même ratio 1024×457 que
l'avant-plan, donc pas de letterbox interne : 100%/100% suffit, jamais
de déformation). position:absolute pour que l'ancien et le nouveau
fond puissent coexister le temps du glissement (voir changerFond()). */
.fond-slide{
position:absolute;
inset:0;
background-size:100% 100%;
background-position:center;
background-repeat:no-repeat;
}
/* Avant-plan (classeurs + micro + carnet + téléphone + plateau du bureau) :
identique quel que soit le fond affiché derrière, voir consigne de
Jean-Marc du 8 septembre 2026. Toujours positionné/dimensionné
exactement comme #fonds-container (même calcul dans positionner()),
jamais animé — pointer-events:none pour laisser passer le clic vers les
zones cliquables réelles (classeur-zone, micro-desk, etc.), positionnées
par-dessus au même titre qu'avant. */
#avant-plan{
position:fixed;
width:100%;height:100%;
pointer-events:none;
display:block;
}
/* Flèches de navigation entre fonds (8 septembre 2026) : posées sur le
bureau à droite, taille volontairement modeste ("pas trop grosses",
Jean-Marc) — ARROW_TAILLE_FRAC plus bas dans le script, à ajuster une
fois vu en conditions réelles ("ça va dépendre du rendu", Jean-Marc).
pointer-events déjà "auto" par défaut (bouton), pas besoin de le forcer. */
.fleche-nav{
position:fixed;
border:none;background:transparent;padding:0;margin:0;
cursor:pointer;
display:block;
}
.fleche-nav img{
width:100%;height:100%;display:block;
}
.fleche-nav:hover{
filter:brightness(1.15);
}
/* Étiquettes des 9 classeurs (7 septembre 2026 — évolution de l'essai à
un seul classeur "Contacts" du 2 septembre). Nouvelle photo fournie par
Jean-Marc, mêmes dimensions de référence (1024×457) et même bureau/mur/
micro (vérifié par comparaison pixel à pixel avec l'ancienne image :
seule la zone des classeurs, x≈[40,400], diffère réellement — le reste
n'est que du bruit de compression) — les constantes MICRO_ZONE, ICONE et
ZONE_POSTITS restent donc inchangées plus bas, seules les constantes de
classeurs et la pastille "Contacts" ont été remesurées sur la nouvelle
photo.
Toujours positionnées en JS (voir creerClasseurs()/positionner()) pour
le même motif que le 2 septembre : l'image étant en "contain", sa
taille/position réelles à l'écran changent avec la proportion de la
fenêtre. rotate(-90deg) : lecture bas → haut (on tourne la tête pour
lire), inchangé. Règles génériques (classe, plus un seul id) : il y a
désormais 9 étiquettes/classeurs au lieu d'un seul — voir le tableau
CLASSEURS dans le script. */
.etiquette-classeur{
position:fixed;
transform:translate(-50%,-50%) rotate(-90deg);
transform-origin:center;
white-space:nowrap;
color:#2a2a2a;
font-family:"Segoe UI","Arial",sans-serif;
font-weight:600;
letter-spacing:0.03em;
pointer-events:none;
user-select:none;
}
/* Zone calée sur la tranche visible (face plate, sans le dessus incliné)
de chaque classeur — même principe de positionnement en JS que
l'étiquette ci-dessus. Pas d'habillage visuel par défaut (le classeur
photographié suffit), juste un léger éclaircissement au survol pour
signaler que c'est cliquable, en plus du curseur et de l'infobulle —
UNIQUEMENT sur le classeur réellement branché (voir .classeur-actif
ci-dessous) : les 8 autres n'ont pas encore d'écran derrière (voir
tableau CLASSEURS), pas de survol trompeur pour eux tant que ce n'est
pas le cas.*/
.classeur-zone{
position:fixed;
display:block;
}
.classeur-zone.classeur-actif{
cursor:pointer;
box-shadow:inset 0 0 0 0 rgba(255,255,255,0);
transition:box-shadow .12s ease;
}
.classeur-zone.classeur-actif:hover{
box-shadow:inset 0 0 0 2px rgba(255,255,255,.7);
}
/* Chevalet supprimé le 3 septembre 2026 (voir journal) : Jean-Marc l'a
rangé — "libère de la place sur le bureau" — dans la nouvelle photo qui
ne le montre plus. Le prénom de l'utilisateur reste affiché ailleurs,
dans le groupe société/prénom/marque repris de liste_contacts.html
(voir #statut-version-bloc plus bas), désormais fixé en bas à droite
de l'écran plutôt que calé sur un objet du bureau. */
/* Micro cliquable (2 septembre 2026, essai — retouché le même jour après
retour de Jean-Marc). Deux éléments distincts, superposés mais avec des
rôles différents :
- #micro-desk : la zone cliquable/survolable RÉELLE, calée sur tout le
silhouette du micro (pied, tige, corps, tête) — pas seulement la
petite tête. Corrige le souci remonté : l'infobulle ne se déclenchait
qu'en survolant précisément la petite grille du haut, zone trop
étroite pour être fiable à la souris. Invisible (pas de fond), juste
le curseur + l'infobulle (title) + le clic.
- #micro-icone : uniquement l'icône elle-même, petite, calée sur le
rond noir de la tête (voir positionner()). pointer-events:none pour
laisser passer le clic/survol vers #micro-desk en dessous. Le léger
liseré au survol (auparavant sur le bouton) est maintenant affiché
ici via le sélecteur de fratrie ~ quand #micro-desk est survolé, pour
rester visuellement au niveau de la tête plutôt qu'engober tout le
micro d'un cadre. Icônes redessinées en SVG à partir de
mic_repos.png / mic_ecoute.png (déjà utilisées pour le bouton micro
du bandeau, voir micro.js) — recolorées, sans le disque rouge
d'origine, pour se fondre dans la grille plutôt que ressortir comme
un autocollant. Fermé = glyphe micro en gris clair ; ouvert = barres
façon vu-mètre en --rouge-micro, animées (voir @keyframes) — "les
barres qui bougent" demandé par Jean-Marc.
Branché sur la reconnaissance vocale réelle le 3 septembre 2026 :
un script pilote désormais #micro-desk et bascule la classe "ecoute"
dessus (voir plus bas #micro-desk.ecoute ~ #micro-icone pour la
bascule visuelle des deux glyphes, via un combinateur de fratrie
puisque #micro-icone n'est pas un descendant de #micro-desk — voir
plus haut pourquoi ces deux éléments sont séparés). Le toggle JS
local qui existait ici pour le tout premier essai d'affichage (pur
visuel, sans reconnaissance vocale) est retiré.
Mise à jour du même jour (suite de session) : ce micro est
temporairement détourné de son usage final. Plutôt que micro.js (le
vrai pilotage, agents nommés Viktor/Matilda/Léon/Nikita, mode
notation — resté inchangé et toujours utilisé tel quel sur
liste_contacts.html), c'est d'abord micro_test.js qui a été chargé
ici, le temps de la phase de constitution d'une banque de dictées
réelles (table ASP_DicteesTest, thème "Gestion des Contacts").
4 septembre 2026 (soir) : cette phase de collecte a rempli son rôle
(décision de Jean-Marc, "l'ancien panneau ne nous sert plus à rien")
— micro_test.js est retiré d'ici (le fichier reste en place, juste
plus chargé) au profit de contact_ia_panel.js, qui branche sur ce
même bouton la première interprétation réelle d'une dictée de
contact par LLM (voir contact_ia_interprete.php). Toujours un outil
de mise au point, pas un module utilisateur final : rien n'est créé
en base contact/profession réels, seulement enregistré pour analyse
(voir contact_ia_valide.php). Pour reconnecter le vrai pilotage sur
ce micro une fois cette phase terminée : remettre
<script src="micro.js"> à la place de
<script src="contact_ia_panel.js"> en bas de cette page, rien
d'autre à défaire. */
#micro-desk{
position:fixed;
border:none;background:transparent;padding:0;margin:0;
cursor:pointer;
}
#micro-icone{
position:fixed;
display:flex;align-items:center;justify-content:center;
border-radius:50%;
pointer-events:none;
box-shadow:0 0 0 0 rgba(255,255,255,0);
transition:box-shadow .12s ease;
}
#micro-desk:hover ~ #micro-icone{
box-shadow:0 0 0 3px rgba(255,255,255,.3);
}
#micro-icone svg{
width:70%;height:70%;display:block;
}
#micro-icone svg[hidden]{display:none;}
/* Bascule repos/écoute pilotée par la classe "ecoute" que le script chargé
en bas de page ajoute sur #micro-desk (contact_ia_panel.js actuellement —
voir commentaire plus haut sur le détournement temporaire de ce micro ; même
mécanisme que #bouton-micro.ecoute dans liste_contacts.html avec
micro.js) — combinateur de fratrie ~ nécessaire ici puisque
#micro-icone n'est pas un descendant du bouton. Spécificité (3 id + 1
classe) volontairement plus élevée que la règle [hidden] ci-dessus pour
la surclasser sans !important, l'attribut hidden restant présent dans
le HTML de #glyphe-ecoute (jamais modifié en JS ici désormais). */
#micro-desk.ecoute ~ #micro-icone #glyphe-repos{display:none;}
#micro-desk.ecoute ~ #micro-icone #glyphe-ecoute{display:block;}
#glyphe-repos{
fill:#d9d9d3;
opacity:.92;
}
#glyphe-ecoute{
fill:#e14a5c; /* proche de --rouge-micro (#D9364B) de liste_contacts.html */
}
#glyphe-ecoute .barre{
transform-box:fill-box;
transform-origin:50% 100%;
animation:pulse-barre 1s ease-in-out infinite;
}
#glyphe-ecoute .barre:nth-child(1){ animation-delay:-.6s; }
#glyphe-ecoute .barre:nth-child(2){ animation-delay:-.3s; }
#glyphe-ecoute .barre:nth-child(3){ animation-delay:0s; }
#glyphe-ecoute .barre:nth-child(4){ animation-delay:-.45s; }
@keyframes pulse-barre{
0%,100%{ transform:scaleY(.55); }
50%{ transform:scaleY(1); }
}
/* Numéro de version : voir #statut-version-bloc plus haut — depuis le
3 septembre 2026, fixé en bas à droite de l'écran (même place et même
habillage que dans liste_contacts.html) plutôt que calé sur le bureau. */
/* Pastille "nombre de contacts" sur le classeur Contacts (2 septembre
2026, essai). Idée de Jean-Marc : au fur et à mesure que chaque classeur
sera branché à sa table, afficher une petite pastille collée dessous le
trou cerclé de métal, avec un chiffre — pour donner une indication
immédiate du contenu sans avoir à ouvrir le classeur. Couleur de fond
mesurée sur ecran_fond.jpg (teinte réelle du classeur "Contacts",
rgb≈3,79,99, plus sombre en photo qu'à l'écran) puis assombrie d'un
facteur ~0.55 pour que le chiffre blanc ressorte sans jurer avec le
classeur — pas une couleur arbitraire. Liseré blanc autour de la
pastille (2 fichiers/gris clair, cohérent avec le liseré déjà utilisé
sur les étiquettes de tous les classeurs sur la photo) + légère ombre
portée pour la détacher visuellement, plutôt qu'un anneau de couleur :
Jean-Marc n'avait pas de préférence tranchée sur la couleur de l'anneau,
le blanc a été choisi pour rester cohérent avec l'habillage existant
(à changer facilement si Jean-Marc préfère une autre couleur après test
réel). pointer-events:none : la pastille est purement informative, le
clic doit continuer à passer à travers vers #classeur-contacts en
dessous (même logique que #micro-icone au-dessus de #micro-desk). Le
pattern (variables IMG, positionnement dans positionner(), chargement
via un endpoint réel) est pensé pour être répliqué sur les prochains
classeurs au fur et à mesure de leur branchement — voir commentaire de
PASTILLE_CONTACTS_* plus bas dans le script. Retouchée le même jour
après premier test réel de Jean-Marc ("trop présente, liseré trop
pétant") : diamètre réduit (22px de référence → 17px) et liseré blanc
assoupli (opacité .85→.4, épaisseur 2px→1.5px) pour rester une
indication discrète plutôt qu'un élément qui attire l'œil en premier. */
#pastille-contacts{
position:fixed;
border-radius:50%;
background:#022b36;
box-shadow:0 0 0 1.5px rgba(255,255,255,.4), 0 1px 2px rgba(0,0,0,.45);
display:flex;align-items:center;justify-content:center;
pointer-events:none;
user-select:none;
}
#pastille-contacts span{
color:#fff;
font-family:"Segoe UI","Arial",sans-serif;
font-weight:700;
line-height:1;
}
/* Bloc société + prénom + marque (3 septembre 2026, voir journal). Posé
d'abord calé sur le bureau comme le reste de cet écran, puis Jean-Marc a
précisé vouloir "les infos en bas à droite à la même place que dans la
liste des contacts" — repris ici en position FIXE par rapport à l'écran
(et non plus en fraction de l'image), avec exactement les mêmes id, la
même mise en forme et les mêmes tailles qu'en bas de liste_contacts.html
(pixels fixes, pas de recalcul dans positionner()) : société, point
séparateur, prénom, point séparateur, puis #marque-aspic (icône Mévélia
+ logo ASPIC miniature + version). */
#statut-version-bloc{
position:fixed;
right:16px;
bottom:10px;
display:flex;align-items:center;gap:8px;
}
.point-separateur{
font-size:14px;line-height:1;color:#2a2a2a;
}
#societe-utilisateur, #prenom-utilisateur{
font-size:11px;font-weight:700;color:#2a2a2a;
font-family:"Segoe UI","Arial",sans-serif;
white-space:nowrap;
}
#marque-aspic{
display:flex;align-items:center;gap:4px;
}
#icone-mevelia{
width:16px;height:16px;
border-radius:50%;
display:block;
}
#logo-aspic-mini{
display:flex;align-items:center;
background:#1B1B1B;
border-radius:4px;
padding:2px 6px;
font-family:"Metro DF","Segoe UI Black","Segoe UI Semibold","Segoe UI",Arial,sans-serif;
font-weight:900;font-size:9px;letter-spacing:1px;
}
#logo-aspic-mini .A{color:#FF5A1F;}
#logo-aspic-mini .rest{color:#FFFFFF;}
#statut-version{
color:#4a4a4a;
font-family:"Segoe UI","Arial",sans-serif;
font-weight:700;
font-size:11px;
}
/* Bouton "BO" (11 septembre 2026) : n'apparaît que pour un utilisateur
avec MIR_BO='O' (voir chargerIdentiteUtilisateur(), preferences.php) —
seul accès au BackOffice depuis la CRM, jamais d'URL à taper à la main.
hidden par défaut, retiré par JS uniquement si l'accès est accordé. */
#bouton-bo{
font:inherit;font-family:"Segoe UI","Arial",sans-serif;font-weight:700;
font-size:10px;background:#1B1B1B;color:#f2d94e;border:1px solid #444;
border-radius:4px;padding:3px 8px;cursor:pointer;
}
#bouton-bo[hidden]{display:none;}
/* Post-its de la ToDoList personnelle (2 septembre 2026, premier
affichage réel — voir journal section 28). Posés dans la zone décrite
par ZONE_POSTITS_* plus bas dans le script : une bande occupant 30% de
la largeur de l'écran depuis le bord droit, jusqu'à la ligne du bureau
(chevalet supprimé le 3 septembre 2026, voir journal — la zone descend
désormais jusqu'à la nouvelle limite mesurée sur la photo, avec la même
marge de sécurité qu'avant) — décision de Jean-Marc ("le vilain petit canard sera l'humain", donc
plus de place que chaque assistant, dont la ToDoList n'est pas encore
gérée — voir Réflexions stratégiques section 11 et le fil du 2-3
septembre). Grille à 4 colonnes × 5 lignes = 20 cases, volontairement
alignée sur CAPACITE_TABLEAU_PAR_UTILISATEUR (todolist.php) pour cette
première version — les deux devront évoluer ensemble si la capacité
change. Chaque post-it reçoit une légère rotation/décalage à
l'intérieur de sa propre case (jamais à cheval sur la case voisine,
donc jamais de chevauchement ce soir — Jean-Marc a autorisé le
chevauchement partiel mais a refusé l'empilement en tas ; rester dans
sa case est la façon la plus simple de garantir qu'on ne recrée jamais
ce tas). Décalage/rotation calculés de façon déterministe à partir de
TDL_Incr (voir hashDeterministe() dans le script) : la disposition ne
"saute" pas à chaque rechargement de l'écran.
Couleur jaune classique pour l'utilisateur (--postit-jaune) — les
couleurs par assistant restent à faire le jour où leur ToDoList sera
gérée (AGN_Couleur, voir Réflexions stratégiques section 11).
Punaise ronde en haut (photo confirmée comme du vrai liège par
Jean-Marc le 2 septembre) plutôt qu'un simple post-it posé — cohérent
avec la matière réelle du fond d'écran. Pastille d'urgence toujours
dans le coin haut-droit, jamais recouverte par du jitter (voir calcul
de bornes dans positionnerPostits()) : c'est le repère visuel ET la
garantie d'une zone de clic non ambiguë même si deux post-its finissent
visuellement proches. Pas de texte sur le post-it (choix explicite de
Jean-Marc) : le titre de la tâche n'apparaît qu'au survol (title natif),
le clic ne fait rien pour l'instant (voir journal — navigation vers
liste_todolist.html volontairement pas encore branchée).
Attachés au fond liège depuis le 8 septembre 2026 (retour de Jean-Marc :
"ils seront attachés au panneau de liège, ils partiront avec lui") : le
conteneur #postits-layer est déplacé (au sens JS, pas dupliqué) à
l'intérieur de la slide du fond liège (index 0) à chaque fois qu'elle
existe (voir creerFondSlide()/changerFond() dans le script) — les
post-its glissent donc avec ce fond précis pendant la transition, et
restent invisibles (hors DOM visible, simplement parqués dans une slide
retirée) tant qu'un autre fond est affiché. position:absolute (plus
fixed) car désormais positionnés par rapport à ce conteneur local, pas
par rapport à la fenêtre — voir positionner() (zoneLeft/zoneTop ne
rajoutent plus offX/offY). */
#postits-layer{
position:absolute;
inset:0;
}
.postit{
position:absolute;
box-shadow:2px 4px 8px rgba(0,0,0,.35);
background:
linear-gradient(135deg, transparent 50%, rgba(0,0,0,.12) 50%, rgba(0,0,0,.12) 62%, transparent 62%) bottom right / 22% 22% no-repeat,
var(--couleur-postit, #fbe14a);
cursor:default;
}
.postit::before{
content:'';
position:absolute;
top:-9px; left:50%;
transform:translateX(-50%);
width:13%; height:13%;
min-width:8px; min-height:8px;
border-radius:50%;
background:radial-gradient(circle at 35% 30%, #fff8, transparent 40%), #8a3b12;
box-shadow:0 2px 3px rgba(0,0,0,.5);
}
.postit .pastille-urgence-postit{
position:absolute;
top:-15%; right:-15%;
width:32%; height:32%;
border-radius:50%;
box-shadow:0 0 0 2px rgba(255,255,255,.9), 0 1px 2px rgba(0,0,0,.4);
}
.postit .pastille-urgence-postit.rouge{ background:#c0392b; }
.postit .pastille-urgence-postit.orange{ background:#ff5a1f; }
/* Portraits des assistants, écran 2 (9 septembre 2026 — voir journal).
Même principe d'attachement que #postits-layer pour le fond liège :
#assistants-layer est déplacé (au sens JS) à l'intérieur de la slide du
fond "tasseaux gris" (index 1) à chaque fois qu'elle existe, voir
creerFondSlide()/changerFond() plus bas — invisible tant qu'un autre
fond est affiché, sans dupliquer sa logique de position. position:absolute
(pas fixed) : positionné par rapport à ce conteneur local (lui-même
inset:0 dans la slide, elle-même calée sur le rectangle "contain"), pas
par rapport à la fenêtre — voir positionner().
Nombre de colonnes non codé en dur (décision de Jean-Marc, 9 septembre
2026) : une carte par agent réellement renvoyé par agents.php, largeur
de colonne = largeur totale / nombre d'agents — s'adapte donc tout seul
si un agent est ajouté ou retiré, sans repasser par ce fichier. */
#assistants-layer{
position:absolute;
inset:0;
}
.assistant-carte{
position:absolute;
display:flex;
flex-direction:column;
align-items:center;
pointer-events:none; /* purement visuel pour l'instant, comme les post-its */
}
.assistant-cadre{
background:#fff;
box-shadow:2px 4px 8px rgba(0,0,0,.35);
box-sizing:border-box;
display:block;
}
.assistant-photo{
display:block;
width:100%;
height:100%;
object-fit:cover;
}
/* Repli visuel pour un agent sans AGN_Photo renseignée (ex. le temps que
Jean-Marc dépose la photo) : même esprit que le "?" utilisé dans les
essais visuels, jamais un cadre vide sans explication. */
.assistant-photo-repli{
display:flex;align-items:center;justify-content:center;
width:100%;height:100%;
background:#e0e0dc;
color:#9a9a95;
font-weight:700;
font-family:"Segoe UI","Arial",sans-serif;
}
.assistant-nom{
margin-top:6px;
font-weight:700;
color:#2a2a2a;
font-family:"Segoe UI","Arial",sans-serif;
white-space:nowrap;
line-height:1.15;
}
.assistant-fonction{
color:#3a3a3a;
font-weight:700; /* forcé en gras le 9 septembre 2026 (retour Jean-Marc) : la ligne passait inaperçue sur le fond tasseaux */
font-family:"Segoe UI","Arial",sans-serif;
white-space:nowrap;
line-height:1.15;
}
/* Panneau "Bureau des assistants" (9 septembre 2026) : même esprit que les
cadres photo (liseré blanc, ombre portée) mais fond gris plutôt que
blanc — posé une seule fois (pas par agent) dans l'espace qui reste
libre sous les 2 dernières colonnes (aujourd'hui Léon et Sophia, voir
positionner()), tant qu'il n'y a rien d'autre à y afficher. */
.bureau-panneau{
position:absolute;
box-sizing:border-box;
background:#cfcfc9;
border-style:solid;
border-color:#fff;
box-shadow:2px 4px 8px rgba(0,0,0,.35);
display:flex;
align-items:center;
justify-content:center;
text-align:center;
color:#2a2a2a;
font-weight:700;
font-family:"Segoe UI","Arial",sans-serif;
pointer-events:none;
}
</style>
</head>
<body>
<!-- Fonds interchangeables + avant-plan fixe (8 septembre 2026, voir
commentaire d'en-tête). #fonds-container reste vide ici : rempli par
initialiserFonds() au chargement (1er fond) puis par changerFond() à
chaque clic sur une flèche. -->
<div id="fonds-container"></div>
<img id="avant-plan" src="avant_plan_bureau.png" alt="">
<!-- Les 9 zones cliquables + étiquettes de classeurs (7 septembre 2026) sont
créées par creerClasseurs() (voir script) à partir du tableau CLASSEURS,
sur le même principe que les post-its plus bas : éviter de dupliquer 9
fois à la main un bloc HTML quasi identique. -->
<button id="micro-desk" type="button" title="Le micro est inactif"></button>
<div id="micro-icone">
<svg id="glyphe-repos" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>
<svg id="glyphe-ecoute" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" hidden>
<rect class="barre" x="2" y="14" width="3.5" height="7" rx="1"/>
<rect class="barre" x="8" y="9" width="3.5" height="12" rx="1"/>
<rect class="barre" x="14" y="4" width="3.5" height="17" rx="1"/>
<rect class="barre" x="20" y="11" width="3.5" height="10" rx="1"/>
</svg>
</div>
<div id="pastille-contacts"><span id="pastille-contacts-nombre">…</span></div>
<div id="statut-version-bloc">
<span id="societe-utilisateur" hidden></span>
<span class="point-separateur" id="point-societe" hidden aria-hidden="true">&#8226;</span>
<span id="prenom-utilisateur"></span>
<span class="point-separateur" aria-hidden="true">&#8226;</span>
<div id="marque-aspic" title="ASPIC
Créé par Mevelia Solutions">
<img id="icone-mevelia" src="mevelia-icone.png" alt="Mévélia Solutions">
<span id="logo-aspic-mini"><span class="A">A</span><span class="rest">SPIC</span></span>
<span id="statut-version"></span>
</div>
<button id="bouton-bo" type="button" hidden title="Ouvrir le BackOffice">BO</button>
</div>
<!-- Flèches de navigation entre fonds (8 septembre 2026) : posées sur le
bureau à droite (consigne de Jean-Marc), après tous les autres éléments
fixes pour rester cliquables au-dessus de tout (ordre du document =
ordre d'empilement, aucun z-index explicite ailleurs sur cette page). -->
<button id="fleche-precedente" class="fleche-nav" type="button" title="Écran précédent">
<img src="fleche_precedente.png" alt="Écran précédent">
</button>
<button id="fleche-suivante" class="fleche-nav" type="button" title="Écran suivant">
<img src="fleche_suivante.png" alt="Écran suivant">
</button>
<script>
// Référence de l'image : 1024×457, inchangée en dimensions (voir
// ecran_fond.jpg — 3ᵉ version le 3 septembre 2026, Jean-Marc a raccourci
// le bureau de 13px en bas de la photo et ajouté 13px de liège en haut,
// chevalet retiré). Toutes les coordonnées ci-dessous sont exprimées en
// pixels de CETTE image de référence, puis converties en fraction pour
// rester calées quelle que soit la taille réelle d'affichage (voir calcul
// dans positionner()). Constantes remesurées directement sur la nouvelle
// photo (comparaison pixel à pixel avec l'ancienne, tranche du classeur et
// silhouette du micro) : décalage vertical uniforme de +13 confirmé pour
// tout ce qui était calé sur le bureau ou le mur, aucun décalage
// horizontal — cohérent avec la description de Jean-Marc.
// 4ᵉ version le 7 septembre 2026 : Jean-Marc a retravaillé l'image pour
// passer d'un unique classeur "Contacts" à une rangée de 9 classeurs
// (mêmes dimensions 1024×457, même bureau/mur/micro/carnet/téléphone,
// vérifié par différence pixel à pixel entre les deux photos — seule la
// zone des classeurs, x≈[40,400], a réellement changé). Constantes de
// classeurs et pastille "Contacts" remesurées ci-dessous sur cette
// nouvelle version ; MICRO_ZONE_*/ICONE_*/ZONE_POSTITS_* inchangées.
const IMG_W = 1024, IMG_H = 457;
// -------- Classeurs (7 septembre 2026 — 9 classeurs remplacent l'unique
// classeur "Contacts" du 2 septembre) --------
// Nouvelle photo fournie par Jean-Marc, mêmes dimensions de référence
// 1024×457. Toutes les coordonnées ci-dessous ont été mesurées directement
// sur cette nouvelle photo par analyse pixel par pixel (relevé des
// transitions de couleur colonne par colonne / ligne par ligne), pas à
// l'œil — même exigence de précision que les mesures manuelles de
// Jean-Marc sur les versions précédentes.
//
// CLASSEURS_Y1/Y2 : rectangle vertical de la tranche visible, commun aux
// 9 classeurs (mesuré quasi identique sur chacun, 230-233 en haut et
// 409-411 en bas — la photo montre les 9 classeurs à la même hauteur).
// Remesuré le 8 septembre 2026 sur le nouvel avant-plan (avant_plan_bureau.png,
// classeurs redessinés — voir commentaire d'en-tête) : le bas passe de 410
// à 371 (ligne du bureau plus haute sur ce nouveau visuel, vérifiée sur
// toute la largeur de l'image — 100% des pixels opaques au-delà, contre
// 0% avant), et le haut de 231 à 236 (bas du petit "toit" incliné du
// classeur, mesuré par analyse pixel comme d'habitude).
// Remesuré une 2ᵉ fois le 8 septembre 2026 (soir) : Jean-Marc a recadré
// l'avant-plan (traces blanches sur le fond retirées, micro nettoyé), ce
// qui a décalé toute la composition d'environ 20px vers le bas — mêmes
// abscisses (vérifié par comparaison des silhouettes), toutes les
// ordonnées remesurées directement sur le nouveau fichier.
const CLASSEURS_Y1 = 257, CLASSEURS_Y2 = 393;
// Rectangle vertical utile de la partie blanche de l'étiquette (mesuré
// constant à 249-333 sur les 9 classeurs du nouvel avant-plan, 8 septembre
// 2026) : c'est cette hauteur, une fois convertie à l'écran, qui borne la
// largeur du texte tourné à -90° (voir calculerPoliceEtiquette() plus bas).
// Remesuré le 8 septembre 2026 (soir), même recadrage que ci-dessus.
const ETIQ_LABEL_Y1 = 269, ETIQ_LABEL_Y2 = 353;
const ETIQ_CENTRE_Y_FRAC = ((ETIQ_LABEL_Y1 + ETIQ_LABEL_Y2) / 2) / IMG_H;
// Largeur utile de la partie blanche de l'étiquette (mesurée 22-25px selon
// le classeur, valeur basse retenue par sécurité — cette largeur borne
// l'ÉPAISSEUR du texte tourné, donc indirectement la taille de police
// maximale des libellés courts, voir calculerPoliceEtiquette() plus bas).
// Remesuré le 8 septembre 2026 sur le nouvel avant-plan (22-26px selon le
// classeur) : la même valeur basse (22) reste le bon choix par sécurité.
const ETIQ_LABEL_LARGEUR = 22;
// Marges de sécurité (10-6%) sur les deux contraintes ci-dessus. Plus
// resserrées que la précédente version (12%) : la taille de police n'est
// plus estimée à partir d'un ratio de largeur de caractère deviné, mais
// mesurée pour de vrai par le navigateur lui-même (voir
// calculerPoliceEtiquette()) — la marge ne sert donc plus qu'à absorber
// l'écart entre la police réellement utilisée chez Jean-Marc (Segoe UI,
// Windows) et celle du moteur qui affiche la page au moment du calcul,
// jamais à compenser une approximation de largeur de texte.
const ETIQ_MARGE_HAUTEUR = 0.94;
const ETIQ_MARGE_LARGEUR = 0.90;
// Facteur hauteur de ligne approximatif (line-height non fixé en CSS ici,
// donc dépendant du navigateur — 1.2 est une valeur usuelle prudente) :
// c'est cette épaisseur de ligne, pas la taille de police elle-même, qui
// doit tenir dans ETIQ_LABEL_LARGEUR une fois le texte tourné à -90°.
const ETIQ_LIGNE_HAUTEUR = 1.2;
// Seuil plancher, en dessous duquel le texte deviendrait illisible quel
// que soit le libellé (aucun classeur actuel ne devrait l'atteindre).
const ETIQ_POLICE_MIN_FRAC = 9 / IMG_W;
// Plafond commun aux libellés courts : épaisseur de ligne maximale qui
// tient dans la largeur physique de l'étiquette (2ᵉ contrainte de
// calculerPoliceEtiquette() ci-dessous, extraite ici en constante
// partagée) — c'est la taille qu'atteignent "Contacts", "Catalogue", "En
// cours" ou "Factures" une fois que la contrainte de hauteur cesse d'être
// la plus stricte des deux. Sert aussi de police forcée pour "Documents"
// (policeForceeFrac dans CLASSEURS, voir plus bas) le 7 septembre 2026
// (soir).
const ETIQ_POLICE_CAP_FRAC = (ETIQ_LABEL_LARGEUR * ETIQ_MARGE_LARGEUR) / ETIQ_LIGNE_HAUTEUR / IMG_W;
// Tableau des 9 classeurs, dans l'ordre demandé par Jean-Marc le
// 7 septembre 2026 (photo retravaillée : le 1er classeur a glissé vers la
// gauche et devient "Consignes", "Contacts" passe en 2e position).
// x1/x2 : bornes horizontales de la tranche visible (classeur entier, y
// compris le bord en plastique coloré autour de l'étiquette) — définissent
// la zone cliquable/survolable. labelCx : centre horizontal mesuré du
// RECTANGLE BLANC de l'étiquette lui-même (pas du classeur entier) —
// retouché le 7 septembre 2026 après retour de Jean-Marc ("Consignes" trop
// à droite, "Contacts"/"Catalogue" à décaler vers la gauche) : l'étiquette
// n'est pas toujours parfaitement centrée sur la tranche du classeur,
// mieux vaut mesurer sa propre position que supposer qu'elle coïncide avec
// le centre de la tranche. Re-décalé une seconde fois le 7 septembre 2026
// (soir) pour "Consignes" seul ("presque parfait", encore un peu trop à
// droite) : -2px supplémentaires. href : écran réel derrière le classeur,
// ou null quand cet écran n'existe pas encore — seul un classeur avec href
// reçoit la classe "classeur-actif" (survol + clic, voir creerClasseurs()) ;
// les 8 autres affichent leur étiquette mais ne sont pas encore cliquables,
// faute d'écran à ouvrir (voir le journal pour la liste des écrans
// restant à construire). policeForceeFrac : taille de police imposée (en
// fraction de IMG_W), au lieu de la double contrainte calculée par
// calculerPoliceEtiquette() — voir commentaire sur "Documents" ci-dessous
// pour le seul cas qui l'utilise pour l'instant.
// Remesuré intégralement le 8 septembre 2026 sur le nouvel avant-plan
// (avant_plan_bureau.png — 9 classeurs redessinés par Jean-Marc, mêmes
// libellés/ordre/href que la version précédente, seules les coordonnées
// changent). x1/x2 = limite de séparation entre classeurs (point le plus
// sombre mesuré dans l'intervalle entre deux étiquettes, pas juste le
// milieu géométrique — même principe que le 7 septembre). labelCx = centre
// du rectangle blanc de l'étiquette, mesuré indépendamment (les 9
// rectangles ne sont pas exactement centrés dans leur classeur).
// Nouvelle répartition (9 septembre 2026, réflexion stratégique de
// Jean-Marc — regroupement plus cohérent des rubriques, ex. Prospection
// fusionnée dans Contacts, En-cours+Journalier scindés différemment en
// Missions/Propositions/Livraisons/Journalier). Reprend exactement les 9
// positions physiques déjà mesurées sur l'avant-plan (x1/x2/labelCx
// inchangés, dans l'ordre des classeurs sur la photo) : seuls les
// libellés/ids/href changent, aucune nouvelle mesure de pixels nécessaire
// tant que l'avant-plan lui-même ne change pas.
const CLASSEURS = [
{id:'catalogue', label:'Catalogue', x1:44, x2:83, labelCx:63.5, href:null},
{id:'documents', label:'Documents', x1:83, x2:120, labelCx:101.0, href:null},
{id:'contacts', label:'Contacts', x1:120, x2:158, labelCx:138.5, href:'liste_contacts.html'},
{id:'missions', label:'Missions', x1:158, x2:197, labelCx:177.0, href:null},
{id:'propositions', label:'Propositions', x1:197, x2:239, labelCx:216.0, href:null},
{id:'livraisons', label:'Livraisons', x1:239, x2:280, labelCx:259.0, href:null},
{id:'factures', label:'Factures', x1:280, x2:321, labelCx:300.5, href:null},
{id:'journalier', label:'Journalier', x1:321, x2:363, labelCx:341.5, href:null},
{id:'gestion', label:'Gestion', x1:363, x2:415, labelCx:383.5, href:null},
];
// Zone cliquable/survolable du micro et centre de l'icône : remesurés le
// 8 septembre 2026 sur le nouvel avant-plan — micro d'un tout autre style
// (corps arrondi de studio, plus de petite grille ronde isolée en haut
// comme sur l'ancienne photo) : zone calée sur la silhouette entière
// (mesure directe des pixels non transparents), icône centrée sur le
// reflet du corps, repère visuel le plus naturel sur ce nouveau micro.
// Y1/Y2/icône remesurés le 8 septembre 2026 (soir), même recadrage
// (traces blanches/halo retirés autour du micro par Jean-Marc) — X
// inchangés (silhouette identique, seule la position verticale a bougé).
const MICRO_ZONE_X1 = 478, MICRO_ZONE_X2 = 549;
const MICRO_ZONE_Y1 = 271, MICRO_ZONE_Y2 = 393;
// X ramené vers la gauche le 8 septembre 2026 (retour de Jean-Marc après
// essai réel : le décalage à droite du 8 septembre précédent allait trop
// loin) — Y inchangé (remonté juste avant, jugé bon depuis).
const ICONE_CENTRE_X = 515, ICONE_CENTRE_Y = 288;
const ICONE_TAILLE_FRAC = 22 / IMG_W;
// Pastille "nombre de contacts" — remesurée le 8 septembre 2026 sur le
// classeur Contacts du nouvel avant-plan (2ᵉ classeur, labelCx=101).
// Trou de préhension nettement plus bas et plus proche du bureau que sur
// l'ancienne photo (quasiment au ras de CLASSEURS_Y2=371, sans place pour
// la caler "en dessous" comme avant) : posée à la place juste au-dessus du
// trou, sous l'étiquette (ETIQ_LABEL_Y2=333) — premier essai, à revoir
// avec Jean-Marc après un rendu réel si trop proche du trou ou du texte.
// Y remesuré le 8 septembre 2026 (soir), même recadrage.
// Redescendue un peu le 8 septembre 2026 (soir), retour de Jean-Marc
// après 6ᵉ test réel (trop remontée la fois précédente) : 401 → 404.
// X déplacé le 9 septembre 2026 : "Contacts" passe du 2ᵉ au 3ᵉ classeur
// dans la nouvelle répartition (voir tableau CLASSEURS) — X reprend le
// labelCx du nouvel emplacement (138.5) pour que la pastille reste sous
// le bon classeur ; Y inchangé (même hauteur de trou sur les 9 classeurs).
const PASTILLE_CONTACTS_CENTRE_X = 138.5, PASTILLE_CONTACTS_CENTRE_Y = 404;
const PASTILLE_CONTACTS_DIAM_FRAC = 17 / IMG_W;
// Zone des post-its personnels (2 septembre 2026, premier affichage réel
// — voir commentaire CSS .postit ci-dessus pour le détail des décisions).
// 30% de la largeur de l'écran depuis le bord droit (1024 × 0.30 ≈ 307).
// Bas de la zone (Y2) recalculé le 3 septembre 2026 : n'était plus borné
// par le chevalet (supprimé) mais par la nouvelle ligne de bureau
// (CLASSEURS_Y2 = 410, ex-CLASSEUR_Y2 avant le passage à 9 classeurs le
// 7 septembre — valeur inchangée), avec la même marge de sécurité qu'avant
// (~15px) — 352 devient 395, quelques pixels de plus qu'un simple +13
// puisque l'espace se libère réellement, comme relevé par Jean-Marc. Grille
// 4 colonnes × 5 lignes = 20 cases, volontairement alignée sur
// CAPACITE_TABLEAU_PAR_UTILISATEUR (todolist.php) — à faire évoluer
// ensemble le jour où cette capacité changera.
// Y2 remesuré le 8 septembre 2026 (nouvel avant-plan, CLASSEURS_Y2=371 —
// voir plus haut) : même marge de sécurité qu'avant l'utilisant.
// Y2 remesuré le 8 septembre 2026 (soir), même recadrage (CLASSEURS_Y2
// passé à 393) — même marge de sécurité qu'avant l'utilisant.
const ZONE_POSTITS_X1 = 717, ZONE_POSTITS_X2 = 1024;
const ZONE_POSTITS_Y1 = 6, ZONE_POSTITS_Y2 = 378;
const GRILLE_POSTITS_COLONNES = 4, GRILLE_POSTITS_LIGNES = 5;
// Taille d'un post-it : ~60% de la plus petite dimension de case, pour
// laisser de la marge au jitter sans jamais déborder sur la case voisine
// (voir positionnerPostits()).
const POSTIT_RATIO_CASE = 0.6;
// -------- Flèches de navigation entre fonds (8 septembre 2026) --------
// Posées sur le bureau à droite (consigne de Jean-Marc), dans la zone
// vide du plateau au-delà du carnet/téléphone (x>668). Taille volontairement
// modeste ("pas trop grosses") — premier réglage, Jean-Marc a prévenu que
// la taille définitive "va dépendre du rendu" réel chez lui.
// Retouché le 8 septembre 2026 (soir), retour de Jean-Marc après premier
// test réel : remontées (trop près du bas), rapprochées l'une de l'autre,
// et collées plus à droite.
// Réduite d'environ 30% le 8 septembre 2026 (soir), retour de Jean-Marc
// après second test réel (34 → 24).
const FLECHE_TAILLE_FRAC = 24 / IMG_W;
// Flèche précédente réécartée un peu le 8 septembre 2026 (soir), retour
// de Jean-Marc après 4ᵉ test réel ("trop proches l'une de l'autre").
const FLECHE_PRECEDENTE_CENTRE_X = 962, FLECHE_CENTRE_Y = 410;
const FLECHE_SUIVANTE_CENTRE_X = 998;
// -------- Portraits des assistants, écran 2 (9 septembre 2026) --------
// Réglages issus des essais visuels validés par Jean-Marc (voir journal) :
// marge réduite au-dessus de la tête, visage recentré, cadrage descendant
// sur le buste, taille réduite de 15% par rapport au tout premier essai.
// ASSISTANT_PHOTO_RATIO doit rester cohérent avec le recadrage utilisé pour
// produire les fichiers assistant_<prenom>.jpg (outillage côté Claude) — à
// revoir ensemble si Jean-Marc fournit un jour des photos au format déjà
// différent.
const ASSISTANT_ZONE_TOP = 14; // marge haute, px de référence (IMG_H)
const ASSISTANT_PHOTO_H_MAX = 162; // hauteur max de la photo (hors liseré), px de référence
const ASSISTANT_PHOTO_RATIO = 0.657; // largeur / hauteur de la photo
const ASSISTANT_MAT = 5; // épaisseur du liseré blanc, px de référence
const ASSISTANT_COL_MARGE = 0.85; // fraction de la largeur de colonne réellement utilisable (reste = gouttière)
const ASSISTANT_NOM_POLICE_FRAC = 17 / IMG_W;
const ASSISTANT_FONCTION_POLICE_FRAC = 12 / IMG_W;
// Panneau "Bureau des assistants" (9 septembre 2026, retour Jean-Marc) :
// posé dans l'espace qui reste libre sous les 2 dernières colonnes de
// portraits (aujourd'hui Léon et Sophia), entre le bas du texte des
// cartes et le haut du plateau du bureau. BUREAU_PANNEAU_BAS reprend la
// même limite que CLASSEURS_Y2/MICRO_ZONE_Y2 plus haut (haut du plateau,
// commun à toute la largeur de l'image) avec la même petite marge de
// sécurité que ZONE_POSTITS_Y2.
const BUREAU_PANNEAU_BAS = 390; // px de référence (IMG_H)
const BUREAU_PANNEAU_GAP = 8; // espace entre le texte des cartes et le panneau, px de référence
const BUREAU_PANNEAU_POLICE_FRAC = 16 / IMG_W;
// Le panneau est un encadré ajusté au texte (mesuré, voir positionner()),
// pas une zone qui occupe tout l'espace disponible — cet espace ne sert
// qu'à centrer l'encadré (retour Jean-Marc, 9 septembre 2026).
const BUREAU_PANNEAU_PAD_H = 16; // marge intérieure horizontale, px de référence
const BUREAU_PANNEAU_PAD_V = 10; // marge intérieure verticale, px de référence
/**
* Petit générateur pseudo-aléatoire déterministe (0..1), à partir d'un
* entier (TDL_Incr) — pour que le jitter (rotation/décalage) d'un post-it
* donné reste toujours le même d'un rechargement à l'autre de l'écran,
* plutôt que de "sauter" à chaque fois (mulberry32, implémentation
* minimale, largement suffisante ici — pas besoin de qualité crypto).
*/
function pseudoAleatoire(graine) {
let t = (graine + 0x6D2B79F5) | 0;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
// Crée les 9 zones cliquables + étiquettes de classeurs, une seule fois
// (voir tableau CLASSEURS plus haut) — même principe que les post-its,
// appelée une fois au chargement, positionnée ensuite (comme tout le
// reste) dans positionner(). Un classeur avec href (voir CLASSEURS) est
// un vrai lien <a> ; les 8 autres, sans écran derrière pour l'instant,
// sont de simples <div> — pas de lien mort ni de survol trompeur (voir
// commentaire CSS .classeur-zone plus haut).
function creerClasseurs() {
CLASSEURS.forEach(function (c) {
const zone = document.createElement(c.href ? 'a' : 'div');
zone.className = 'classeur-zone' + (c.href ? ' classeur-actif' : '');
zone.id = 'classeur-' + c.id;
zone.dataset.classeurId = c.id;
if (c.href) {
zone.href = c.href;
zone.title = 'Ouvrir le classeur ' + c.label.toLowerCase();
}
document.body.appendChild(zone);
const etiquette = document.createElement('div');
etiquette.className = 'etiquette-classeur';
etiquette.id = 'etiquette-' + c.id;
etiquette.textContent = c.label;
document.body.appendChild(etiquette);
});
}
// Mesure de la largeur RÉELLE d'un texte (7 septembre 2026, remplace une
// première version qui estimait cette largeur à partir d'un ratio de
// largeur de caractère deviné — trop approximatif : "Prospection" et
// "Journalier" étaient ressortis trop petits à l'usage réel chez
// Jean-Marc). Utilise un <canvas> hors-écran avec exactement la même
// police que .etiquette-classeur (voir CSS plus haut) : measureText()
// interroge directement le moteur de rendu de police du navigateur EN
// COURS D'UTILISATION — celui de Jean-Marc (Segoe UI, Windows) une fois la
// page redéployée chez lui — plutôt qu'une estimation calculée ici. Un
// seul canvas et un petit cache par (libellé, taille) : positionner() est
// rappelée à chaque redimensionnement de fenêtre, pas la peine de remesurer
// à chaque fois un texte qui n'a pas changé.
const canvasMesure = document.createElement('canvas');
const ctxMesure = canvasMesure.getContext('2d');
const largeursTexteMesurees = {};
function mesurerLargeurTexte(libelle, taillePx) {
const cle = libelle + '@' + taillePx;
if (largeursTexteMesurees[cle] !== undefined) return largeursTexteMesurees[cle];
ctxMesure.font = '600 ' + taillePx + 'px "Segoe UI","Arial",sans-serif';
// letter-spacing:0.03em (voir .etiquette-classeur) ajouté à la main :
// measureText() ne le prend pas en compte nativement de façon fiable
// sur tous les navigateurs, un calcul explicite est plus sûr qu'un
// support incertain de ctx.letterSpacing.
const largeurBase = ctxMesure.measureText(libelle).width;
const largeur = largeurBase + (libelle.length - 1) * 0.03 * taillePx;
largeursTexteMesurees[cle] = largeur;
return largeur;
}
// Taille de police (en fraction de IMG_W) pour l'étiquette d'un classeur —
// double contrainte, la plus stricte des deux l'emporte :
// 1) La largeur RÉELLE du texte (mesurée ci-dessus à une taille de
// référence, puis mise à l'échelle) doit tenir dans la hauteur
// disponible de l'étiquette (ETIQ_LABEL_Y2-Y1) : c'est cette hauteur
// qui devient la largeur du texte une fois tourné à -90°. C'est cette
// contrainte qui borne les libellés longs ("Documentations",
// "Communication").
// 2) L'épaisseur d'une ligne de texte (≈ taille × ETIQ_LIGNE_HAUTEUR) doit
// tenir dans la largeur physique de l'étiquette (ETIQ_LABEL_LARGEUR) :
// sans cette 2ᵉ contrainte, un libellé court comme "Factures" pourrait
// grandir au point de déborder du rectangle blanc sur les côtés.
// Bornée par ETIQ_POLICE_MIN_FRAC en dessous (seuil de lisibilité) — pas de
// plafond arbitraire au-dessus, les deux contraintes ci-dessus suffisent.
function calculerPoliceEtiquette(libelle) {
const TAILLE_REF = 100; // px CSS, taille arbitraire pour la mesure
const largeurRef = mesurerLargeurTexte(libelle, TAILLE_REF);
const hauteurDispoImg = (ETIQ_LABEL_Y2 - ETIQ_LABEL_Y1) * ETIQ_MARGE_HAUTEUR;
const parContrainteHauteur = (hauteurDispoImg / largeurRef) * TAILLE_REF / IMG_W;
return Math.max(ETIQ_POLICE_MIN_FRAC, Math.min(parContrainteHauteur, ETIQ_POLICE_CAP_FRAC));
}
function positionner() {
const vw = window.innerWidth, vh = window.innerHeight;
const ratioImg = IMG_W / IMG_H;
const ratioVp = vw / vh;
let dispW, dispH, offX, offY;
if (ratioVp > ratioImg) {
dispH = vh; dispW = dispH * ratioImg;
offX = (vw - dispW) / 2; offY = 0;
} else {
dispW = vw; dispH = dispW / ratioImg;
offX = 0; offY = (vh - dispH) / 2;
}
// Fonds interchangeables + avant-plan (8 septembre 2026) : occupent
// exactement le même rectangle "contain" que l'ancienne image unique,
// pour rester alignés au pixel près avec les classeurs/micro/pastille/
// post-its positionnés plus bas dans cette même fonction.
const fondsContainer = document.getElementById('fonds-container');
fondsContainer.style.left = offX + 'px';
fondsContainer.style.top = offY + 'px';
fondsContainer.style.width = dispW + 'px';
fondsContainer.style.height = dispH + 'px';
const avantPlan = document.getElementById('avant-plan');
avantPlan.style.left = offX + 'px';
avantPlan.style.top = offY + 'px';
avantPlan.style.width = dispW + 'px';
avantPlan.style.height = dispH + 'px';
// Flèches de navigation entre fonds (voir FLECHE_* plus haut).
const flecheTaille = FLECHE_TAILLE_FRAC * dispW;
const flechePrecedente = document.getElementById('fleche-precedente');
flechePrecedente.style.left = (offX + (FLECHE_PRECEDENTE_CENTRE_X / IMG_W) * dispW - flecheTaille / 2) + 'px';
flechePrecedente.style.top = (offY + (FLECHE_CENTRE_Y / IMG_H) * dispH - flecheTaille / 2) + 'px';
flechePrecedente.style.width = flecheTaille + 'px';
flechePrecedente.style.height = flecheTaille + 'px';
const flecheSuivante = document.getElementById('fleche-suivante');
flecheSuivante.style.left = (offX + (FLECHE_SUIVANTE_CENTRE_X / IMG_W) * dispW - flecheTaille / 2) + 'px';
flecheSuivante.style.top = (offY + (FLECHE_CENTRE_Y / IMG_H) * dispH - flecheTaille / 2) + 'px';
flecheSuivante.style.width = flecheTaille + 'px';
flecheSuivante.style.height = flecheTaille + 'px';
// Les 9 classeurs (zone cliquable + étiquette tournée, voir
// creerClasseurs() et calculerPoliceEtiquette() plus haut).
CLASSEURS.forEach(function (c) {
const zone = document.getElementById('classeur-' + c.id);
zone.style.left = (offX + (c.x1 / IMG_W) * dispW) + 'px';
zone.style.top = (offY + (CLASSEURS_Y1 / IMG_H) * dispH) + 'px';
zone.style.width = (((c.x2 - c.x1) / IMG_W) * dispW) + 'px';
zone.style.height = (((CLASSEURS_Y2 - CLASSEURS_Y1) / IMG_H) * dispH) + 'px';
const etiquette = document.getElementById('etiquette-' + c.id);
etiquette.style.left = (offX + (c.labelCx / IMG_W) * dispW) + 'px';
etiquette.style.top = (offY + ETIQ_CENTRE_Y_FRAC * dispH) + 'px';
const taillePoliceFrac = (c.policeForceeFrac !== undefined) ? c.policeForceeFrac : calculerPoliceEtiquette(c.label);
etiquette.style.fontSize = (taillePoliceFrac * dispW) + 'px';
});
const microDesk = document.getElementById('micro-desk');
microDesk.style.left = (offX + (MICRO_ZONE_X1 / IMG_W) * dispW) + 'px';
microDesk.style.top = (offY + (MICRO_ZONE_Y1 / IMG_H) * dispH) + 'px';
microDesk.style.width = (((MICRO_ZONE_X2 - MICRO_ZONE_X1) / IMG_W) * dispW) + 'px';
microDesk.style.height = (((MICRO_ZONE_Y2 - MICRO_ZONE_Y1) / IMG_H) * dispH) + 'px';
const microIcone = document.getElementById('micro-icone');
const iconeTaille = ICONE_TAILLE_FRAC * dispW;
microIcone.style.left = (offX + (ICONE_CENTRE_X / IMG_W) * dispW - iconeTaille / 2) + 'px';
microIcone.style.top = (offY + (ICONE_CENTRE_Y / IMG_H) * dispH - iconeTaille / 2) + 'px';
microIcone.style.width = iconeTaille + 'px';
microIcone.style.height = iconeTaille + 'px';
// #statut-version-bloc (société/prénom/marque) ne fait plus partie de ce
// calcul depuis le 3 septembre 2026 : position fixe en bas à droite de
// l'écran, en pixels fixes CSS — voir le commentaire au-dessus de sa
// règle #statut-version-bloc et le journal.
const pastilleContacts = document.getElementById('pastille-contacts');
const pastilleDiam = PASTILLE_CONTACTS_DIAM_FRAC * dispW;
pastilleContacts.style.left = (offX + (PASTILLE_CONTACTS_CENTRE_X / IMG_W) * dispW - pastilleDiam / 2) + 'px';
pastilleContacts.style.top = (offY + (PASTILLE_CONTACTS_CENTRE_Y / IMG_H) * dispH - pastilleDiam / 2) + 'px';
pastilleContacts.style.width = pastilleDiam + 'px';
pastilleContacts.style.height = pastilleDiam + 'px';
// Taille de police proportionnelle au diamètre, réduite si le nombre a
// 3 chiffres ou plus pour continuer à tenir dans le rond sans déborder.
const nbChiffres = document.getElementById('pastille-contacts-nombre').textContent.length;
const facteurPolice = nbChiffres >= 4 ? 0.34 : (nbChiffres === 3 ? 0.42 : 0.52);
document.getElementById('pastille-contacts-nombre').style.fontSize = (pastilleDiam * facteurPolice) + 'px';
// Post-its personnels : chaque .postit porte sa case (data-colonne/
// data-ligne, attribuées au chargement — voir creerPostits()) et une
// graine (data-graine = TDL_Incr) pour un jitter stable d'un
// rechargement à l'autre. Le jitter reste borné à l'intérieur de la
// case (marge × 0.8, jamais 1) pour ne jamais déborder sur la case
// voisine — c'est ce qui garantit l'absence d'empilement.
// zoneLeft/zoneTop : plus de offX/offY depuis le 8 septembre 2026 — les
// post-its sont désormais positionnés à l'intérieur de #postits-layer,
// lui-même dans la slide du fond liège (inset:0 sur les deux), donc déjà
// calé sur le rectangle "contain" par la structure DOM elle-même (voir
// commentaire CSS #postits-layer plus haut).
const zoneW = ((ZONE_POSTITS_X2 - ZONE_POSTITS_X1) / IMG_W) * dispW;
const zoneH = ((ZONE_POSTITS_Y2 - ZONE_POSTITS_Y1) / IMG_H) * dispH;
const caseW = zoneW / GRILLE_POSTITS_COLONNES;
const caseH = zoneH / GRILLE_POSTITS_LIGNES;
const postitTaille = Math.min(caseW, caseH) * POSTIT_RATIO_CASE;
const zoneLeft = (ZONE_POSTITS_X1 / IMG_W) * dispW;
const zoneTop = (ZONE_POSTITS_Y1 / IMG_H) * dispH;
const margeX = (caseW - postitTaille) / 2 * 0.8;
const margeY = (caseH - postitTaille) / 2 * 0.8;
document.querySelectorAll('.postit').forEach(function (postit) {
const col = Number(postit.dataset.colonne);
const ligne = Number(postit.dataset.ligne);
const graine = Number(postit.dataset.graine);
const caseLeft = zoneLeft + col * caseW;
const caseTop = zoneTop + ligne * caseH;
const decalageX = (pseudoAleatoire(graine) * 2 - 1) * margeX;
const decalageY = (pseudoAleatoire(graine + 1) * 2 - 1) * margeY;
const rotation = (pseudoAleatoire(graine + 2) * 2 - 1) * 7; // ±7°
postit.style.width = postitTaille + 'px';
postit.style.height = postitTaille + 'px';
postit.style.left = (caseLeft + (caseW - postitTaille) / 2 + decalageX) + 'px';
postit.style.top = (caseTop + (caseH - postitTaille) / 2 + decalageY) + 'px';
postit.style.transform = 'rotate(' + rotation.toFixed(1) + 'deg)';
});
// Portraits des assistants (écran 2) : nombre de colonnes = nombre de
// cartes réellement créées par creerCartesAssistants() (voir plus bas),
// pas de valeur codée en dur — la largeur de colonne se recalcule donc
// toute seule si un agent est ajouté ou retiré côté ASP_Agents. scale
// convertit un px de référence (IMG_W/IMG_H) en px écran ; identique
// dans les deux sens puisque le rectangle "contain" respecte toujours le
// ratio IMG_W/IMG_H (voir calcul de dispW/dispH plus haut).
const cartesAssistants = coucheAssistants.querySelectorAll('.assistant-carte');
if (cartesAssistants.length > 0) {
const scale = dispW / IMG_W;
const colW = dispW / cartesAssistants.length;
const matPx = ASSISTANT_MAT * scale;
let photoH = ASSISTANT_PHOTO_H_MAX * scale;
// Contrainte de largeur : si la colonne est trop étroite pour la
// hauteur visée (beaucoup d'agents), on réduit la photo en proportion
// plutôt que de déborder sur la colonne voisine.
const largeurMaxCarte = colW * ASSISTANT_COL_MARGE;
if (photoH * ASSISTANT_PHOTO_RATIO + 2 * matPx > largeurMaxCarte) {
photoH = (largeurMaxCarte - 2 * matPx) / ASSISTANT_PHOTO_RATIO;
}
const photoW = photoH * ASSISTANT_PHOTO_RATIO;
const carteW = photoW + 2 * matPx;
const carteH = photoH + 2 * matPx;
const topPx = ASSISTANT_ZONE_TOP * scale;
const nomPx = ASSISTANT_NOM_POLICE_FRAC * dispW;
const fonctionPx = ASSISTANT_FONCTION_POLICE_FRAC * dispW;
cartesAssistants.forEach(function (carte, index) {
const cx = colW * index + colW / 2;
carte.style.left = (cx - carteW / 2) + 'px';
carte.style.top = topPx + 'px';
carte.style.width = carteW + 'px';
const cadre = carte.querySelector('.assistant-cadre');
cadre.style.width = carteW + 'px';
cadre.style.height = carteH + 'px';
cadre.style.padding = matPx + 'px';
const nomEl = carte.querySelector('.assistant-nom');
if (nomEl) nomEl.style.fontSize = nomPx + 'px';
const fonctionEl = carte.querySelector('.assistant-fonction');
if (fonctionEl) fonctionEl.style.fontSize = fonctionPx + 'px';
const repli = carte.querySelector('.assistant-photo-repli');
if (repli) repli.style.fontSize = (photoH * 0.3) + 'px';
});
// Panneau "Bureau des assistants" : la zone sous les 2 dernières
// colonnes (aujourd'hui Léon et Sophia) ne sert qu'à CENTRER
// l'encadré — l'encadré lui-même est ajusté au texte (mesuré via
// ctxMesure, voir plus haut), pas étiré sur toute la zone (retour
// Jean-Marc, 9 septembre 2026). Bas du texte des cartes calculé
// analytiquement (pas mesuré en DOM) : coucheAssistants peut être
// parquée hors document (fond non affiché), où
// offsetHeight/getBoundingClientRect renverraient 0.
if (cartesAssistants.length >= 2) {
const nomLigne = nomPx * 1.15; // line-height:1.15, voir .assistant-nom
const nomMargeHaut = 6 * scale; // margin-top:6px, voir .assistant-nom
const fonctionLigne = fonctionPx * 1.15; // line-height:1.15, voir .assistant-fonction
const basCarte = topPx + carteH + nomMargeHaut + nomLigne + fonctionLigne;
const zoneTop = basCarte + BUREAU_PANNEAU_GAP * scale;
const zoneBas = BUREAU_PANNEAU_BAS * scale;
const zoneGauche = colW * (cartesAssistants.length - 2);
const zoneLargeur = colW * 2;
const zoneCentreX = zoneGauche + zoneLargeur / 2;
const zoneCentreY = zoneTop + (zoneBas - zoneTop) / 2;
const panneauPolicePx = BUREAU_PANNEAU_POLICE_FRAC * dispW;
ctxMesure.font = '700 ' + panneauPolicePx + 'px "Segoe UI","Arial",sans-serif';
const texteLargeur = ctxMesure.measureText(panneauBureau.textContent).width;
const padH = BUREAU_PANNEAU_PAD_H * scale;
const padV = BUREAU_PANNEAU_PAD_V * scale;
const panneauLargeur = texteLargeur + 2 * padH;
const panneauHauteur = panneauPolicePx * 1.15 + 2 * padV;
panneauBureau.style.display = 'flex';
panneauBureau.style.left = (zoneCentreX - panneauLargeur / 2) + 'px';
panneauBureau.style.top = (zoneCentreY - panneauHauteur / 2) + 'px';
panneauBureau.style.width = panneauLargeur + 'px';
panneauBureau.style.height = panneauHauteur + 'px';
panneauBureau.style.borderWidth = matPx + 'px';
panneauBureau.style.fontSize = panneauPolicePx + 'px';
} else {
panneauBureau.style.display = 'none';
}
} else {
panneauBureau.style.display = 'none';
}
}
// -------- Fonds interchangeables (8 septembre 2026) --------
// 3 fonds codés en dur (décision de Jean-Marc, pas de liste extensible
// pour l'instant) : liège actuel, tasseaux gris (futur écran assistants,
// présentation pas encore décidée), tableau blanc (futurs tableaux de
// bord). Toujours fondIndex=0 au chargement, aucune mémorisation d'une
// session à l'autre (décision explicite de Jean-Marc).
const FONDS = ['fond_bureau_1.jpg', 'fond_bureau_2.jpg', 'fond_bureau_3.jpg'];
let fondActuel = 0;
let glissementEnCours = false;
const DUREE_GLISSEMENT_MS = 450;
// Conteneur des post-its personnels, attaché au fond liège (index 0) —
// voir commentaire CSS #postits-layer plus haut. Créé une seule fois, vide
// au départ (chargerMesPostits() le remplit plus bas, de façon asynchrone,
// qu'il soit affiché ou parqué à ce moment-là — sans incidence puisque
// positionner() peut être appelée sans risque sur un élément hors DOM
// visible). Une variable JS, jamais document.getElementById() : l'élément
// reste valable même temporairement détaché du document (parqué dans une
// ancienne slide retirée), ce que getElementById ne retrouverait pas de la
// même façon si la recherche se faisait après un tel retrait.
const couchePostits = document.createElement('div');
couchePostits.id = 'postits-layer';
// Portraits des assistants (écran 2, 9 septembre 2026) : même principe que
// couchePostits ci-dessus — un noeud créé une seule fois, rempli de façon
// asynchrone par chargerAgentsEcranAssistants() plus bas, déplacé (jamais
// dupliqué) entre les slides par creerFondSlide().
const coucheAssistants = document.createElement('div');
coucheAssistants.id = 'assistants-layer';
// Panneau "Bureau des assistants" (voir commentaire CSS .bureau-panneau et
// constantes BUREAU_PANNEAU_* plus haut) : contenu statique, créé une
// seule fois ici et jamais recréé par creerCartesAssistants() (qui ne
// touche qu'aux .assistant-carte) — seule sa position/taille est
// recalculée à chaque appel de positionner().
const panneauBureau = document.createElement('div');
panneauBureau.className = 'bureau-panneau';
panneauBureau.textContent = "L'équipe des collaborateurs";
coucheAssistants.appendChild(panneauBureau);
function creerFondSlide(index) {
const slide = document.createElement('div');
slide.className = 'fond-slide';
slide.style.backgroundImage = "url('" + FONDS[index] + "')";
// Le fond liège (index 0) embarque la couche des post-its, le fond
// tasseaux gris (index 1, écran assistants) la couche des portraits : un
// simple appendChild déplace le noeud existant (le retire au passage de
// son ancien parent, s'il en avait un) — pas de duplication, pas de perte
// du contenu déjà chargé.
if (index === 0) {
slide.appendChild(couchePostits);
} else if (index === 1) {
slide.appendChild(coucheAssistants);
}
return slide;
}
// Affiche le tout premier fond au chargement, sans transition.
function initialiserFonds() {
const conteneur = document.getElementById('fonds-container');
conteneur.appendChild(creerFondSlide(fondActuel));
}
// direction : +1 (flèche "suivant", le fond n+1 entre par la droite en
// poussant l'actuel vers la gauche) ou -1 (flèche "précédent", l'inverse)
// — sens du glissement lié à la flèche pressée, consigne explicite de
// Jean-Marc. Boucle sur les 3 fonds (n+1 après le 3ᵉ revient au 1er).
function changerFond(direction) {
if (glissementEnCours) return;
glissementEnCours = true;
const conteneur = document.getElementById('fonds-container');
const ancienneSlide = conteneur.querySelector('.fond-slide');
const nouvelIndex = (fondActuel + direction + FONDS.length) % FONDS.length;
const nouvelleSlide = creerFondSlide(nouvelIndex);
// Position de départ de la nouvelle slide (hors champ, côté d'où elle
// doit entrer), posée sans transition avant d'être ajoutée au DOM.
nouvelleSlide.style.transform = 'translateX(' + (direction > 0 ? '100%' : '-100%') + ')';
conteneur.appendChild(nouvelleSlide);
// Forcer un reflow avant d'activer la transition, sinon le navigateur
// regroupe la position de départ et la position d'arrivée en une seule
// étape (pas de glissement visible).
void nouvelleSlide.offsetWidth;
nouvelleSlide.style.transition = 'transform ' + DUREE_GLISSEMENT_MS + 'ms ease';
ancienneSlide.style.transition = 'transform ' + DUREE_GLISSEMENT_MS + 'ms ease';
requestAnimationFrame(function () {
nouvelleSlide.style.transform = 'translateX(0)';
ancienneSlide.style.transform = 'translateX(' + (direction > 0 ? '-100%' : '100%') + ')';
});
ancienneSlide.addEventListener('transitionend', function fin() {
ancienneSlide.removeEventListener('transitionend', fin);
ancienneSlide.remove();
glissementEnCours = false;
});
fondActuel = nouvelIndex;
}
document.getElementById('fleche-precedente').addEventListener('click', function () { changerFond(-1); });
document.getElementById('fleche-suivante').addEventListener('click', function () { changerFond(1); });
initialiserFonds();
creerClasseurs();
window.addEventListener('resize', positionner);
positionner();
// Bascule ouvert/fermé du micro du bureau : gérée par le script chargé en
// fin de page (contact_ia_panel.js actuellement, voir commentaire plus haut),
// qui pilote le clic, la reconnaissance vocale et la classe "ecoute" sur
// #micro-desk — voir la règle CSS correspondante plus
// haut pour la bascule visuelle des deux glyphes. L'ancien toggle purement
// visuel du premier essai (2-3 septembre 2026) est retiré.
// Société et prénom de l'utilisateur connecté (3 septembre 2026, voir
// journal) : preferences.php, même endpoint et même logique que
// chargerPreferences() dans liste_contacts.html — repris ici à l'identique
// pour le groupe #statut-version-bloc (voir plus haut, id communs avec
// liste_contacts.html). Le chevalet qui affichait seul le prénom
// (USR_Prenom, ajouté le 2 septembre 2026 dans ASP_Users) a été retiré du
// bureau le 3 septembre — cette fonction ne porte donc plus que ce groupe.
// Repli silencieux si la session n'est pas active.
async function chargerIdentiteUtilisateur() {
const elSociete = document.getElementById('societe-utilisateur');
const elPointSociete = document.getElementById('point-societe');
const elPrenom = document.getElementById('prenom-utilisateur');
try {
const reponsePrefs = await fetch('preferences.php');
if (!reponsePrefs.ok) throw new Error('HTTP ' + reponsePrefs.status);
const prefs = await reponsePrefs.json();
if (prefs.USR_Societe) {
elSociete.textContent = prefs.USR_Societe;
elSociete.hidden = false;
elPointSociete.hidden = false;
} else {
elSociete.hidden = true;
elPointSociete.hidden = true;
}
if (prefs.USR_Prenom) elPrenom.textContent = prefs.USR_Prenom;
// Bouton "BO" (11 septembre 2026) : n'apparaît que si la session porte
// l'accès (USR_BackOffice, posé par aspic.php depuis MIR_BO) — voir
// commentaire CSS #bouton-bo.
if (prefs.USR_BackOffice) {
document.getElementById('bouton-bo').hidden = false;
}
} catch (erreur) {
console.error('Erreur de chargement de preferences.php (identité) :', erreur);
}
}
chargerIdentiteUtilisateur();
document.getElementById('bouton-bo').addEventListener('click', function () {
window.location.href = 'backoffice.html';
});
// Numéro de version : même endpoint que #statut-version dans
// liste_contacts.html (chargerVersion(), voir journal). Repli silencieux
// si non joignable (fichier hors serveur, ou version.php absent de ce
// dossier) : l'écran reste utilisable sans.
async function chargerVersionBureau() {
try {
const reponse = await fetch('version.php');
if (!reponse.ok) throw new Error('HTTP ' + reponse.status);
const data = await reponse.json();
if (data.version) {
document.getElementById('statut-version').textContent = data.version;
// Infobulle du groupe icône Mévélia + logo ASPIC + version (3
// septembre 2026, voir journal), identique à liste_contacts.html.
const elMarque = document.getElementById('marque-aspic');
if (elMarque) elMarque.title = 'ASPIC ' + data.version + '\nCréé par Mevelia Solutions';
}
} catch (erreur) {
console.error('Erreur de chargement de version.php (bureau) :', erreur);
}
}
chargerVersionBureau();
// Pastille "nombre de contacts" du classeur Contacts : total réel de la
// table (contacts.php?compte=1, voir journal section 28 et commentaire
// ajouté dans contacts.php), jamais un chiffre figé. Repli visuel "—" en
// cas d'échec (même logique que chargerIdentiteUtilisateur), pour ne jamais
// laisser une pastille vide sans explication. Le pattern (constantes
// PASTILLE_*, positionnement dans positionner(), chargement ici) est
// pensé pour être dupliqué sur les prochains classeurs à mesure qu'ils
// seront branchés à leur table.
async function chargerCompteContacts() {
const elNombre = document.getElementById('pastille-contacts-nombre');
try {
const reponse = await fetch('contacts.php?compte=1');
if (!reponse.ok) throw new Error('HTTP ' + reponse.status);
const data = await reponse.json();
elNombre.textContent = (typeof data.total === 'number') ? String(data.total) : '—';
} catch (erreur) {
console.error('Erreur de chargement de contacts.php?compte=1 (pastille classeur) :', erreur);
elNombre.textContent = '—';
}
positionner(); // recalcule la taille de police selon le nombre réel de chiffres
}
chargerCompteContacts();
// Post-its personnels (2 septembre 2026, premier affichage réel — voir
// commentaire CSS .postit et constantes ZONE_POSTITS_*/GRILLE_POSTITS_*
// plus haut). Source : todolist.php (déjà utilisé par liste_todolist.html,
// voir journal section 26), filtré ici côté client sur l'utilisateur
// connecté (TDL_ToDoUser) et sur les tâches non réalisées (TDL_RealDate
// vide) — l'endpoint reste volontairement non filtré par utilisateur
// (voir son commentaire d'en-tête), donc le tri "c'est MES post-its" se
// fait ici. L'API renvoie déjà les tâches triées par urgence (rouge avant
// orange avant vert, voir calculerGroupe()) : cet ordre est conservé pour
// remplir la grille case par case, donc les plus urgentes arrivent
// toujours en premier (en haut à gauche de la zone). Une tâche déjà
// réalisée ne crée jamais de post-it : elle libère sa place sur le
// tableau dès qu'elle est marquée faite (voir aussi CAPACITE_TABLEAU_
// PAR_UTILISATEUR dans todolist.php — même logique des deux côtés).
// Coupé à la capacité de la grille par sécurité (ne devrait normalement
// jamais arriver, la capacité étant déjà appliquée à la création côté
// todolist.php).
async function chargerMesPostits() {
try {
const [reponsePrefs, reponseTaches] = await Promise.all([
fetch('preferences.php'),
fetch('todolist.php'),
]);
if (!reponsePrefs.ok) throw new Error('HTTP ' + reponsePrefs.status + ' (preferences.php)');
if (!reponseTaches.ok) throw new Error('HTTP ' + reponseTaches.status + ' (todolist.php)');
const prefs = await reponsePrefs.json();
const dataTaches = await reponseTaches.json();
const capacite = GRILLE_POSTITS_COLONNES * GRILLE_POSTITS_LIGNES;
const mesTaches = (dataTaches.taches || [])
.filter(t => t.TDL_ToDoUser === prefs.USR_Code && !t.TDL_RealDate)
.slice(0, capacite);
mesTaches.forEach((tache, index) => {
const postit = document.createElement('div');
postit.className = 'postit';
postit.style.setProperty('--couleur-postit', '#fbe14a'); // jaune classique, toujours pour l'humain
postit.title = tache.TDL_ToDoTxt || '';
postit.dataset.colonne = index % GRILLE_POSTITS_COLONNES;
postit.dataset.ligne = Math.floor(index / GRILLE_POSTITS_COLONNES);
postit.dataset.graine = tache.TDL_Incr;
const pastille = document.createElement('div');
// TDL_Groupe 0 = rouge, 1 = orange (le vert n'atteint jamais cet
// écran, voir filtre ci-dessus sur TDL_RealDate).
pastille.className = 'pastille-urgence-postit ' + (tache.TDL_Groupe === 0 ? 'rouge' : 'orange');
postit.appendChild(pastille);
// Attaché à couchePostits (fond liège), pas à document.body depuis
// le 8 septembre 2026 — voir commentaire sur couchePostits plus haut.
couchePostits.appendChild(postit);
});
positionner(); // place les post-its qui viennent d'être créés
} catch (erreur) {
console.error('Erreur de chargement des post-its personnels (todolist.php/preferences.php) :', erreur);
}
}
chargerMesPostits();
// Portraits des assistants, écran 2 (9 septembre 2026 — voir journal et
// réflexions stratégiques). Source : agents.php, qui sert déjà
// AGN_Prenom/AGN_Fonction pour le pilotage vocal — AGN_Photo (nom de
// fichier, ex. "assistant_matilda.jpg") et le tri par AGN_OrdreAffichage
// s'y ajoutent pour cet écran (voir commentaire d'en-tête de agents.php). Un agent sans
// AGN_Photo reste affiché (repli visuel "?", voir .assistant-photo-repli) :
// jamais de colonne manquante silencieusement. Nombre de colonnes non codé
// en dur : une carte par agent réellement renvoyé (voir positionner()).
function creerCartesAssistants() {
// Ne retire que les cartes (pas le panneau "Bureau des assistants",
// ajouté une seule fois à la création de coucheAssistants — voir
// panneauBureau plus haut).
coucheAssistants.querySelectorAll('.assistant-carte').forEach(function (el) { el.remove(); });
(agentsEcranAssistants || []).forEach(function (agent) {
const carte = document.createElement('div');
carte.className = 'assistant-carte';
const cadre = document.createElement('div');
cadre.className = 'assistant-cadre';
if (agent.AGN_Photo) {
const img = document.createElement('img');
img.className = 'assistant-photo';
img.alt = agent.AGN_Prenom || '';
img.src = agent.AGN_Photo;
cadre.appendChild(img);
} else {
const repli = document.createElement('div');
repli.className = 'assistant-photo-repli';
repli.textContent = '?';
cadre.appendChild(repli);
}
carte.appendChild(cadre);
const nom = document.createElement('div');
nom.className = 'assistant-nom';
nom.textContent = agent.AGN_Prenom || '';
carte.appendChild(nom);
if (agent.AGN_Fonction) {
const fonction = document.createElement('div');
fonction.className = 'assistant-fonction';
fonction.textContent = agent.AGN_Fonction;
carte.appendChild(fonction);
}
coucheAssistants.appendChild(carte);
});
positionner(); // place les cartes qui viennent d'être (re)créées
}
let agentsEcranAssistants = [];
async function chargerAgentsEcranAssistants() {
try {
const reponse = await fetch('agents.php');
if (!reponse.ok) throw new Error('HTTP ' + reponse.status);
const data = await reponse.json();
agentsEcranAssistants = data.agents || [];
creerCartesAssistants();
} catch (erreur) {
console.error('Erreur de chargement de agents.php (écran assistants) :', erreur);
}
}
chargerAgentsEcranAssistants();
// Lien visible "→ Liste des tâches" retiré le 2 septembre 2026 à la
// demande de Jean-Marc. Échap avait été conservé ce soir-là comme
// échappatoire silencieuse vers liste_todolist.html, pour ne pas rester
// bloqué sur cet écran tant qu'il ne portait aucun objet fonctionnel.
// Retiré le 3 septembre 2026 (bug signalé par Jean-Marc) : cet écran est
// depuis la veille le véritable écran d'accueil post-identification (voir
// journal, section 28) — liste_contacts.html y ramène désormais par Échap
// (voir journal du 3 septembre), donc un Échap qui, depuis l'écran
// d'accueil lui-même, repartait ailleurs créait une boucle incohérente.
// Aucune redirection ne remplace celle-ci : Échap ne fait plus rien ici,
// faute d'un "retour" qui ait un sens depuis l'écran d'accueil. À revoir
// le jour où la ToDoList aura son propre objet cliquable sur le bureau.
</script>
<script src="contact_ia_panel.js"></script>
</body>
</html>