mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 17:17:08 +00:00
2099 lines
106 KiB
HTML
2099 lines
106 KiB
HTML
<!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;
|
||
}
|
||
/* 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;
|
||
}
|
||
|
||
/* Outils "Construire une identité" / "Tester l'identité" (10 septembre
|
||
2026, réflexions stratégiques section 20 ; repositionnés et redesignés
|
||
le 10 septembre 2026 (suite), retour de Jean-Marc après un premier essai
|
||
réel) — deux boutons juste sous le panneau "L'équipe des collaborateurs",
|
||
positionnés par JS dans positionner() (comme panneauBureau lui-même :
|
||
largeur/police recalculées, pas de pourcentage fixe) puisqu'ils doivent
|
||
suivre la même zone sous les 2 dernières colonnes. */
|
||
#outils-collaborateurs{
|
||
position:absolute;
|
||
display:flex;
|
||
gap:8px;
|
||
pointer-events:auto;
|
||
}
|
||
.bouton-outil-collab{
|
||
font:inherit;
|
||
font-family:"Segoe UI","Arial",sans-serif;
|
||
font-weight:700;
|
||
background:#fff;
|
||
color:#2a2a2a;
|
||
border:1px solid #999;
|
||
border-radius:5px;
|
||
cursor:pointer;
|
||
box-shadow:2px 3px 6px rgba(0,0,0,.3);
|
||
white-space:nowrap;
|
||
}
|
||
.bouton-outil-collab.actif{
|
||
background:#2a2a2a;
|
||
color:#fff;
|
||
}
|
||
|
||
/* Écran plein cadre "Construire une identité" / "Tester l'identité" (refonte
|
||
du 10 septembre 2026, retour explicite de Jean-Marc) : position:fixed sur
|
||
TOUT le viewport, au-dessus de tout le reste (fond photo, avant-plan,
|
||
portraits) — "efface complètement l'écran, y compris l'écran de fond".
|
||
Volontairement hors de #assistants-layer/#fonds-container : un simple
|
||
calque opaque par-dessus tout suffit à "effacer" visuellement sans avoir
|
||
à toucher à l'état des autres calques (rien à restaurer à la fermeture).
|
||
Element statique du body (jamais créé/attaché dynamiquement) : présent
|
||
dans le document dès le chargement de la page, contrairement au premier
|
||
essai (nœuds attachés à #assistants-layer, donc absents du document tant
|
||
que l'écran des collaborateurs n'avait pas encore été affiché une
|
||
première fois — cause du bug "écran blanc" remonté par Jean-Marc). */
|
||
#overlay-collab{
|
||
position:fixed;
|
||
inset:0;
|
||
z-index:500;
|
||
background:#1c1c1c;
|
||
display:flex;
|
||
flex-direction:column;
|
||
align-items:center;
|
||
box-sizing:border-box;
|
||
/* Haut/bas resserrés le 10 septembre 2026 (soir), demande de Jean-Marc :
|
||
remonter le titre et redescendre les boutons au plus près des bords,
|
||
pour laisser le plus de hauteur possible aux zones de saisie/dialogue
|
||
(ni la taille des photos ni celle du texte sous les photos n'y touche,
|
||
seul l'espacement autour change). */
|
||
padding:10px 32px;
|
||
font-family:"Segoe UI","Arial",sans-serif;
|
||
color:#eee;
|
||
}
|
||
/* Bug trouvé en test automatisé le 10 septembre 2026 : l'ID selector
|
||
ci-dessus (display:flex) l'emporte en spécificité sur la règle UA
|
||
[hidden]{display:none}, donc SANS cette ligne #overlay-collab reste
|
||
affiché (et intercepte tous les clics, plein écran) même avec l'attribut
|
||
hidden posé — l'écran plein cadre bloquait alors silencieusement toute
|
||
la navigation dès le chargement de la page, hidden ou pas. */
|
||
#overlay-collab[hidden]{
|
||
display:none;
|
||
}
|
||
#overlay-collab-titre{
|
||
font-size:16px;
|
||
font-weight:700;
|
||
color:#eee;
|
||
margin-bottom:8px;
|
||
}
|
||
.ligne-photos-collab{
|
||
display:flex;
|
||
gap:22px;
|
||
flex-wrap:wrap;
|
||
justify-content:center;
|
||
margin-bottom:10px;
|
||
}
|
||
.photo-collab-selection{
|
||
display:flex;
|
||
flex-direction:column;
|
||
align-items:center;
|
||
gap:6px;
|
||
cursor:pointer;
|
||
background:none;
|
||
border:none;
|
||
padding:6px;
|
||
border-radius:8px;
|
||
}
|
||
.photo-collab-selection:hover{
|
||
background:rgba(255,255,255,.08);
|
||
}
|
||
.photo-collab-selection.selectionne{
|
||
background:rgba(255,255,255,.16);
|
||
}
|
||
.photo-collab-selection img,
|
||
.photo-collab-selection .repli{
|
||
width:72px;height:72px;border-radius:8px;object-fit:cover;
|
||
background:#3a3a3a;box-shadow:1px 2px 6px rgba(0,0,0,.4);
|
||
}
|
||
.photo-collab-selection .repli{
|
||
display:flex;align-items:center;justify-content:center;
|
||
color:#999;font-weight:700;
|
||
}
|
||
.photo-collab-selection .prenom-collab{
|
||
font-size:13px;font-weight:700;color:#f0f0f0;
|
||
}
|
||
.photo-collab-selection .fonction-collab{
|
||
font-size:11px;font-weight:700;color:#aaa;
|
||
}
|
||
|
||
.overlay-collab-corps{
|
||
flex:1;
|
||
width:100%;
|
||
max-width:760px;
|
||
display:flex;
|
||
flex-direction:column;
|
||
min-height:0;
|
||
}
|
||
/* Même correction que #overlay-collab[hidden] ci-dessus, pour la même
|
||
raison : sans cette règle, corps-identite et corps-test resteraient
|
||
TOUS LES DEUX affichés en permanence (superposés), l'attribut hidden
|
||
posé par le script (voir ouvrirConstruireIdentite()/ouvrirTesterIdentite())
|
||
n'ayant plus aucun effet face à ce display:flex d'origine auteur. */
|
||
.overlay-collab-corps[hidden]{
|
||
display:none;
|
||
}
|
||
/* Zone de saisie "terminal" (même esprit que les outils de mise au point
|
||
internes déjà livrés sur ce projet — contact_ia_test.html,
|
||
dictees_test.php) : fond noir, police mono, ascenseur si le texte
|
||
dépasse. */
|
||
#texte-identite{
|
||
flex:1;
|
||
min-height:0;
|
||
resize:none;
|
||
overflow-y:auto;
|
||
background:#000;
|
||
color:#ddd;
|
||
font-family:"Courier New",Consolas,monospace;
|
||
font-size:13px;
|
||
line-height:1.5;
|
||
border:1px solid #444;
|
||
border-radius:5px;
|
||
padding:12px;
|
||
}
|
||
.boutons-identite{
|
||
display:flex;
|
||
justify-content:flex-end;
|
||
gap:10px;
|
||
margin-top:12px;
|
||
}
|
||
#compteur-identite{
|
||
text-align:right;
|
||
font-size:11px;
|
||
color:#888;
|
||
margin-top:4px;
|
||
}
|
||
#compteur-identite.avertissement{color:#d9a84e;}
|
||
#compteur-identite.limite{color:#e14a5c;font-weight:700;}
|
||
.boutons-identite button{
|
||
font:inherit;font-size:13px;font-weight:700;border-radius:5px;
|
||
padding:9px 20px;cursor:pointer;
|
||
}
|
||
.boutons-identite button:disabled{opacity:.4;cursor:default;}
|
||
#btn-abandonner-identite{
|
||
background:#333;color:#eee;border:1px solid #555;
|
||
}
|
||
#btn-valider-identite{
|
||
background:#2a4d2a;color:#bdf7bd;border:1px solid #3a6d3a;
|
||
}
|
||
#btn-fermer-test-identite{
|
||
background:#333;color:#eee;border:1px solid #555;
|
||
}
|
||
#btn-chat-envoyer{
|
||
background:#2a4d2a;color:#bdf7bd;border:1px solid #3a6d3a;
|
||
}
|
||
#message-identite{
|
||
font-size:12px;margin-top:6px;min-height:14px;
|
||
}
|
||
#message-identite.ok{color:#7be07b;}
|
||
#message-identite.ko{color:#e14a5c;}
|
||
|
||
/* Fenêtre "type session Claude" (Tester l'identité) : bulles alternées,
|
||
couleur du collaborateur (AGN_Couleur) pour ses propres bulles — même
|
||
teinte que sa carte sur l'écran des collaborateurs et déjà anticipée
|
||
pour cet usage (réflexions stratégiques, section 18/28). */
|
||
#chat-messages{
|
||
flex:1;
|
||
min-height:0;
|
||
overflow-y:auto;
|
||
background:#111;
|
||
border:1px solid #444;
|
||
border-radius:5px;
|
||
padding:14px;
|
||
display:flex;
|
||
flex-direction:column;
|
||
gap:10px;
|
||
}
|
||
.bulle-chat{
|
||
max-width:75%;
|
||
padding:9px 13px;
|
||
border-radius:12px;
|
||
font-size:13px;
|
||
line-height:1.5;
|
||
white-space:pre-wrap;
|
||
}
|
||
.bulle-chat.moi{
|
||
align-self:flex-end;
|
||
background:#f2d94e;
|
||
color:#2a2a2a;
|
||
border-bottom-right-radius:3px;
|
||
}
|
||
.bulle-chat.collab{
|
||
align-self:flex-start;
|
||
color:#1a1a1a;
|
||
border-bottom-left-radius:3px;
|
||
}
|
||
.bulle-chat.systeme{
|
||
align-self:center;
|
||
color:#999;
|
||
font-size:12px;
|
||
font-style:italic;
|
||
background:none;
|
||
}
|
||
/* Zone de saisie du message — sur sa propre ligne, deux lignes de haut par
|
||
défaut (attribut rows="2" côté HTML), Envoyer et Fermer regroupés
|
||
juste en dessous (voir .boutons-identite, réutilisée telle quelle ici :
|
||
10 septembre 2026 soir, demande de Jean-Marc — même agencement que
|
||
"Construire une identité", Fermer/Envoyer à la place d'Abandonner/Valider). */
|
||
#chat-input{
|
||
width:100%;
|
||
box-sizing:border-box;
|
||
resize:none;
|
||
background:#000;
|
||
color:#ddd;
|
||
font:inherit;
|
||
font-size:13px;
|
||
border:1px solid #444;
|
||
border-radius:5px;
|
||
padding:9px 12px;
|
||
margin-top:12px;
|
||
}
|
||
#btn-chat-envoyer:disabled{opacity:.4;cursor:default;}
|
||
</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">•</span>
|
||
<span id="prenom-utilisateur"></span>
|
||
<span class="point-separateur" aria-hidden="true">•</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>
|
||
</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>
|
||
|
||
<!-- Écran plein cadre "Construire une identité" / "Tester l'identité"
|
||
(10 septembre 2026, réflexions stratégiques section 20 — voir commentaire
|
||
CSS #overlay-collab). Statique ici, dans le body, pour être présent dans
|
||
le document dès le chargement — voir ce même commentaire pour la raison
|
||
précise (bug corrigé du 10 septembre). Les deux "corps" (identité /
|
||
test) partagent l'entête (titre + photos) et se basculent l'un l'autre,
|
||
jamais affichés en même temps. -->
|
||
<div id="overlay-collab" hidden>
|
||
<div id="overlay-collab-titre"></div>
|
||
<div class="ligne-photos-collab" id="overlay-collab-photos"></div>
|
||
|
||
<div id="corps-identite" class="overlay-collab-corps" hidden>
|
||
<textarea id="texte-identite" spellcheck="false"></textarea>
|
||
<!-- Indicateur ajouté le 10 septembre 2026 (soir), demande de Jean-Marc :
|
||
AGN_Identite est une colonne MEDIUMTEXT (élargie le même soir depuis
|
||
TEXT — voir collaborateurs_identite.php, en-tête — en prévision d'un
|
||
texte appelé à s'enrichir avec l'usage réel de l'application, pas une
|
||
biographie figée) — 16 777 215 octets maximum en MySQL, PAS
|
||
16 777 215 caractères (utf8mb4 : de 1 à 4 octets par caractère selon
|
||
le caractère). Compte donc en octets (encodage UTF-8, comme MySQL le
|
||
stocke réellement), seule mesure exacte face à cette limite. Voir
|
||
mettreAJourCompteurIdentite(). -->
|
||
<div id="compteur-identite"></div>
|
||
<div class="boutons-identite">
|
||
<button type="button" id="btn-abandonner-identite">Abandonner</button>
|
||
<button type="button" id="btn-valider-identite">Valider</button>
|
||
</div>
|
||
<div id="message-identite"></div>
|
||
</div>
|
||
|
||
<div id="corps-test" class="overlay-collab-corps" hidden>
|
||
<div id="chat-messages"></div>
|
||
<textarea id="chat-input" rows="2" placeholder="Écris un message…"></textarea>
|
||
<div class="boutons-identite">
|
||
<button type="button" id="btn-fermer-test-identite">Fermer</button>
|
||
<button type="button" id="btn-chat-envoyer">Envoyer</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<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';
|
||
|
||
// Boutons "Construire une identité"/"Tester l'identité" (10 septembre
|
||
// 2026, retour de Jean-Marc) : juste sous panneauBureau, centrés sur
|
||
// le même axe — translateX(-50%) plutôt qu'une largeur mesurée, la
|
||
// largeur réelle du groupe de boutons dépendant du rendu du
|
||
// navigateur (police, padding), pas d'un texte qu'on mesure nous-mêmes
|
||
// comme pour panneauBureau.
|
||
outilsCollaborateurs.style.display = 'flex';
|
||
outilsCollaborateurs.style.fontSize = (panneauPolicePx * 0.8) + 'px';
|
||
outilsCollaborateurs.style.gap = (8 * scale) + 'px';
|
||
outilsCollaborateurs.style.top = (zoneCentreY + panneauHauteur / 2 + BUREAU_PANNEAU_GAP * scale) + 'px';
|
||
outilsCollaborateurs.style.left = zoneCentreX + 'px';
|
||
outilsCollaborateurs.style.transform = 'translateX(-50%)';
|
||
outilsCollaborateurs.querySelectorAll('.bouton-outil-collab').forEach(function (b) {
|
||
b.style.padding = (padV * 0.8) + 'px ' + (padH * 0.8) + 'px';
|
||
b.style.borderRadius = (5 * scale) + 'px';
|
||
});
|
||
} else {
|
||
panneauBureau.style.display = 'none';
|
||
outilsCollaborateurs.style.display = 'none';
|
||
}
|
||
} else {
|
||
panneauBureau.style.display = 'none';
|
||
outilsCollaborateurs.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);
|
||
|
||
// -------- Outils "Construire une identité" / "Tester l'identité" --------
|
||
// (10 septembre 2026, réflexions stratégiques section 20 ; repositionnés
|
||
// le 10 septembre 2026 (suite), sous panneauBureau.) Les deux boutons
|
||
// restent propres à l'écran des collaborateurs (déplacés avec
|
||
// coucheAssistants comme panneauBureau) ; l'écran plein cadre qu'ils
|
||
// ouvrent, lui, est statique dans le body — voir #overlay-collab, HTML,
|
||
// et le commentaire CSS associé pour la raison précise.
|
||
const outilsCollaborateurs = document.createElement('div');
|
||
outilsCollaborateurs.id = 'outils-collaborateurs';
|
||
|
||
const btnConstruireIdentite = document.createElement('button');
|
||
btnConstruireIdentite.type = 'button';
|
||
btnConstruireIdentite.className = 'bouton-outil-collab';
|
||
btnConstruireIdentite.textContent = 'Construire une identité';
|
||
|
||
const btnTesterIdentite = document.createElement('button');
|
||
btnTesterIdentite.type = 'button';
|
||
btnTesterIdentite.className = 'bouton-outil-collab';
|
||
btnTesterIdentite.textContent = "Tester l'identité";
|
||
|
||
outilsCollaborateurs.appendChild(btnConstruireIdentite);
|
||
outilsCollaborateurs.appendChild(btnTesterIdentite);
|
||
coucheAssistants.appendChild(outilsCollaborateurs);
|
||
|
||
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;
|
||
} catch (erreur) {
|
||
console.error('Erreur de chargement de preferences.php (identité) :', erreur);
|
||
}
|
||
}
|
||
chargerIdentiteUtilisateur();
|
||
|
||
// 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();
|
||
|
||
// -------- "Construire une identité" / "Tester l'identité" --------
|
||
// (10 septembre 2026, réflexions stratégiques section 20 — redesign du
|
||
// même jour, après-midi : écran plein cadre partagé entre les deux outils,
|
||
// #overlay-collab statique dans le body — voir commentaire CSS/HTML plus
|
||
// haut pour le bug que ce choix corrige au passage.)
|
||
|
||
// Ébauches proposées par Claude le 10 septembre 2026 — affichées comme
|
||
// point de départ dans la zone de saisie UNIQUEMENT tant qu'aucune identité
|
||
// n'a encore été enregistrée en base pour ce collaborateur (voir
|
||
// selectionnerCollaborateurIdentite() plus bas). Clé = AGN_Prenom exact.
|
||
const EBAUCHES_IDENTITE = {
|
||
'Matilda': "Matilda a fait des études de gestion avant de passer quinze ans comme assistante de direction dans une PME industrielle familiale, où elle a appris à anticiper les besoins de son dirigeant avant même qu'il les formule. Rigoureuse sur les échéances, elle relit toujours deux fois avant d'envoyer et a un sens aigu de la confidentialité — ce qui passe par elle n'en ressort jamais sans son accord. Plutôt directe à l'oral, elle préfère dire clairement ce qui ne va pas plutôt que de laisser traîner un doute.",
|
||
'Viktor': "Viktor a démarré comme commercial terrain dans la distribution avant de se spécialiser dans la vente B2B pour des PME de services. Énergique et optimiste, il croit qu'un bon relationnel finit toujours par payer — quitte à pousser un peu fort quand il sent une affaire prête à aboutir. Il déteste laisser un prospect refroidir et relance volontiers plus tôt que prévu. Un travers qu'on lui connaît : il a tendance à sous-estimer le temps qu'une négociation de prix peut prendre côté client.",
|
||
'Léon': "Léon a passé plus de vingt-cinq ans comme contrôleur de gestion dans plusieurs entreprises avant de rejoindre l'équipe sur le tard, avec la conviction qu'une affaire mal chiffrée finit toujours par coûter cher. Posé, mesuré dans ses propos, il prend le temps de vérifier avant de conclure et n'hésite jamais à freiner un enthousiasme collectif s'il juge une marge trop courte. Mieux vaut arriver avec de vrais chiffres quand on lui soumet un dossier.",
|
||
'Nikita': "Nikita vient du monde de la veille digitale et des réseaux sociaux, où elle a appris à recouper une information avant de s'y fier. Curieuse et sociable, elle aime creuser au-delà du premier résultat et préfère dire honnêtement qu'elle n'a rien trouvé plutôt que d'avancer une réponse rassurante mais bancale. Elle se méfie instinctivement d'un profil qui n'existe nulle part en ligne.",
|
||
'Sophia': "Sophia a un parcours de communication et de storytelling de marque, passée par une agence avant de rejoindre l'équipe. Créative et spontanée, elle voit dans chaque situation — bonne ou mauvaise — une histoire à raconter, et pense volontiers en termes d'image et de témoignage client plutôt qu'en termes de process. Elle a tendance à s'enthousiasmer vite pour une idée, quitte à devoir être ramenée aux contraintes concrètes du terrain."
|
||
};
|
||
|
||
const overlayCollab = document.getElementById('overlay-collab');
|
||
const overlayCollabTitre = document.getElementById('overlay-collab-titre');
|
||
const overlayCollabPhotos = document.getElementById('overlay-collab-photos');
|
||
const corpsIdentite = document.getElementById('corps-identite');
|
||
const corpsTest = document.getElementById('corps-test');
|
||
const texteIdentite = document.getElementById('texte-identite');
|
||
const messageIdentite = document.getElementById('message-identite');
|
||
const btnAbandonnerIdentite = document.getElementById('btn-abandonner-identite');
|
||
const btnValiderIdentite = document.getElementById('btn-valider-identite');
|
||
const chatMessages = document.getElementById('chat-messages');
|
||
const chatInput = document.getElementById('chat-input');
|
||
const btnChatEnvoyer = document.getElementById('btn-chat-envoyer');
|
||
const btnFermerTestIdentite = document.getElementById('btn-fermer-test-identite');
|
||
const compteurIdentite = document.getElementById('compteur-identite');
|
||
|
||
// AGN_Identite est une colonne MEDIUMTEXT (16 777 215 octets maximum en
|
||
// MySQL — élargie le 10 septembre 2026 depuis TEXT, à la demande de
|
||
// Jean-Marc : ce texte est voué à s'enrichir avec l'usage réel de
|
||
// l'application plutôt qu'à rester une biographie figée écrite une fois —
|
||
// voir collaborateurs_identite.php, en-tête, et compteur-identite en HTML
|
||
// plus haut).
|
||
const TEXTE_IDENTITE_MAX_OCTETS = 16777215;
|
||
const codeurUtf8 = new TextEncoder();
|
||
|
||
function mettreAJourCompteurIdentite() {
|
||
const octets = codeurUtf8.encode(texteIdentite.value).length;
|
||
compteurIdentite.textContent = octets.toLocaleString('fr-FR') + ' / ' +
|
||
TEXTE_IDENTITE_MAX_OCTETS.toLocaleString('fr-FR') + ' octets';
|
||
const depasse = octets > TEXTE_IDENTITE_MAX_OCTETS;
|
||
compteurIdentite.className = depasse ? 'limite' : (octets > TEXTE_IDENTITE_MAX_OCTETS * 0.9 ? 'avertissement' : '');
|
||
btnValiderIdentite.disabled = depasse;
|
||
return depasse;
|
||
}
|
||
|
||
texteIdentite.addEventListener('input', mettreAJourCompteurIdentite);
|
||
|
||
let collaborateursIdentite = null; // rempli au premier chargement réussi de collaborateurs_identite.php
|
||
let selectionIdentiteIncr = null;
|
||
let modeOverlayActif = null; // 'identite' | 'test' | null — jamais les deux en même temps
|
||
let collaborateurTest = null; // collaborateur choisi pour la discussion "Tester l'identité" en cours
|
||
let historiqueChatTest = []; // [{role:'user'|'assistant', content:'...'}] — API sans état, tout réexpédié à chaque appel
|
||
|
||
// Construit la ligne de photos cliquables, partagée entre les deux
|
||
// écrans (Construire/Tester) — même liste de collaborateurs, seule la
|
||
// réaction au clic diffère (onClic).
|
||
function remplirLignePhotos(conteneur, collaborateurs, onClic) {
|
||
conteneur.innerHTML = '';
|
||
collaborateurs.forEach(function (collab) {
|
||
const bouton = document.createElement('button');
|
||
bouton.type = 'button';
|
||
bouton.className = 'photo-collab-selection';
|
||
bouton.dataset.incr = collab.AGN_Incr;
|
||
|
||
if (collab.AGN_Photo) {
|
||
const img = document.createElement('img');
|
||
img.src = collab.AGN_Photo;
|
||
img.alt = collab.AGN_Prenom || '';
|
||
bouton.appendChild(img);
|
||
} else {
|
||
const repli = document.createElement('div');
|
||
repli.className = 'repli';
|
||
repli.textContent = '?';
|
||
bouton.appendChild(repli);
|
||
}
|
||
|
||
const prenom = document.createElement('div');
|
||
prenom.className = 'prenom-collab';
|
||
prenom.textContent = collab.AGN_Prenom || '';
|
||
bouton.appendChild(prenom);
|
||
|
||
if (collab.AGN_Fonction) {
|
||
const fonction = document.createElement('div');
|
||
fonction.className = 'fonction-collab';
|
||
fonction.textContent = collab.AGN_Fonction;
|
||
bouton.appendChild(fonction);
|
||
}
|
||
|
||
bouton.addEventListener('click', function () { onClic(collab, bouton); });
|
||
conteneur.appendChild(bouton);
|
||
});
|
||
}
|
||
|
||
function marquerPhotoSelectionnee(conteneur, incr) {
|
||
conteneur.querySelectorAll('.photo-collab-selection').forEach(function (b) {
|
||
b.classList.toggle('selectionne', Number(b.dataset.incr) === Number(incr));
|
||
});
|
||
}
|
||
|
||
// Referme l'écran plein cadre quel que soit le mode en cours, et remet les
|
||
// deux boutons déclencheurs dans leur état au repos.
|
||
function fermerOverlay() {
|
||
overlayCollab.hidden = true;
|
||
corpsIdentite.hidden = true;
|
||
corpsTest.hidden = true;
|
||
btnConstruireIdentite.classList.remove('actif');
|
||
btnConstruireIdentite.textContent = 'Construire une identité';
|
||
btnTesterIdentite.classList.remove('actif');
|
||
btnTesterIdentite.textContent = "Tester l'identité";
|
||
modeOverlayActif = null;
|
||
selectionIdentiteIncr = null;
|
||
collaborateurTest = null;
|
||
}
|
||
|
||
// -------- "Construire une identité" --------
|
||
async function ouvrirConstruireIdentite() {
|
||
modeOverlayActif = 'identite';
|
||
overlayCollab.hidden = false;
|
||
corpsIdentite.hidden = true; // tant qu'aucune photo n'est choisie
|
||
corpsTest.hidden = true;
|
||
overlayCollabTitre.textContent = 'Construire une identité — cliquer sur une photo';
|
||
overlayCollabPhotos.innerHTML = 'Chargement…';
|
||
btnConstruireIdentite.classList.add('actif');
|
||
btnConstruireIdentite.textContent = 'Fermer';
|
||
btnTesterIdentite.classList.remove('actif');
|
||
btnTesterIdentite.textContent = "Tester l'identité";
|
||
selectionIdentiteIncr = null;
|
||
|
||
if (!collaborateursIdentite) {
|
||
try {
|
||
const reponse = await fetch('collaborateurs_identite.php');
|
||
const data = await reponse.json();
|
||
if (!reponse.ok) throw new Error(data.error || ('HTTP ' + reponse.status));
|
||
collaborateursIdentite = data.collaborateurs || [];
|
||
} catch (erreur) {
|
||
overlayCollabPhotos.textContent = 'Impossible de charger les identités : ' + erreur.message;
|
||
return;
|
||
}
|
||
}
|
||
remplirLignePhotos(overlayCollabPhotos, collaborateursIdentite, selectionnerCollaborateurIdentite);
|
||
}
|
||
|
||
function selectionnerCollaborateurIdentite(collab, bouton) {
|
||
selectionIdentiteIncr = collab.AGN_Incr;
|
||
marquerPhotoSelectionnee(overlayCollabPhotos, collab.AGN_Incr);
|
||
overlayCollabTitre.textContent = 'Identité de ' + (collab.AGN_Prenom || '') +
|
||
(collab.AGN_Fonction ? ' — ' + collab.AGN_Fonction : '');
|
||
texteIdentite.value = collab.AGN_Identite || EBAUCHES_IDENTITE[collab.AGN_Prenom] || '';
|
||
messageIdentite.textContent = '';
|
||
messageIdentite.className = '';
|
||
mettreAJourCompteurIdentite();
|
||
corpsIdentite.hidden = false;
|
||
texteIdentite.focus();
|
||
}
|
||
|
||
btnAbandonnerIdentite.addEventListener('click', fermerOverlay);
|
||
|
||
btnValiderIdentite.addEventListener('click', async function () {
|
||
if (!selectionIdentiteIncr) return;
|
||
if (mettreAJourCompteurIdentite()) return; // garde-fou : ne jamais envoyer un texte au-delà de ce que la colonne MEDIUMTEXT peut stocker
|
||
const bouton = this;
|
||
bouton.disabled = true;
|
||
messageIdentite.textContent = '';
|
||
messageIdentite.className = '';
|
||
try {
|
||
const reponse = await fetch('collaborateurs_identite.php', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ AGN_Incr: selectionIdentiteIncr, AGN_Identite: texteIdentite.value })
|
||
});
|
||
const data = await reponse.json();
|
||
bouton.disabled = false;
|
||
if (!reponse.ok) {
|
||
messageIdentite.textContent = data.error || 'Erreur';
|
||
messageIdentite.className = 'ko';
|
||
return;
|
||
}
|
||
// Mémorise la valeur enregistrée localement, pour que la prochaine
|
||
// sélection de ce même collaborateur affiche le texte à jour sans
|
||
// recharger tout le tableau.
|
||
const entree = collaborateursIdentite.find(function (c) { return c.AGN_Incr === selectionIdentiteIncr; });
|
||
if (entree) entree.AGN_Identite = texteIdentite.value;
|
||
messageIdentite.textContent = 'Enregistré.';
|
||
messageIdentite.className = 'ok';
|
||
setTimeout(fermerOverlay, 900);
|
||
} catch (erreur) {
|
||
bouton.disabled = false;
|
||
messageIdentite.textContent = 'Erreur réseau';
|
||
messageIdentite.className = 'ko';
|
||
}
|
||
});
|
||
|
||
btnConstruireIdentite.addEventListener('click', function () {
|
||
if (modeOverlayActif === 'identite') {
|
||
fermerOverlay();
|
||
} else {
|
||
ouvrirConstruireIdentite();
|
||
}
|
||
});
|
||
|
||
// -------- "Tester l'identité" --------
|
||
// Dialogue en direct avec le collaborateur choisi, via l'API Anthropic
|
||
// (collaborateur_dialogue.php, voir ce fichier) — l'identité elle-même
|
||
// (AGN_Identite) n'est JAMAIS renvoyée au front-end, seule la voix du
|
||
// collaborateur en sort, dans ses réponses.
|
||
function ajouterBulleChat(role, texte, couleur) {
|
||
const bulle = document.createElement('div');
|
||
bulle.className = 'bulle-chat ' + role; // 'moi' | 'collab' | 'systeme'
|
||
if (role === 'collab') bulle.style.background = couleur || '#ccc';
|
||
bulle.textContent = texte;
|
||
chatMessages.appendChild(bulle);
|
||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||
return bulle;
|
||
}
|
||
|
||
function ouvrirTesterIdentite() {
|
||
modeOverlayActif = 'test';
|
||
overlayCollab.hidden = false;
|
||
corpsIdentite.hidden = true;
|
||
corpsTest.hidden = true; // tant qu'aucune photo n'est choisie
|
||
overlayCollabTitre.textContent = "Tester l'identité — cliquer sur une photo";
|
||
remplirLignePhotos(overlayCollabPhotos, agentsEcranAssistants, selectionnerCollaborateurTest);
|
||
btnTesterIdentite.classList.add('actif');
|
||
btnTesterIdentite.textContent = 'Fermer';
|
||
btnConstruireIdentite.classList.remove('actif');
|
||
btnConstruireIdentite.textContent = 'Construire une identité';
|
||
collaborateurTest = null;
|
||
}
|
||
|
||
function selectionnerCollaborateurTest(collab, bouton) {
|
||
collaborateurTest = collab;
|
||
historiqueChatTest = [];
|
||
marquerPhotoSelectionnee(overlayCollabPhotos, collab.AGN_Incr);
|
||
overlayCollabTitre.textContent = 'Discussion avec ' + (collab.AGN_Prenom || '') +
|
||
(collab.AGN_Fonction ? ' — ' + collab.AGN_Fonction : '');
|
||
chatMessages.innerHTML = '';
|
||
ajouterBulleChat('systeme', 'Session de mise au point — ' + (collab.AGN_Prenom || 'ce collaborateur') + ' ne voit jamais ce message.');
|
||
corpsTest.hidden = false;
|
||
chatInput.value = '';
|
||
chatInput.focus();
|
||
}
|
||
|
||
async function envoyerMessageTest() {
|
||
const texte = chatInput.value.trim();
|
||
if (!texte || !collaborateurTest) return;
|
||
chatInput.value = '';
|
||
chatInput.disabled = true;
|
||
btnChatEnvoyer.disabled = true;
|
||
ajouterBulleChat('moi', texte);
|
||
historiqueChatTest.push({ role: 'user', content: texte });
|
||
const bulleAttente = ajouterBulleChat('systeme', (collaborateurTest.AGN_Prenom || 'Le collaborateur') + ' réfléchit…');
|
||
try {
|
||
const reponse = await fetch('collaborateur_dialogue.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ AGN_Incr: collaborateurTest.AGN_Incr, messages: historiqueChatTest })
|
||
});
|
||
const data = await reponse.json();
|
||
bulleAttente.remove();
|
||
if (!reponse.ok) {
|
||
ajouterBulleChat('systeme', data.error || 'Erreur');
|
||
historiqueChatTest.pop(); // pas de réponse obtenue : on ne le garde pas dans l'historique réexpédié au prochain essai
|
||
return;
|
||
}
|
||
historiqueChatTest.push({ role: 'assistant', content: data.reponse });
|
||
ajouterBulleChat('collab', data.reponse, collaborateurTest.AGN_Couleur);
|
||
} catch (erreur) {
|
||
bulleAttente.remove();
|
||
ajouterBulleChat('systeme', 'Erreur réseau');
|
||
historiqueChatTest.pop();
|
||
} finally {
|
||
chatInput.disabled = false;
|
||
btnChatEnvoyer.disabled = false;
|
||
chatInput.focus();
|
||
}
|
||
}
|
||
|
||
btnChatEnvoyer.addEventListener('click', envoyerMessageTest);
|
||
chatInput.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
envoyerMessageTest();
|
||
}
|
||
});
|
||
|
||
btnFermerTestIdentite.addEventListener('click', fermerOverlay);
|
||
|
||
btnTesterIdentite.addEventListener('click', function () {
|
||
if (modeOverlayActif === 'test') {
|
||
fermerOverlay();
|
||
} else {
|
||
ouvrirTesterIdentite();
|
||
}
|
||
});
|
||
|
||
// 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>
|