// ==UserScript==
// @name Click&Fit
// @namespace https://precilens.com/
// @version 4.9
// @description Module Click&Fit avec upload topographies et autres fonctionnalités V4
// @author Precilens
// @match https://click-fit.precilens.com/*
// @icon https://www.precilens.fr/favicon.ico
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// Double clique sur la consultation
document.addEventListener('dblclick', function(e) {
let el = e.target;
while (el && el !== document && (!el.id || !el.id.startsWith('file-'))) {
el = el.parentElement;
}
if (el && el.id && el.id.startsWith('file-')) {
const openBtn = document.querySelector(
'#wrapper > main > app-home > div > div:nth-child(3) > app-file-preview > div > div > amds-button.file-open-btn.hydrated > button'
);
if (openBtn) {
openBtn.click();
} else {
}
}
});
// Style CSS du fichier
function injectStyles() {
const styleId = 'click-fit-custom-styles';
if (document.getElementById(styleId)) return;
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.modal textarea#input-content {
min-height: 150px !important;
}
/* CORRECTIF: Agrandir la zone d'édition des notes existantes */
textarea#input-editContent {
min-height: 200px !important;
max-height: 500px !important;
resize: vertical !important;
font-size: 14px !important;
line-height: 1.4 !important;
padding: 12px !important;
border: 2px solid #e0e0e0 !important;
border-radius: 8px !important;
transition: border-color 0.3s ease, height 0.2s ease !important;
overflow: hidden !important;
box-sizing: border-box !important;
}
textarea#input-editContent:focus {
border-color: #2196f3 !important;
outline: none !important;
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1) !important;
}
/* CORRECTIF: Centrage parfait des modals */
.modal.modal--size-medium {
position: fixed !important;
margin: 0 !important;
z-index: 1050 !important;
transform: none !important;
}
/* BOUTON FLOTTANT ET MENU */
.clickfit-fab {
position: fixed;
bottom: 20px;
right: 20px;
width: 60px;
height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.clickfit-fab:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.clickfit-fab.active {
transform: rotate(45deg);
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.clickfit-fab-icon {
color: white;
font-size: 28px;
font-weight: bold;
transition: transform 0.3s ease;
}
.clickfit-fab-menu {
position: fixed;
bottom: 90px;
right: 20px;
display: flex;
flex-direction: column;
gap: 15px;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.clickfit-fab-menu.active {
opacity: 1;
visibility: visible;
}
.clickfit-fab-option {
background: white;
border-radius: 30px;
padding: 12px 20px;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
white-space: nowrap;
transform: translateX(20px);
opacity: 0;
transition: all 0.3s ease;
}
.clickfit-fab-menu.active .clickfit-fab-option {
transform: translateX(0);
opacity: 1;
}
.clickfit-fab-menu.active .clickfit-fab-option:nth-child(1) {
transition-delay: 0.1s;
}
.clickfit-fab-menu.active .clickfit-fab-option:nth-child(2) {
transition-delay: 0.15s;
}
.clickfit-fab-menu.active .clickfit-fab-option:nth-child(3) {
transition-delay: 0.2s;
}
.clickfit-fab-menu.active .clickfit-fab-option:nth-child(4) {
transition-delay: 0.25s;
}
.clickfit-fab-menu.active .clickfit-fab-option:nth-child(5) {
transition-delay: 0.3s;
}
.clickfit-fab-menu.active .clickfit-fab-option:nth-child(6) {
transition-delay: 0.35s;
}
.clickfit-fab-option:hover {
transform: translateX(-5px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
}
.clickfit-fab-option-icon {
width: 24px;
height: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
}
.clickfit-fab-option-text {
color: #333;
font-size: 14px;
font-weight: 500;
font-family: "Fira Sans", -apple-system, BlinkMacSystemFont;
}
/* Toast notifications */
.clickfit-toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: #333;
color: white;
padding: 12px 24px;
border-radius: 25px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
opacity: 0;
transition: all 0.3s ease;
z-index: 10000;
}
.clickfit-toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* INDICATEUR TOPOGRAPHIES */
.topo-indicator {
position: fixed;
bottom: 100px;
right: 20px;
width: 60px;
height: 60px;
background: #17a2b8;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(23, 162, 184, 0.3);
z-index: 9998;
}
.topo-indicator:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(23, 162, 184, 0.5);
}
.topo-indicator.has-files {
background: #28a745;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 4px 15px rgba(40, 167, 69, 0.4); }
50% { box-shadow: 0 4px 25px rgba(40, 167, 69, 0.6); }
100% { box-shadow: 0 4px 15px rgba(40, 167, 69, 0.4); }
}
.topo-badge {
position: absolute;
top: -5px;
right: -5px;
background: #dc3545;
color: white;
border-radius: 50%;
width: 22px;
height: 22px;
display: none;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
.topo-indicator.has-files .topo-badge {
display: flex;
}
/* DIALOG TOPOGRAPHIES */
.topo-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 15px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
z-index: 10001;
min-width: 500px;
max-width: 90vw;
max-height: 80vh;
overflow: hidden;
display: none;
}
.topo-dialog.show {
display: block;
animation: dialogSlideIn 0.3s ease;
}
@keyframes dialogSlideIn {
from {
opacity: 0;
transform: translate(-50%, -45%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
.topo-dialog-header {
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.topo-dialog-header h3 {
margin: 0;
font-size: 20px;
}
.topo-dialog-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.3s;
}
.topo-dialog-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.topo-dialog-body {
padding: 20px;
max-height: 60vh;
overflow-y: auto;
}
.topo-file-list {
margin: 15px 0;
}
.topo-file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 15px;
margin: 8px 0;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
transition: all 0.3s;
}
.topo-file-item:hover {
background: #e9ecef;
border-color: #17a2b8;
}
.topo-file-info {
display: flex;
align-items: center;
gap: 12px;
}
.topo-file-icon {
font-size: 24px;
}
.topo-file-details {
display: flex;
flex-direction: column;
}
.topo-file-name {
font-weight: 600;
color: #333;
}
.topo-file-meta {
font-size: 12px;
color: #6c757d;
}
.topo-file-status {
padding: 4px 12px;
border-radius: 15px;
font-size: 12px;
font-weight: 500;
}
.topo-file-status.pending {
background: #fff3cd;
color: #856404;
}
.topo-file-status.processing {
background: #cce5ff;
color: #004085;
}
.topo-file-status.success {
background: #d4edda;
color: #155724;
}
.topo-file-status.error {
background: #f8d7da;
color: #721c24;
}
.topo-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #dee2e6;
}
.topo-actions-left {
display: flex;
align-items: center;
gap: 10px;
}
.topo-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.topo-btn-primary {
background: #17a2b8;
color: white;
}
.topo-btn-primary:hover {
background: #138496;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
}
.topo-btn-secondary {
background: #6c757d;
color: white;
}
.topo-btn-secondary:hover {
background: #5a6268;
}
.topo-connection-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #6c757d;
}
.topo-connection-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #dc3545;
}
.topo-connection-dot.connected {
background: #28a745;
}
/* Modal medium: resize and overflow */
.modal.modal--size-medium {
resize: both !important;
overflow: auto !important;
min-width: 400px;
min-height: 200px;
max-width: 95vw !important;
max-height: 90vh !important;
}
/* Indicateur d'œil détecté */
.topo-file-item[data-eye="od"] {
border-left: 4px solid #17a2b8;
}
.topo-file-item[data-eye="og"] {
border-left: 4px solid #28a745;
}
.topo-file-item[data-eye="both"] {
border-left: 4px solid #ffc107;
}
.eye-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
margin-left: 8px;
}
.eye-badge.od {
background: #e3f2fd;
color: #1976d2;
}
.eye-badge.og {
background: #e8f5e9;
color: #388e3c;
}
.eye-badge.both {
background: #fff3e0;
color: #f57c00;
}
`;
document.head.appendChild(style);
console.log('Styles personnalisés injectés');
}
// État de l'auto-save
let autoSaveEnabled = true;
// Afficher une notification
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'clickfit-toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Menu bouton flottant en bas à droite
function toggleMenu() {
const fab = document.querySelector('.clickfit-fab');
const menu = document.querySelector('.clickfit-fab-menu');
fab.classList.toggle('active');
menu.classList.toggle('active');
}
// Passage de l'astigmatisme en rouge si > 1,00 Dioptrie
function recolorAstigmatisme() {
// Chercher TOUS les éléments contenant "Astigmatisme interne"
const allElements = document.querySelectorAll('amds-text div, .amds-text div');
allElements.forEach(el => {
const txt = el.textContent.trim();
// Vérifier que c'est bien l'astigmatisme interne
if (!/Astigmatisme interne/i.test(txt)) return;
console.log('Astigmatisme trouvé:', txt);
// Regex plus flexible pour extraire la valeur
// Accepte différents formats : "1.25", "1,25", "+1.25", "-1.25", "1.25 /", etc.
const patterns = [
/Astigmatisme interne\s*[::]?\s*([-+−]?\d+[.,]\d+)/i,
/Astigmatisme interne\s*[::]?\s*([-+−]?\d+)/i,
/([-+−]?\d+[.,]\d+)\s*(?:D|dioptries)?/i
];
let value = null;
for (let pattern of patterns) {
const match = txt.match(pattern);
if (match) {
value = parseFloat(match[1].replace(',', '.').replace('−', '-'));
break;
}
}
console.log('Valeur extraite:', value);
if (value !== null && Math.abs(value) > 1.00) {
el.style.color = 'red';
el.style.fontWeight = 'bold';
console.log('→ Mise en rouge (>', 1.00);
} else {
el.style.color = '';
el.style.fontWeight = '';
}
});
}
// Observer avec debounce
let debounceTimer;
const observer = new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
recolorAstigmatisme();
}, 100); // Réduit le délai à 100ms
});
// Observer
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
// Exécution
recolorAstigmatisme();
setInterval(recolorAstigmatisme, 2000);
// Bouton flottant
function createFloatingButton() {
if (document.querySelector('.clickfit-fab')) return;
const fab = document.createElement('div');
fab.className = 'clickfit-fab';
fab.innerHTML = '<span class="clickfit-fab-icon">+</span>';
const menu = document.createElement('div');
menu.className = 'clickfit-fab-menu';
const menuOptions = [
{
icon: '📄',
text: 'Copier les paramètres',
action: copyParametersToClipboard
},
// Split View
{
icon: '🖥️',
text: 'Split View',
action: () => {
activateSplitView();
toggleMenu();
}
},
{
icon: '🔭',
text: 'Calcul LRPG (F4)',
action: performLRPGCalculation
},
{
icon: '🔬',
text: 'Calcul OrthoK (F3) ',
action: performOrthoKCalculation
},
{
icon: '📋',
text: 'Dupliquer OD→OG (F2)',
action: duplicateODtoOG
},
{
icon: '👓',
text: 'Coller réfraction (F1)',
action: pasteRefractionFromClipboard
},
// Import Topographie
{
icon: '🖥️',
text: 'Import Topographies',
action: () => {
if (window.DesktopImportModule) {
window.DesktopImportModule.showDesktopImportModal();
} else {
showToast('❌ Module d\'import non chargé');
}
toggleMenu();
}
},
// Créer un client
// {
// icon: '👤',
// text: 'Créer un client',
// action: () => {
// automateClientCreation();
// toggleMenu();
// }
// }
];
menuOptions.forEach(option => {
const optionEl = document.createElement('div');
optionEl.className = 'clickfit-fab-option';
optionEl.innerHTML = `
<div class="clickfit-fab-option-icon">${option.icon}</div>
<div class="clickfit-fab-option-text">${option.text}</div>
`;
optionEl.addEventListener('click', () => {
option.action();
// Éviter double-toggle
});
menu.appendChild(optionEl);
});
document.body.appendChild(fab);
document.body.appendChild(menu);
fab.addEventListener('click', toggleMenu);
document.addEventListener('click', (e) => {
if (!fab.contains(e.target) && !menu.contains(e.target)) {
fab.classList.remove('active');
menu.classList.remove('active');
}
});
}
// Création automatique de client
function automateClientCreation() {
// Cliquer sur "Liste des clients"
const listeBtn = document.querySelector('header nav ul li:nth-child(2) button');
if (!listeBtn) return alert("Bouton 'Liste des clients' introuvable");
listeBtn.click();
setTimeout(() => {
// Cliquer sur "Ajouter un client"
const ajouterBtn = document.querySelector('main app-list amds-button:nth-child(2) button');
if (!ajouterBtn) return alert("Bouton 'Ajouter un client' introuvable");
ajouterBtn.click();
setTimeout(() => {
// Récupérer texte du presse-papiers
navigator.clipboard.readText().then(text => {
const lines = text.split(/\n|<br>|,/).map(l => l.trim()).filter(Boolean);
let compte = lines.find(l => /\d{8}/.test(l))?.match(/\d{8}/)?.[0] ?? '';
let mail = lines.find(l => /@/.test(l)) ?? '';
let tel = lines.find(l => /\d{9,}/.test(l)) ?? '';
// Remplir les champs
document.querySelector('#input-accountNumber')?.focus();
if (document.querySelector('#input-accountNumber')) document.querySelector('#input-accountNumber').value = compte;
document.querySelector('#input-phoneNumber')?.focus();
if (document.querySelector('#input-phoneNumber')) document.querySelector('#input-phoneNumber').value = tel;
document.querySelector('#input-email')?.focus();
if (document.querySelector('#input-email')) document.querySelector('#input-email').value = mail;
}).catch(() => {
alert("Impossible de lire le presse-papiers");
});
}, 500);
}, 500);
}
// Auto-consultation sur clic porteur
(function setupAutoConsultationClick() {
function waitAndClick(selector, maxWait = 4000) {
return new Promise((resolve, reject) => {
const start = Date.now();
const interval = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
el.click();
clearInterval(interval);
resolve(true);
} else if (Date.now() - start > maxWait) {
clearInterval(interval);
reject(`Timeout: ${selector} non trouvé`);
}
}, 100);
});
}
document.body.addEventListener('click', function(e) {
const tr = e.target.closest('#wearer-list-table tr.selectable');
if (!tr) return;
// Éviter double-trigger
if (tr.dataset.autoConsulting) return;
tr.dataset.autoConsulting = "1";
// Attendre Angular
setTimeout(async () => {
// Vérifier sélection
if (!tr.classList.contains('selected')) {
delete tr.dataset.autoConsulting;
return;
}
try {
await waitAndClick(
'#wrapper > main > app-home > div > div:nth-child(2) > app-files-list > div.files.hide-scrollbars.ng-star-inserted > app-file-card:nth-child(1)'
);
// showToast('Consultation ouverte !');
} catch (err) {
console.warn('[AutoConsultation]', err);
} finally {
// Reset
delete tr.dataset.autoConsulting;
}
}, 400);
}, true);
})();
// Copier paramètres
function copyParametersToClipboard() {
try {
const nameElement = document.querySelector('#wrapper > header > div.header__team-selector.ng-star-inserted > app-file-header-info > div > amds-text.file-wearer-name.hydrated > div');
const name = nameElement ? nameElement.textContent.trim() : 'Non renseigné';
const dateOfBirth = '02/01/2015';
const getValue = (selector) => {
const element = document.querySelector(selector);
return element ? element.value : null;
};
const getSelectedText = (selector) => {
const element = document.querySelector(selector);
if (!element) return null;
const selectedOption = element.options[element.selectedIndex];
return selectedOption ? selectedOption.textContent.trim() : null;
};
const formatMaterial = (material) => {
if (!material) return '';
let formatted = material.replace(/^(Boston|Contamac)\s*/i, '');
if (formatted === 'XO 100') {
formatted = 'XO';
}
return formatted;
};
const formatModel = (model) => {
if (!model) return 'Non renseigné';
const parts = model.split(':');
const lastPart = parts[parts.length - 1];
return lastPart.toUpperCase();
};
const buildEyeParameters = (side) => {
const prefix = side === 'right' ? 'right' : 'left';
const diameter = getValue(`#input-${prefix}totalDiameter`);
const k = getValue(`#input-${prefix}kParameter`);
const kp = getValue(`#input-${prefix}steepKParameter`);
const m = getValue(`#input-${prefix}mParameter`);
const h = getValue(`#input-${prefix}hParameter`);
const c = getValue(`#input-${prefix}cParameter`);
const p = getValue(`#input-${prefix}pParameter`);
const pp = getValue(`#input-${prefix}steepPParameter`);
let zof = getValue(`#input-${prefix}backFlatOpticalComplementaryDiameter`);
let zos = getValue(`#input-${prefix}backSteepOpticalComplementaryDiameter`);
if (!zof) {
zof = getValue(`#input-${prefix}backOpticalZoneDiameter`);
}
let params = [];
if (diameter) params.push(`ØT ${diameter}`);
if (k) params.push(`K ${k}`);
if (kp && parseFloat(kp) !== 0) params.push(`Kp ${kp}`);
if (m) {
params.push(`M ${m}`);
} else if (h) {
params.push(`H ${h}`);
}
if (c && parseFloat(c) !== 0) params.push(`C ${c}`);
if (p) params.push(`P ${p}`);
if (pp && parseFloat(pp) !== 0) params.push(`P' ${pp}`);
if (zof && parseFloat(zof) !== 0) {
let myopicParams = [`Zof ${zof}`];
const zosValue = (zos && parseFloat(zos) !== 0) ? zos : zof;
myopicParams.push(`Zos ${zosValue}`);
params.push(`Contrôle myopique : ${myopicParams.join(', ')}`);
}
return params.join(', ');
};
const rightModel = formatModel(getValue('#input-rightmodel'));
const rightMaterial = formatMaterial(getSelectedText('#input-rightmaterial'));
const rightColor = getSelectedText('#input-rightcolor') || '';
const rightParams = buildEyeParameters('right');
let rightModelDesc = rightModel;
if (rightMaterial) rightModelDesc += ` - ${rightMaterial}`;
if (rightColor) rightModelDesc += ` ${rightColor}`;
const leftModel = formatModel(getValue('#input-leftmodel'));
const leftMaterial = formatMaterial(getSelectedText('#input-leftmaterial'));
const leftColor = getSelectedText('#input-leftcolor') || '';
const leftParams = buildEyeParameters('left');
let leftModelDesc = leftModel;
if (leftMaterial) leftModelDesc += ` - ${leftMaterial}`;
if (leftColor) leftModelDesc += ` ${leftColor}`;
const textToCopy = `Porteur
Nom = ${name}
Date de naissance = ${dateOfBirth}
Œil droit
Modèle = ${rightModelDesc}
Paramètres = ${rightParams || 'Non renseignés'}
Œil gauche
Modèle = ${leftModelDesc}
Paramètres = ${leftParams || 'Non renseignés'}`;
navigator.clipboard.writeText(textToCopy).then(() => {
showToast('📄 Paramètres copiés dans le presse-papier !');
console.log('Texte copié :', textToCopy);
}).catch(err => {
console.error('Erreur lors de la copie :', err);
showToast('❌ Erreur lors de la copie');
});
} catch (error) {
console.error('Erreur lors de la récupération des paramètres :', error);
showToast('❌ Erreur lors de la récupération des paramètres');
}
}
// Module Import Topographies
const DesktopImportModule = {
apiUrl: 'http://localhost:8765/api',
currentGroups: [],
init() {
console.log('🖥️ Module Import Topo initialisé');
this.injectModalStyles();
// Debug
window.DIM = this;
},
injectModalStyles() {
if (document.getElementById('desktop-import-styles')) return;
const style = document.createElement('style');
style.id = 'desktop-import-styles';
style.textContent = `
/* Overlay pour bloquer l'arrière-plan */
.dim-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 999998;
}
.dim-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
z-index: 999999;
min-width: 500px;
max-width: 80vw;
max-height: 80vh;
overflow: hidden;
}
.dim-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.dim-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
}
.dim-body {
padding: 20px;
max-height: 60vh;
overflow-y: auto;
}
.dim-group {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
.dim-group.selected-od {
border-color: #2196f3;
background: #e3f2fd;
}
.dim-group.selected-og {
border-color: #4caf50;
background: #e8f5e9;
}
.dim-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
}
.dim-btn {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.dim-btn:hover {
background: #f0f0f0;
}
.dim-btn.active-od {
background: #2196f3;
color: white;
}
.dim-btn.active-og {
background: #4caf50;
color: white;
}
.dim-footer {
padding: 15px 20px;
border-top: 1px solid #ddd;
text-align: center;
}
.dim-import-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
}
.dim-import-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
document.head.appendChild(style);
},
async showDesktopImportModal() {
console.log('🔍 Début scan bureau...');
try {
// Test connexion
const testResponse = await fetch(`${this.apiUrl}/status`);
if (!testResponse.ok) {
throw new Error('Scanner non accessible');
}
// Scanner bureau
const response = await fetch(`${this.apiUrl}/scan-desktop`);
const data = await response.json();
console.log('📊 Réponse scan:', data);
if (!data.success) {
throw new Error(data.error || 'Erreur scan');
}
if (!data.groups || data.groups.length === 0) {
alert('📭 Aucune topographie trouvée sur le bureau');
return;
}
// Stocker groupes
this.currentGroups = data.groups;
// Créer modal
this.createSimpleModal();
} catch (error) {
console.error('❌ Erreur:', error);
alert('❌ Erreur: Vérifiez que le scanner Python est lancé\n\n' + error.message);
}
},
createSimpleModal() {
// Nettoyer modal
this.closeModal();
// Overlay
const overlay = document.createElement('div');
overlay.className = 'dim-overlay';
overlay.id = 'dim-overlay';
// Modal
const modal = document.createElement('div');
modal.className = 'dim-modal';
modal.id = 'dim-modal';
// HTML
let groupsHtml = '';
this.currentGroups.forEach((group, index) => {
groupsHtml += `
<div class="dim-group" data-index="${index}">
<strong>${group.icon} ${group.topographer_name}</strong>
<div style="font-size: 12px; color: #666; margin: 5px 0;">
${group.files.map(f => '• ' + f.split('\\').pop()).join('<br>')}
</div>
<div class="dim-buttons">
<button class="dim-btn dim-select-od" data-index="${index}" data-eye="od">
OD
</button>
<button class="dim-btn dim-select-og" data-index="${index}" data-eye="og">
OG
</button>
<button class="dim-btn dim-select-skip" data-index="${index}" data-eye="skip">
Ignorer
</button>
</div>
</div>
`;
});
modal.innerHTML = `
<div class="dim-header">
<h3>Import depuis le bureau (${this.currentGroups.length} groupes)</h3>
<button class="dim-close" id="dim-close">×</button>
</div>
<div class="dim-body">
${groupsHtml}
</div>
<div class="dim-footer">
<button class="dim-import-btn" id="dim-import">
Lancer l'import
</button>
</div>
`;
// Ajouter DOM
document.body.appendChild(overlay);
document.body.appendChild(modal);
// Attacher événements
this.attachEvents();
console.log('✅ Modal créé avec', this.currentGroups.length, 'groupes');
},
attachEvents() {
const modal = document.getElementById('dim-modal');
if (!modal) {
console.error('Modal non trouvé !');
return;
}
// Fermer modal
document.getElementById('dim-close')?.addEventListener('click', () => {
console.log('Fermeture modal');
this.closeModal();
});
document.getElementById('dim-overlay')?.addEventListener('click', () => {
this.closeModal();
});
// Sélection
modal.querySelectorAll('.dim-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(btn.dataset.index);
const eye = btn.dataset.eye;
console.log(`Clic: Groupe ${index} → ${eye}`);
this.selectGroupEye(index, eye);
});
});
// Tout OD
document.getElementById('dim-all-od')?.addEventListener('click', () => {
console.log('Tout en OD');
this.currentGroups.forEach((g, i) => this.selectGroupEye(i, 'od'));
});
// Tout OG
document.getElementById('dim-all-og')?.addEventListener('click', () => {
console.log('Tout en OG');
this.currentGroups.forEach((g, i) => this.selectGroupEye(i, 'og'));
});
// Import
document.getElementById('dim-import')?.addEventListener('click', () => {
console.log('Lancement import');
this.startImport();
});
// ESC
document.addEventListener('keydown', this.escHandler = (e) => {
if (e.key === 'Escape') this.closeModal();
});
},
selectGroupEye(index, eye) {
const group = this.currentGroups[index];
if (!group) return;
// Mettre à jour
group.selectedEye = eye;
// UI
const groupEl = document.querySelector(`.dim-group[data-index="${index}"]`);
if (groupEl) {
// Reset
groupEl.classList.remove('selected-od', 'selected-og');
groupEl.querySelectorAll('.dim-btn').forEach(b => {
b.classList.remove('active-od', 'active-og');
});
// Classes
if (eye === 'od') {
groupEl.classList.add('selected-od');
groupEl.querySelector('.dim-select-od').classList.add('active-od');
} else if (eye === 'og') {
groupEl.classList.add('selected-og');
groupEl.querySelector('.dim-select-og').classList.add('active-og');
}
}
console.log(`✅ Groupe ${group.topographer_name} → ${eye}`);
},
closeModal() {
document.getElementById('dim-modal')?.remove();
document.getElementById('dim-overlay')?.remove();
if (this.escHandler) {
document.removeEventListener('keydown', this.escHandler);
}
},
async startImport() {
const toImport = this.currentGroups.filter(g => g.selectedEye && g.selectedEye !== 'skip');
if (toImport.length === 0) {
alert('⚠️ Sélectionnez au moins un œil pour l\'import');
return;
}
console.log(`🚀 Import séquentiel de ${toImport.length} groupe(s)...`);
// Fermer modal
this.closeModal();
// Toast
if (window.showToast) {
window.showToast(`🚀 Import de ${toImport.length} groupe(s) en cours...`);
}
let successCount = 0;
let errorCount = 0;
// Import séquentiel
for (let i = 0; i < toImport.length; i++) {
const group = toImport[i];
try {
console.log(`📦 Import ${i + 1}/${toImport.length}: ${group.topographer_name} → ${group.selectedEye.toUpperCase()}`);
// Progression
if (window.showToast) {
window.showToast(`Import ${i + 1}/${toImport.length}: ${group.topographer_name} → ${group.selectedEye.toUpperCase()}`);
}
// Attendre import
await this.performRealImport(group, group.selectedEye);
successCount++;
// Pause
if (i < toImport.length - 1) {
console.log('⏳ Pause avant le prochain import...');
await new Promise(r => setTimeout(r, 2000));
}
} catch (error) {
console.error(`❌ Erreur import ${group.topographer_name}:`, error);
errorCount++;
}
}
// Résultat
// const message = `Terminé: ${successCount} importé(s)` +
// (errorCount > 0 ? `, ${errorCount} erreur(s)` : '');
// if (window.showToast) {
// window.showToast(message);
// } else {
// alert(message);
// }
// Rafraîchir
if (window.TopographyModule) {
window.TopographyModule.checkForFiles();
}
},
async performRealImport(group, eye) {
console.log(`🎯 Import: ${group.topographer_name} vers ${eye.toUpperCase()}`);
try {
// Bouton download
let uploadButton;
if (eye === 'od') {
uploadButton = document.querySelector('app-file-information-eye:nth-child(1) button i.ri-download-2-fill')?.parentElement?.parentElement;
if (!uploadButton) {
uploadButton = document.querySelector('app-file-information-eye:nth-child(1) button:has(i.ri-download-2-fill)');
}
} else {
uploadButton = document.querySelector('app-file-information-eye:nth-child(2) button i.ri-download-2-fill')?.parentElement?.parentElement;
if (!uploadButton) {
uploadButton = document.querySelector('app-file-information-eye:nth-child(2) button:has(i.ri-download-2-fill)');
}
}
if (!uploadButton) {
const allButtons = document.querySelectorAll('button');
const downloadButtons = Array.from(allButtons).filter(b =>
b.querySelector('i.ri-download-2-fill')
);
if (eye === 'od' && downloadButtons[0]) {
uploadButton = downloadButtons[0];
} else if (eye === 'og' && downloadButtons[1]) {
uploadButton = downloadButtons[1];
}
}
if (!uploadButton) {
console.error(`❌ Bouton ${eye} non trouvé`);
alert(`Bouton d'import ${eye.toUpperCase()} non trouvé`);
return;
}
console.log('✅ Bouton trouvé, clic...');
uploadButton.click();
// Attendre modal
await new Promise(r => setTimeout(r, 1500));
// Sélection topographe
const topographerSelect = document.querySelector('#input-topographer');
if (topographerSelect) {
console.log(`🔍 Sélection du topographe: ${group.topographer_name}`);
const topographerMapping = {
'tms4': 'tms_4',
'tms5': 'tms_5',
'medmont': 'medmont_6',
'pentacam': 'oculus_pentacam',
'keratron': 'keratron_scout',
'atlas': 'atlas9000',
'phoenix': 'sirius_phoenix',
'ca200': 'ca200',
'opd_scan': 'opdscan',
'orbscan': 'orbscan',
'keratograph': 'oculus_keratograph',
'easygraph': 'oculus_easygraph'
};
const selectValue = topographerMapping[group.topographer];
if (selectValue) {
topographerSelect.value = selectValue;
topographerSelect.dispatchEvent(new Event('change', { bubbles: true }));
topographerSelect.dispatchEvent(new Event('input', { bubbles: true }));
console.log(`✅ Topographe sélectionné: ${selectValue}`);
await new Promise(r => setTimeout(r, 500));
} else {
console.log(`⚠️ Pas de mapping pour: ${group.topographer}`);
// Recherche par nom
const options = topographerSelect.querySelectorAll('option');
options.forEach(option => {
const optionText = option.textContent.trim().toLowerCase();
const groupName = group.topographer_name.toLowerCase();
if (optionText.includes(groupName) || groupName.includes(optionText)) {
topographerSelect.value = option.value;
topographerSelect.dispatchEvent(new Event('change', { bubbles: true }));
console.log(`✅ Topographe trouvé par nom: ${option.value}`);
}
});
}
}
// Input file
const fileInput = document.querySelector('input[type="file"]');
if (!fileInput) {
console.error('❌ Input file non trouvé');
return;
}
console.log('✅ Modal d\'upload ouvert');
// Charger fichiers
const files = [];
for (const filepath of group.files) {
const filename = filepath.split('/').pop().split('\\').pop();
console.log(`Téléchargement: ${filename}`);
try {
const response = await fetch(`${this.apiUrl}/file/${filename}`);
if (!response.ok) {
console.error(`Fichier non trouvé: ${filename}`);
continue;
}
const blob = await response.blob();
console.log(`Fichier reçu: ${filename} (${blob.size} octets)`);
const file = new File([blob], filename, {
type: blob.type || 'application/octet-stream',
lastModified: Date.now()
});
files.push(file);
} catch (error) {
console.error(`❌ Erreur téléchargement ${filename}:`, error);
}
}
if (files.length === 0) {
console.error('❌ Aucun fichier chargé');
return;
}
console.log(`📊 ${files.length} fichiers prêts`);
// Assigner fichiers
const dt = new DataTransfer();
files.forEach(file => dt.items.add(file));
try {
fileInput.files = dt.files;
} catch(e) {
Object.defineProperty(fileInput, 'files', {
value: dt.files,
writable: false,
configurable: true
});
}
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
console.log('Fichiers assignés à l\'input');
// Attendre un peu
await new Promise(r => setTimeout(r, 800));
// Bouton Importer
const importBtn = Array.from(document.querySelectorAll('button')).find(btn =>
btn.textContent.includes('Importer') && !btn.disabled
);
if (importBtn) {
console.log('✅ Clic sur Importer');
importBtn.click();
// Marquer importé
await this.markAsImported(group.files, group);
// Marquer serveur
await this.markAsImported(group.files);
await new Promise(r => setTimeout(r, 2000));
console.log(' Import terminé');
} else {
console.error(' Bouton Importer non trouvé dans le modal');
}
} catch (error) {
console.error(' Erreur import:', error);
}
},
async loadFilesFromServer(group) {
const files = [];
for (const filepath of group.files) {
try {
// Extraire nom fichier
const filename = filepath.split('/').pop().split('\\').pop();
console.log(` Chargement: ${filename}`);
// Appeler API
const response = await fetch(`${this.apiUrl}/file/${encodeURIComponent(filename)}`);
if (!response.ok) {
console.error(` Erreur chargement ${filename}: ${response.status}`);
continue;
}
const blob = await response.blob();
console.log(` Fichier reçu: ${filename} (${blob.size} octets)`);
// Créer File
const file = new File([blob], filename, {
type: this.getMimeType(filename)
});
files.push(file);
} catch (error) {
console.error(` Erreur chargement fichier:`, error);
}
}
console.log(`📊 Total: ${files.length} fichiers chargés`);
return files;
},
getMimeType(filename) {
const ext = filename.split('.').pop().toLowerCase();
const mimeTypes = {
'tgl': 'application/octet-stream',
'hgt': 'application/octet-stream',
'dst': 'application/octet-stream',
'csv': 'text/csv',
'txt': 'text/plain',
'xml': 'text/xml',
'dat': 'application/octet-stream'
};
return mimeTypes[ext] || 'application/octet-stream';
},
async waitForUploadModal(maxWait = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
const modal = document.querySelector('.modal, app-modal, [role="dialog"]');
if (modal) {
console.log(' Modal d\'upload trouvée');
await new Promise(r => setTimeout(r, 500));
return modal;
}
await new Promise(r => setTimeout(r, 100));
}
throw new Error('Timeout: Modal non trouvée');
},
async findDropzone() {
// Sélecteurs
const selectors = [
'.dropzone.visible',
'.dropzone',
'app-files-dropzone .dropzone',
'[class*="drop"][class*="zone"]',
'.modal input[type="file"]'
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) {
console.log(` Dropzone trouvée: ${selector}`);
return element;
}
}
// Input file parent
const fileInput = document.querySelector('input[type="file"]');
if (fileInput) {
return fileInput.parentElement;
}
return null;
},
async findValidateButton() {
// Sélecteurs bouton
const selectors = [
'.modal__footer button[type="submit"]',
'.modal__footer button.modal-submit-btn',
'.modal button:last-child',
'button[class*="submit"]',
'button[class*="import"]'
];
for (const selector of selectors) {
const button = document.querySelector(selector);
if (button && !button.disabled) {
// Vérifier bouton
const text = button.textContent.toLowerCase();
if (text.includes('import') || text.includes('valid') || text.includes('ok')) {
return button;
}
}
}
// Recherche texte
const allButtons = document.querySelectorAll('.modal button, app-modal button');
for (const button of allButtons) {
const text = button.textContent.toLowerCase();
if ((text.includes('import') || text.includes('valid')) && !button.disabled) {
return button;
}
}
return null;
},
async waitForModalClose(maxWait = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < maxWait) {
const modal = document.querySelector('.modal, app-modal');
if (!modal) {
console.log(' Modal fermée');
return;
}
await new Promise(r => setTimeout(r, 100));
}
},
async markAsImported(files, groupInfo = null) {
try {
// Vérifier TMS-4
const keepXref = groupInfo && groupInfo.topographer === 'tms4' && groupInfo.multiple_eyes;
await fetch(`${this.apiUrl}/mark-imported`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
files: files,
move: true,
keep_xref: keepXref
})
});
console.log('Fichiers marqués comme importés' + (keepXref ? ' (XREF conservé)' : ''));
} catch (error) {
console.error('Erreur marquage:', error);
}
}
};
// Global
window.DesktopImportModule = DesktopImportModule;
// Auto-init
setTimeout(() => {
if (window.DesktopImportModule) {
window.DesktopImportModule.init();
console.log('Module Import Topographie prêt');
}
}, 2000);
// Dupliquer OD vers OG
function duplicateODtoOG() {
console.log('Début de la duplication OD→OG');
const mappings = [
{
from: '#input-rightsphere',
to: '#input-leftsphere',
name: 'Sphère'
},
{
from: '#input-rightcylinder',
to: '#input-leftcylinder',
name: 'Cylindre'
},
{
from: '#input-rightrefractionAxis',
to: '#input-leftrefractionAxis',
name: 'Axe'
}
];
let copiedCount = 0;
let errorMessages = [];
mappings.forEach(mapping => {
const fromInput = document.querySelector(mapping.from);
const toInput = document.querySelector(mapping.to);
if (fromInput && toInput) {
const value = fromInput.value;
if (value && value !== '') {
toInput.value = value;
toInput.dispatchEvent(new Event('input', { bubbles: true }));
toInput.dispatchEvent(new Event('change', { bubbles: true }));
if (mapping.name === 'Sphère' || mapping.name === 'Cylindre') {
const numValue = parseFloat(value);
if (!isNaN(numValue)) {
toInput.value = numValue.toFixed(2);
}
}
console.log(` ${mapping.name}: ${value} copié de OD vers OG`);
copiedCount++;
}
} else {
if (!fromInput) {
errorMessages.push(`Input source ${mapping.from} introuvable`);
}
if (!toInput) {
errorMessages.push(`Input destination ${mapping.to} introuvable`);
}
}
});
if (copiedCount > 0) {
showToast(` ${copiedCount} valeur(s) copiée(s) de OD vers OG`);
} else if (errorMessages.length > 0) {
showToast(' Erreur lors de la duplication');
console.error('Erreurs de duplication:', errorMessages);
} else {
showToast(' Aucune valeur à copier');
}
}
// Calcul OrthoK
async function performOrthoKCalculation() {
showToast('🔬 Démarrage du calcul OrthoK...');
console.log('🔬 Début du calcul OrthoK');
try {
// Onglet lentille
const lensTab = document.querySelector('#wrapper > main > app-file-layout > div > app-tabs-list > div.tabs-container.has-actions > div.tabs.theme--classic.has-separators > div.tab.lens-0-tab.clickable.ng-star-inserted');
if (lensTab) {
lensTab.click();
console.log(' Clic sur l\'onglet lentille');
await new Promise(resolve => setTimeout(resolve, 1000));
} else {
console.warn('Onglet lentille non trouvé, on continue...');
}
// Sélection OrthoK pour OD
const rightTypeSelect = document.querySelector('#input-righttype');
if (rightTypeSelect) {
rightTypeSelect.value = 'lens:type:orthok';
rightTypeSelect.dispatchEvent(new Event('change', { bubbles: true }));
rightTypeSelect.dispatchEvent(new Event('input', { bubbles: true }));
console.log(' OrthoK sélectionné pour OD');
} else {
console.error(' Select OD non trouvé');
}
await new Promise(resolve => setTimeout(resolve, 500));
// Sélection OrthoK pour OG
const leftTypeSelect = document.querySelector('#input-lefttype');
if (leftTypeSelect) {
leftTypeSelect.value = 'lens:type:orthok';
leftTypeSelect.dispatchEvent(new Event('change', { bubbles: true }));
leftTypeSelect.dispatchEvent(new Event('input', { bubbles: true }));
console.log(' OrthoK sélectionné pour OG');
} else {
console.error(' Select OG non trouvé');
}
await new Promise(resolve => setTimeout(resolve, 500));
// Déclencher calcul
if (rightTypeSelect) rightTypeSelect.blur();
if (leftTypeSelect) leftTypeSelect.blur();
// Bouton validation
const validateBtn = document.querySelector('.modal__footer button.modal-submit-btn, .modal__footer button[type="submit"]');
if (validateBtn && !validateBtn.disabled) {
validateBtn.click();
console.log(' Validation du calcul OrthoK');
}
showToast(' Calcul OrthoK terminé avec succès !');
console.log(' Calcul OrthoK complété');
} catch (error) {
console.error(' Erreur lors du calcul OrthoK:', error);
showToast(' Erreur lors du calcul OrthoK');
}
}
// Calcul LRPG
async function performLRPGCalculation() {
showToast('Démarrage du calcul LRPG...');
console.log('Début du calcul LRPG');
try {
const lensTab = document.querySelector('#wrapper > main > app-file-layout > div > app-tabs-list > div.tabs-container.has-actions > div.tabs.theme--classic.has-separators > div.tab.lens-0-tab.clickable.ng-star-inserted');
if (!lensTab) {
showToast('Onglet lentille introuvable');
console.error('Onglet lentille non trouvé');
return;
}
lensTab.click();
console.log('Clic sur l\'onglet lentille');
await new Promise(resolve => setTimeout(resolve, 1000));
const rightTypeSelect = document.querySelector('#input-righttype');
if (rightTypeSelect) {
rightTypeSelect.value = 'lens:type:rigid';
rightTypeSelect.dispatchEvent(new Event('change', { bubbles: true }));
console.log('LRPG (Rigide) sélectionné pour OD');
} else {
console.error('Select OD non trouvé');
}
await new Promise(resolve => setTimeout(resolve, 500));
const leftTypeSelect = document.querySelector('#input-lefttype');
if (leftTypeSelect) {
leftTypeSelect.value = 'lens:type:rigid';
leftTypeSelect.dispatchEvent(new Event('change', { bubbles: true }));
console.log('LRPG (Rigide) sélectionné pour OG');
} else {
console.error(' Select OG non trouvé');
}
await new Promise(resolve => setTimeout(resolve, 500));
showToast(' Calcul LRPG terminé avec succès !');
console.log(' Calcul LRPG complété');
} catch (error) {
console.error(' Erreur lors du calcul LRPG:', error);
showToast(' Erreur lors du calcul LRPG');
}
}
// Coller réfraction
async function pasteRefractionFromClipboard() {
console.log('👓 Tentative de collage de réfraction...');
try {
const clipboardText = await navigator.clipboard.readText();
console.log('Contenu du presse-papier:', clipboardText);
const patterns = {
compactFormat: /([+-]?\d+[.,]\d{2})\s*\(([+-]?\d+[.,]\d{2})\)\s*(\d+)[°]?/g,
altFormat: /([+-]?\d+[.,]\d{2})\s*\(\s*([+-]?\d+[.,]\d{2})\s*[àa]\s*(\d+)[°]?\s*\)/g,
cylinder: /([+-]?\d+[.,]\d{2})\s*[x×]\s*(\d+)[°]?/g,
sphere: /([+-]?\d+[.,]\d{2})(?!\s*[°])/g
};
const cleanValue = (value) => {
if (!value) return '';
return value.replace(',', '.').trim();
};
const isEyeEmpty = (side) => {
const sphereSelector = side === 'od' ? '#input-rightsphere' : '#input-leftsphere';
const cylinderSelector = side === 'od' ? '#input-rightcylinder' : '#input-leftcylinder';
const axisSelector = side === 'od' ? '#input-rightrefractionAxis' : '#input-leftrefractionAxis';
const sphereInput = document.querySelector(sphereSelector);
const cylinderInput = document.querySelector(cylinderSelector);
const axisInput = document.querySelector(axisSelector);
const isEmpty = (input) => {
if (!input || !input.value || input.value === '') return true;
const numValue = parseFloat(input.value);
return numValue === 0;
};
return isEmpty(sphereInput) && isEmpty(cylinderInput) && isEmpty(axisInput);
};
let detectedValues = {
od: { sphere: null, cylinder: null, axis: null },
og: { sphere: null, cylinder: null, axis: null }
};
const lines = clipboardText.split('\n').map(l => l.trim()).filter(l => l);
let currentEye = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (/\b(OD|RE|Œil\s*droit|Oeil\s*droit|Right)/i.test(line)) {
currentEye = 'od';
console.log('👁️ Détection OD à la ligne', i);
} else if (/\b(OG|OS|LE|Œil\s*gauche|Oeil\s*gauche|Left)/i.test(line)) {
currentEye = 'og';
console.log('👁️ Détection OG à la ligne', i);
}
patterns.compactFormat.lastIndex = 0;
patterns.altFormat.lastIndex = 0;
patterns.cylinder.lastIndex = 0;
patterns.sphere.lastIndex = 0;
const compactMatch = patterns.compactFormat.exec(line);
if (compactMatch && currentEye) {
detectedValues[currentEye].sphere = cleanValue(compactMatch[1]);
detectedValues[currentEye].cylinder = cleanValue(compactMatch[2]);
detectedValues[currentEye].axis = compactMatch[3];
console.log(`✅ Format compact détecté pour ${currentEye}:`, detectedValues[currentEye]);
continue;
}
patterns.altFormat.lastIndex = 0;
const altMatch = patterns.altFormat.exec(line);
if (altMatch && currentEye) {
detectedValues[currentEye].sphere = cleanValue(altMatch[1]);
detectedValues[currentEye].cylinder = cleanValue(altMatch[2]);
detectedValues[currentEye].axis = altMatch[3];
console.log(`✅ Format alternatif détecté pour ${currentEye}:`, detectedValues[currentEye]);
continue;
}
patterns.cylinder.lastIndex = 0;
const cylMatch = patterns.cylinder.exec(line);
if (cylMatch && currentEye) {
detectedValues[currentEye].cylinder = cleanValue(cylMatch[1]);
detectedValues[currentEye].axis = cylMatch[2];
console.log(`📐 Cylindre détecté pour ${currentEye}:`, cylMatch[1], 'x', cylMatch[2]);
}
patterns.sphere.lastIndex = 0;
const sphereMatches = line.match(patterns.sphere);
if (sphereMatches && currentEye) {
for (const match of sphereMatches) {
const cleanMatch = cleanValue(match);
if (cleanMatch !== detectedValues[currentEye].cylinder) {
detectedValues[currentEye].sphere = cleanMatch;
console.log(`🔵 Sphère détectée pour ${currentEye}:`, cleanMatch);
break;
}
}
}
}
if (!detectedValues.od.sphere && !detectedValues.og.sphere &&
!detectedValues.od.cylinder && !detectedValues.og.cylinder) {
console.log('⚠️ Pas d\'identification OD/OG, tentative par ordre...');
const refractionLines = lines.filter(line => {
return /([+-]?\d+[.,]\d+)/.test(line);
});
console.log(`📊 Nombre de lignes de réfraction trouvées: ${refractionLines.length}`);
if (refractionLines.length >= 1) {
const firstLine = refractionLines[0];
console.log('📝 Analyse première ligne:', firstLine);
patterns.compactFormat.lastIndex = 0;
patterns.altFormat.lastIndex = 0;
patterns.cylinder.lastIndex = 0;
patterns.sphere.lastIndex = 0;
let compactMatch = patterns.compactFormat.exec(firstLine);
if (compactMatch) {
detectedValues.od.sphere = cleanValue(compactMatch[1]);
detectedValues.od.cylinder = cleanValue(compactMatch[2]);
detectedValues.od.axis = compactMatch[3];
console.log('✅ Format compact détecté pour OD (par ordre):', detectedValues.od);
} else {
patterns.altFormat.lastIndex = 0;
let altMatch = patterns.altFormat.exec(firstLine);
if (altMatch) {
detectedValues.od.sphere = cleanValue(altMatch[1]);
detectedValues.od.cylinder = cleanValue(altMatch[2]);
detectedValues.od.axis = altMatch[3];
} else {
patterns.sphere.lastIndex = 0;
let sphereMatch = patterns.sphere.exec(firstLine);
if (sphereMatch) detectedValues.od.sphere = cleanValue(sphereMatch[1]);
patterns.cylinder.lastIndex = 0;
let cylMatch = patterns.cylinder.exec(firstLine);
if (cylMatch) {
detectedValues.od.cylinder = cleanValue(cylMatch[1]);
detectedValues.od.axis = cylMatch[2];
}
}
}
}
if (refractionLines.length >= 2) {
const secondLine = refractionLines[1];
console.log('📝 Analyse deuxième ligne:', secondLine);
patterns.compactFormat.lastIndex = 0;
patterns.altFormat.lastIndex = 0;
patterns.cylinder.lastIndex = 0;
patterns.sphere.lastIndex = 0;
let compactMatch = patterns.compactFormat.exec(secondLine);
if (compactMatch) {
detectedValues.og.sphere = cleanValue(compactMatch[1]);
detectedValues.og.cylinder = cleanValue(compactMatch[2]);
detectedValues.og.axis = compactMatch[3];
console.log('✅ Format compact détecté pour OG (par ordre):', detectedValues.og);
} else {
patterns.altFormat.lastIndex = 0;
let altMatch = patterns.altFormat.exec(secondLine);
if (altMatch) {
detectedValues.og.sphere = cleanValue(altMatch[1]);
detectedValues.og.cylinder = cleanValue(altMatch[2]);
detectedValues.og.axis = altMatch[3];
} else {
patterns.sphere.lastIndex = 0;
let sphereMatch = patterns.sphere.exec(secondLine);
if (sphereMatch) detectedValues.og.sphere = cleanValue(sphereMatch[1]);
patterns.cylinder.lastIndex = 0;
let cylMatch = patterns.cylinder.exec(secondLine);
if (cylMatch) {
detectedValues.og.cylinder = cleanValue(cylMatch[1]);
detectedValues.og.axis = cylMatch[2];
}
}
}
}
}
let fieldsUpdated = 0;
const fillField = async (selector, value, fieldName) => {
console.log(`� Tentative de remplissage: ${fieldName} avec la valeur "${value}"`);
if (!value || value === 'null' || value === 'undefined') {
console.log(`⚠ Valeur vide ou invalide pour ${fieldName}`);
return false;
}
const input = document.querySelector(selector);
if (!input) {
console.error(`❌ Champ non trouvé: ${selector}`);
return false;
}
let cleanedValue = value.toString().replace(',', '.').replace(/^\+/, '').trim();
if (fieldName.includes('Axe')) {
cleanedValue = parseInt(cleanedValue).toString();
} else {
const numValue = parseFloat(cleanedValue);
if (isNaN(numValue)) {
console.log(`⚠️ Valeur non numérique pour ${fieldName}: ${cleanedValue}`);
return false;
}
cleanedValue = numValue.toFixed(2);
}
try {
input.focus();
await new Promise(r => setTimeout(r, 50));
input.value = '';
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
).set;
nativeInputValueSetter.call(input, cleanedValue);
const inputEvent = new Event('input', { bubbles: true, cancelable: true });
input.dispatchEvent(inputEvent);
await new Promise(r => setTimeout(r, 50));
const changeEvent = new Event('change', { bubbles: true, cancelable: true });
input.dispatchEvent(changeEvent);
input.blur();
await new Promise(r => setTimeout(r, 50));
input.focus();
console.log(` ${fieldName}: ${value} → ${cleanedValue} (valeur finale: ${input.value})`);
return true;
} catch (error) {
console.error(`❌ Erreur lors du remplissage de ${fieldName}:`, error);
return false;
}
};
if ((detectedValues.od.sphere || detectedValues.od.cylinder) &&
(detectedValues.og.sphere || detectedValues.og.cylinder)) {
if (await fillField('#input-rightsphere', detectedValues.od.sphere, 'Sphère OD')) fieldsUpdated++;
await new Promise(r => setTimeout(r, 100));
if (await fillField('#input-rightcylinder', detectedValues.od.cylinder, 'Cylindre OD')) fieldsUpdated++;
await new Promise(r => setTimeout(r, 100));
if (await fillField('#input-rightrefractionAxis', detectedValues.od.axis, 'Axe OD')) fieldsUpdated++;
await new Promise(r => setTimeout(r, 200));
if (await fillField('#input-leftsphere', detectedValues.og.sphere, 'Sphère OG')) fieldsUpdated++;
await new Promise(r => setTimeout(r, 100));
if (await fillField('#input-leftcylinder', detectedValues.og.cylinder, 'Cylindre OG')) fieldsUpdated++;
await new Promise(r => setTimeout(r, 100));
if (await fillField('#input-leftrefractionAxis', detectedValues.og.axis, 'Axe OG')) fieldsUpdated++;
}
else if (detectedValues.od.sphere || detectedValues.od.cylinder ||
detectedValues.og.sphere || detectedValues.og.cylinder) {
console.log('👁️ Un seul œil détecté ou pas d\'indication');
const values = (detectedValues.od.sphere || detectedValues.od.cylinder) ?
detectedValues.od : detectedValues.og;
if (isEyeEmpty('od')) {
if (await fillField('#input-rightsphere', values.sphere, 'Sphère OD')) fieldsUpdated++;
await new Promise(r => setTimeout(r, 100));
if (await fillField('#input-rightcylinder', values.cylinder, 'Cylindre OD')) fieldsUpdated++;
await new Promise(r => setTimeout(r, 100));
if (await fillField('#input-rightrefractionAxis', values.axis, 'Axe OD')) fieldsUpdated++;
}
else if (isEyeEmpty('og')) {
if (await fillField('#input-leftsphere', values.sphere, 'Sphère OG')) fieldsUpdated++;
await new Promise(r => setTimeout(r, 100));
if (await fillField('#input-leftcylinder', values.cylinder, 'Cylindre OG')) fieldsUpdated++;
await new Promise(r => setTimeout(r, 100));
if (await fillField('#input-leftrefractionAxis', values.axis, 'Axe OG')) fieldsUpdated++;
}
else {
showToast('⚠️ Les deux yeux sont déjà remplis');
return;
}
}
if (fieldsUpdated > 0) {
showToast(`✅ ${fieldsUpdated} valeur(s) collée(s) avec succès !`);
console.log('📊 Résumé du collage:', detectedValues);
} else {
showToast('❌ Aucune valeur de réfraction détectée');
}
} catch (error) {
console.error('❌ Erreur lors du collage:', error);
showToast('❌ Erreur lors de la lecture du presse-papier');
}
}
const selectors = [
"#wrapper > main > app-file-layout > div > app-file-tab-information > div.eyes-container > div > app-file-information-eye:nth-child(1) > div > div.header > div.header__actions > amds-button > button", // OD
"#wrapper > main > app-file-layout > div > app-file-tab-information > div.eyes-container > div > app-file-information-eye:nth-child(2) > div > div.header > div.header__actions > amds-button > button", // OG
"#wrapper > main > app-file-layout > div > app-file-tab-first-lens > div.lens-container > form > div > amds-button > button", // Prescripteur
"#wrapper > main > app-file-layout > div > app-file-tab-first-lens > div.lens-container > div > app-file-first-lens:nth-child(1) > div > div.header > div.header__actions > amds-button > button", // Lentille OD
"#wrapper > main > app-file-layout > div > app-file-tab-first-lens > div.lens-container > div > app-file-first-lens:nth-child(2) > div > div.header > div.header__actions > amds-button > button" // Lentille OG
];
const alreadyObserved = new WeakSet();
// Raccourci Double Alt
function setupDoubleAltShortcut() {
console.log('⌨️ Configuration du raccourci Double Alt pour enregistrer et ouvrir les notes...');
let lastAltTime = 0;
const doubleAltDelay = 500;
document.addEventListener('keydown', (e) => {
// Vérifier Alt
if (e.key === 'Alt') {
const currentTime = Date.now();
const timeDiff = currentTime - lastAltTime;
// Double Alt
if (timeDiff < doubleAltDelay) {
console.log('⌨️ Double Alt détecté - Enregistrement + Ouverture des notes');
// Empêcher défaut
e.preventDefault();
e.stopPropagation();
// Enregistrer
console.log('💾 Enregistrement des données...');
// Boutons enregistrement
const saveButtons = document.querySelectorAll('amds-button button');
let savedCount = 0;
saveButtons.forEach((btn, index) => {
if (!btn.disabled && btn.textContent && btn.textContent.includes('Enregistr')) {
console.log(`💾 Clic sur bouton enregistrer ${index + 1}`);
// Montrer bouton
const wasHidden = btn.style.display === 'none';
if (wasHidden) {
btn.style.display = '';
}
btn.click();
savedCount++;
// Re-cacher si nécessaire
if (wasHidden) {
setTimeout(() => {
btn.style.display = 'none';
}, 100);
}
}
});
// Attendre
setTimeout(() => {
console.log('📝 Ouverture des notes après enregistrement...');
// Bouton Notes
const notesButtons = document.querySelectorAll('button');
let notesButton = null;
notesButtons.forEach(btn => {
if (btn.textContent && btn.textContent.includes('Notes') &&
btn.closest('.header__actions')) {
notesButton = btn;
}
});
if (notesButton) {
console.log('📝 Bouton Notes trouvé - Ouverture du modal');
notesButton.click();
} else {
console.log('⚠️ Bouton Notes non trouvé');
showToast('❌ Bouton Notes non trouvé');
}
}, 300);
} else {
// Premier Alt
lastAltTime = currentTime;
}
}
});
}
// Init raccourci
setupDoubleAltShortcut();
// Notification
setTimeout(() => {
showToast('⌨️ Nouveau raccourci : Double Alt pour enregistrer et ouvrir les notes !');
}, 2000);
// Agrandir zone notes
function setupNotesEditAreaEnlargement() {
console.log('📝 Configuration de l\'agrandissement de la zone d\'édition des notes...');
// Observer textarea
const editObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Chercher textarea
const textarea = node.querySelector ?
node.querySelector('textarea#input-editContent') : null;
// Vérifier nœud
const isEditTextarea = node.tagName === 'TEXTAREA' && node.id === 'input-editContent';
if (textarea || isEditTextarea) {
const targetTextarea = textarea || node;
console.log('📝 Zone d\'édition détectée - Application des styles d\'agrandissement');
// Appliquer styles
targetTextarea.style.minHeight = '200px';
targetTextarea.style.resize = 'vertical';
targetTextarea.style.fontSize = '14px';
targetTextarea.style.lineHeight = '1.4';
targetTextarea.style.padding = '12px';
targetTextarea.style.border = '2px solid #e0e0e0';
targetTextarea.style.borderRadius = '8px';
targetTextarea.style.transition = 'border-color 0.3s ease';
targetTextarea.style.overflow = 'hidden';
// Ajuster hauteur
function autoResizeTextarea(textarea) {
// Réinitialiser hauteur
textarea.style.height = 'auto';
// Calculer hauteur
const scrollHeight = textarea.scrollHeight;
const minHeight = 200;
const maxHeight = 500;
// Appliquer hauteur
const newHeight = Math.max(minHeight, Math.min(scrollHeight, maxHeight));
textarea.style.height = newHeight + 'px';
console.log(`📝 Hauteur ajustée automatiquement: ${newHeight}px`);
}
// Hauteur initiale
autoResizeTextarea(targetTextarea);
// Ajuster contenu
targetTextarea.addEventListener('input', () => {
autoResizeTextarea(targetTextarea);
});
// Ajuster focus
targetTextarea.addEventListener('focus', () => {
autoResizeTextarea(targetTextarea);
});
}
}
});
});
});
// Observer tout le document
editObserver.observe(document.body, {
childList: true,
subtree: true
});
}
// Init agrandissement
setupNotesEditAreaEnlargement();
// Centrage et déplacement modals
function setupModalCentering() {
console.log('🎯 Configuration du centrage et déplacement unifié des modals...');
// Centrer modal
function centerModal(modal) {
if (!modal || !modal.classList.contains('modal--size-medium')) return;
// Centrage CSS
modal.style.position = 'fixed';
modal.style.margin = '0';
modal.style.zIndex = '1050';
// Largeur
if (!modal.dataset.cfDefaultWidthSet) {
modal.style.width = '70vw';
modal.dataset.cfDefaultWidthSet = 'true';
}
// Centrer position
const rect = modal.getBoundingClientRect();
const winW = window.innerWidth;
const winH = window.innerHeight;
modal.style.left = Math.max(0, (winW - rect.width) / 2) + 'px';
modal.style.top = Math.max(0, (winH - rect.height) / 2) + 'px';
modal.style.transform = 'none';
console.log('🎯 Modal centré:', modal);
}
// Rendre déplaçable
function makeModalDraggable(modal) {
if (!modal || modal.dataset.cfDraggable === 'true') return;
const header = modal.querySelector('.modal__header');
if (!header) return;
modal.dataset.cfDraggable = 'true';
let isDragging = false;
let startX, startY, startLeft, startTop;
// Style header
header.style.cursor = 'move';
header.style.userSelect = 'none';
// Drag events
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.cf-modal-grip')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
// Récupérer la position actuelle
const rect = modal.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
// S'assurer qu'il n'y a pas de transform
modal.style.transform = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
const newLeft = Math.max(0, Math.min(window.innerWidth - modal.offsetWidth, startLeft + deltaX));
const newTop = Math.max(0, Math.min(window.innerHeight - modal.offsetHeight, startTop + deltaY));
modal.style.left = newLeft + 'px';
modal.style.top = newTop + 'px';
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
console.log('🎯 Modal rendu déplaçable:', modal);
}
// Observer pour détecter l'ouverture de nouveaux modals
const modalObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Chercher les modals dans le nouveau nœud
const modals = node.querySelectorAll ?
node.querySelectorAll('.modal.modal--size-medium') : [];
// Ou vérifier si le nœud lui-même est un modal
if (node.classList && node.classList.contains('modal') &&
node.classList.contains('modal--size-medium')) {
modals.push(node);
}
modals.forEach(modal => {
if (!modal.dataset.cfCentered) {
modal.dataset.cfCentered = 'true';
// Attendre que le modal soit complètement rendu
setTimeout(() => {
centerModal(modal);
makeModalDraggable(modal);
}, 100);
}
});
}
});
});
});
// Observer tout le document
modalObserver.observe(document.body, {
childList: true,
subtree: true
});
// Centrer et rendre déplaçables les modals existants
document.querySelectorAll('.modal.modal--size-medium').forEach(modal => {
if (!modal.dataset.cfCentered) {
modal.dataset.cfCentered = 'true';
centerModal(modal);
makeModalDraggable(modal);
}
});
// Re-centrer lors du redimensionnement de la fenêtre
window.addEventListener('resize', () => {
document.querySelectorAll('.modal.modal--size-medium').forEach(modal => {
centerModal(modal);
});
});
// Gestionnaire global unique pour fermer les modals avec Échap
let escapeHandlerAdded = false;
function addEscapeHandler() {
if (escapeHandlerAdded) return;
escapeHandlerAdded = true;
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
// Trouver tous les modals visibles
const visibleModals = Array.from(document.querySelectorAll('.modal.modal--size-medium')).filter(modal => {
const rect = modal.getBoundingClientRect();
return modal.offsetParent !== null &&
modal.style.display !== 'none' &&
rect.width > 0 &&
rect.height > 0;
});
if (visibleModals.length > 0) {
const modal = visibleModals[visibleModals.length - 1]; // Le plus récent
console.log('⌨️ Échap détecté, fermeture du modal:', modal);
// Essayer de fermer le modal
const closeButton = modal.querySelector('amds-button button[type="button"]');
const closeIcon = modal.querySelector('[name="ri-close-fill"], .ri-close-fill');
const overlay = modal.querySelector('.modal-overlay, .modal__overlay');
if (closeButton) {
console.log('✅ Fermeture via bouton');
closeButton.click();
} else if (closeIcon) {
console.log('✅ Fermeture via icône');
closeIcon.click();
} else if (overlay) {
console.log('✅ Fermeture via overlay');
overlay.click();
} else {
console.log('⚠️ Fermeture forcée');
modal.style.display = 'none';
}
e.preventDefault();
e.stopPropagation();
}
}
});
console.log('⌨️ Gestionnaire Échap global activé');
}
// Activer le gestionnaire
addEscapeHandler();
}
// Initialiser le centrage des modals
setupModalCentering();
// Observation des boutons
function observeAndAutoClick(selector, buttonLabel) {
const btn = document.querySelector(selector);
if (!btn || alreadyObserved.has(btn)) return;
btn.style.display = "none";
// Délai spécifique pour les boutons "Lentille OD" et "Lentille OG"
const needsDelay = buttonLabel.includes("Lentille");
const clickDelay = needsDelay ? 1500 : 0; // 1.5 secondes pour les lentilles
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
if (!btn.disabled && autoSaveEnabled) { // Vérifier si auto-save est activé
if (clickDelay > 0) {
console.log(`Bouton ${buttonLabel} actif → attente de ${clickDelay}ms pour le rendu WebGL...`);
// Vérifier si le canvas existe et attendre qu'il soit prêt
const checkCanvasAndClick = () => {
const canvas = document.querySelector('#webgl-canvas');
if (canvas) {
// Attendre un peu plus pour être sûr que le rendu est fait
setTimeout(() => {
console.log(`Bouton ${buttonLabel} → clic automatique après délai`);
btn.click();
}, clickDelay);
} else {
// Si pas de canvas, cliquer quand même après le délai
setTimeout(() => {
console.log(`Bouton ${buttonLabel} → clic automatique (sans canvas)`);
btn.click();
}, clickDelay);
}
};
checkCanvasAndClick();
} else {
console.log(`Bouton ${buttonLabel} actif → clic automatique immédiat`);
btn.click();
}
}
}
});
});
observer.observe(btn, { attributes: true });
alreadyObserved.add(btn);
console.log(`Observation activée pour ${buttonLabel}`);
}
// Fonction pour scanner et observer tous les boutons
function scanAndObserveButtons() {
const labels = ["OD", "OG", "Prescripteur", "Lentille OD", "Lentille OG"];
selectors.forEach((sel, i) => {
const btn = document.querySelector(sel);
if (btn && !alreadyObserved.has(btn)) {
observeAndAutoClick(sel, labels[i]);
}
});
}
// Observation pour détecter l'apparition de nouveaux boutons
function setupButtonObserver() {
console.log('🔍 Configuration de l\'observateur de boutons...');
// Observer spécifique pour les boutons, pas global
const buttonObserver = new MutationObserver((mutations) => {
let shouldScan = false;
mutations.forEach((mutation) => {
// Vérifier si des boutons ont pu être ajoutés
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
// Vérifier si c'est un bouton ou contient des boutons
if (node.tagName === 'BUTTON' ||
node.querySelector && node.querySelector('button')) {
shouldScan = true;
}
}
});
}
});
if (shouldScan) {
scanAndObserveButtons();
}
});
// Observer seulement les zones où les boutons peuvent apparaître
const targetZones = [
document.querySelector('#wrapper > main'),
document.querySelector('.eyes-container'),
document.querySelector('.lens-container')
].filter(el => el !== null);
targetZones.forEach(zone => {
buttonObserver.observe(zone, {
childList: true,
subtree: true
});
});
// Si aucune zone trouvée, observer le body mais avec plus de restrictions
if (targetZones.length === 0) {
buttonObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
function setupLensPageObserver() {
console.log('Configuration de l\'observateur de page lentilles...');
const lensPageObserver = new MutationObserver((mutations) => {
let shouldCheckForLensPage = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
if (node.textContent &&
(node.textContent.includes('Œil droit') ||
node.textContent.includes('Œil gauche') ||
node.textContent.includes('Oeil droit') ||
node.textContent.includes('Oeil gauche'))) {
shouldCheckForLensPage = true;
}
if (node.querySelector &&
(node.querySelector('div.amds-text.amds-font-headline-h6') ||
node.querySelector('[class*="lens"]'))) {
shouldCheckForLensPage = true;
}
}
});
}
});
if (shouldCheckForLensPage) {
console.log('👁️ Page lentilles potentielle détectée, vérification...');
setTimeout(() => {
if (window.displayRefractionOnLensPage) {
window.displayRefractionOnLensPage();
}
}, 500);
}
});
const targetZones = [
document.querySelector('#wrapper > main'),
document.querySelector('.lens-container'),
document.querySelector('[class*="lens"]')
].filter(el => el !== null);
targetZones.forEach(zone => {
lensPageObserver.observe(zone, {
childList: true,
subtree: true
});
});
if (targetZones.length === 0 && document.querySelector('main')) {
lensPageObserver.observe(document.querySelector('main'), {
childList: true,
subtree: true
});
}
}
// Force le step à 0.25 sur un input donné
function forceCustomStep(inputSelector, inputName) {
const input = document.querySelector(inputSelector);
if (!input || input.dataset.customStepApplied === 'true') return;
console.log(`Application du step personnalisé sur ${inputName}...`);
// Fonction helper pour vérifier si un input doit avoir le step 0.25
function shouldHaveCustomStep(inputElement) {
const stepInputs = [
'#input-rightsphere',
'#input-rightcylinder',
'#input-leftsphere',
'#input-leftcylinder',
'#input-rightaddition',
'#input-leftaddition'
];
return stepInputs.some(selector => {
return inputElement.matches(selector) || inputElement.id === selector.replace('#', '');
});
}
// Vérifier si cet input doit avoir le comportement personnalisé
if (!shouldHaveCustomStep(input)) {
console.log(`⏭️ ${inputName} n'est pas dans la liste - garde son comportement natif`);
return;
}
// Marquer comme traité
input.dataset.customStepApplied = 'true';
// Forcer les attributs
input.setAttribute('step', '0.25');
// Fonction pour arrondir au multiple de 0.25 le plus proche
function roundToStep(value, step = 0.25) {
return Math.round(value / step) * step;
}
// Fonction pour mettre à jour la valeur
function updateValue(newValue, keepFocus = false) {
const rounded = roundToStep(newValue, 0.25);
const formatted = rounded.toFixed(2);
// Sauvegarder l'état du focus et la position du curseur
const hasFocus = document.activeElement === input;
const selectionStart = input.selectionStart;
const selectionEnd = input.selectionEnd;
// Forcer la mise à jour de plusieurs façons
input.value = formatted;
input.setAttribute('value', formatted);
// Déclencher tous les événements possibles
['input', 'change'].forEach(eventType => {
input.dispatchEvent(new Event(eventType, { bubbles: true, cancelable: true }));
});
// Forcer si comp Angular utilsé
if (input._valueTracker) {
input._valueTracker.setValue(formatted);
}
// Restaurer le focus si nécessaire
if ((hasFocus || keepFocus) && document.activeElement !== input) {
input.focus();
// Restaurer la position du curseur
if (selectionStart !== null && selectionEnd !== null) {
input.setSelectionRange(selectionStart, selectionEnd);
}
}
}
// Intercepter et modifier le step natif
let isUpdating = false;
// Observer les changements de valeur
const valueObserver = new MutationObserver((mutations) => {
if (isUpdating) return;
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
const currentValue = parseFloat(input.value) || 0;
const roundedValue = roundToStep(currentValue, 0.25);
if (Math.abs(currentValue - roundedValue) > 0.001) {
isUpdating = true;
updateValue(roundedValue);
setTimeout(() => { isUpdating = false; }, 100);
}
}
});
});
valueObserver.observe(input, {
attributes: true,
attributeFilter: ['value']
});
// Intercepter les événements de changement
input.addEventListener('change', (e) => {
if (isUpdating) return;
const currentValue = parseFloat(e.target.value) || 0;
const roundedValue = roundToStep(currentValue, 0.25);
if (Math.abs(currentValue - roundedValue) > 0.001) {
e.preventDefault();
e.stopPropagation();
isUpdating = true;
updateValue(roundedValue);
setTimeout(() => { isUpdating = false; }, 100);
}
}, true);
// Gérer la molette
input.addEventListener('wheel', (e) => {
e.preventDefault();
e.stopPropagation();
const current = parseFloat(input.value) || 0;
const delta = e.deltaY < 0 ? 0.25 : -0.25;
updateValue(current + delta, true);
}, { passive: false, capture: true });
// Gérer les flèches du clavier
input.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
const current = parseFloat(input.value) || 0;
const delta = e.key === 'ArrowUp' ? 0.25 : -0.25;
updateValue(current + delta, true);
}
}, true);
// Intercepter le clic sur les boutons de step natifs (si présents)
const interceptStepButtons = () => {
const parent = input.parentElement;
if (!parent) return;
parent.addEventListener('click', (e) => {
// Si c'est un clic sur un bouton de step
if (e.target.matches('button, [role="button"]') && e.target !== input) {
e.preventDefault();
e.stopPropagation();
// Déterminer la direction
const rect = input.getBoundingClientRect();
const isUp = e.clientY < rect.top + rect.height / 2;
const current = parseFloat(input.value) || 0;
const delta = isUp ? 0.25 : -0.25;
updateValue(current + delta, true);
}
}, true);
setTimeout(() => {
// Chercher la div .arrows qui contient les boutons
const arrowsContainer = parent.querySelector('.arrows');
if (arrowsContainer) {
console.log(`🎯 Flèches custom trouvées pour ${inputName} (step 0.25)`);
const buttons = arrowsContainer.querySelectorAll('button');
buttons.forEach((button, index) => {
// Remplacer complètement le comportement du bouton
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
newButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const current = parseFloat(input.value) || 0;
// Index 0 = bouton du haut (augmenter), Index 1 = bouton du bas (diminuer)
const delta = index === 0 ? 0.25 : -0.25;
updateValue(current + delta, true);
console.log(`🔺 Clic sur flèche ${index === 0 ? 'haut' : 'bas'}: ${current} → ${current + delta}`);
});
});
}
}, 100);
const arrowObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.classList && node.classList.contains('arrows')) {
console.log(`🆕 Nouvelles flèches détectées pour ${inputName} (step 0.25)`);
const buttons = node.querySelectorAll('button');
buttons.forEach((button, index) => {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const current = parseFloat(input.value) || 0;
const delta = index === 0 ? 0.25 : -0.25;
updateValue(current + delta, true);
}, true);
});
}
});
});
});
arrowObserver.observe(parent, {
childList: true,
subtree: true
});
};
interceptStepButtons();
// Vérification périodiquement que le step est toujours à 0.25
setInterval(() => {
if (input.getAttribute('step') !== '0.25') {
input.setAttribute('step', '0.25');
}
}, 1000);
console.log(`✅ Step personnalisé 0.25 forcé pour ${inputName}`);
}
// Liste des inputs qui doivent avoir le step 0.25
const stepInputs = [
'#input-rightsphere',
'#input-rightcylinder',
'#input-leftsphere',
'#input-leftcylinder',
'#input-rightaddition',
'#input-leftaddition'
];
// Appliquer le step custom à tous les inputs
function applyCustomStepToAll() {
const inputNames = {
'#input-rightsphere': 'Sphère OD',
'#input-rightcylinder': 'Cylindre OD',
'#input-leftsphere': 'Sphère OG',
'#input-leftcylinder': 'Cylindre OG'
};
stepInputs.forEach(selector => {
forceCustomStep(selector, inputNames[selector] || selector);
});
}
function patchColorAutoChange() {
const materialSelectors = ['#input-leftmaterial', '#input-rightmaterial'];
materialSelectors.forEach(selector => {
const materialSelect = document.querySelector(selector);
if (!materialSelect) return;
// Déterminer si c'est pour l'œil gauche ou droit
const isLeft = selector.includes('left');
const colorSelector = isLeft ? '#input-leftcolor' : '#input-rightcolor';
// Sauvegarder la couleur actuelle avant le changement
materialSelect.addEventListener('mousedown', () => {
const colorSelect = document.querySelector(colorSelector);
if (colorSelect && colorSelect.value !== 'null') {
colorSelect.dataset.savedColor = colorSelect.value;
console.log(`Couleur sauvegardée: ${colorSelect.value}`);
}
});
// Restaurer la couleur après le changement de matériau
materialSelect.addEventListener('change', () => {
setTimeout(() => {
const colorSelect = document.querySelector(colorSelector);
if (colorSelect && colorSelect.dataset.savedColor) {
const savedColor = colorSelect.dataset.savedColor;
if (colorSelect.value !== savedColor) {
console.log(`Restauration de la couleur: ${savedColor}`);
colorSelect.value = savedColor;
colorSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
delete colorSelect.dataset.savedColor;
}
}, 100); // Petit délai pour laisser l'application faire son changement automatique
});
});
// Observer l'apparition de nouveaux sélecteurs (pour les pages chargées dynamiquement)
const observer = new MutationObserver(() => {
materialSelectors.forEach(selector => {
const select = document.querySelector(selector);
if (select && !select.dataset.colorPatchApplied) {
select.dataset.colorPatchApplied = 'true';
patchColorAutoChange();
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Remplissage France automatique pour le prescripteur
function autoFillPrescripteurCountry() {
console.log('Activation de l\'auto-remplissage du pays prescripteur...');
// Observer l'apparition du modal
const modalObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// Vérifier si des nœuds ont été ajoutés
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
// Vérifier si c'est un élément DOM
if (node.nodeType === 1) {
// Chercher le titre du modal dans le noeud ou ses enfants
let hasTitle = false;
// Vérifier le texte du noeud lui-même
if (node.textContent && node.textContent.includes('Création d\'un prescripteur')) {
hasTitle = true;
}
// Vérifier les enfants
if (!hasTitle && node.querySelectorAll) {
const titles = node.querySelectorAll('.modal_header, .modal-title, h1, h2, h3, h4, h5, h6');
titles.forEach(title => {
if (title.textContent.includes('Création d\'un prescripteur')) {
hasTitle = true;
}
});
}
if (hasTitle && !node.dataset.countryFilled) {
console.log('Modal prescripteur détecté');
node.dataset.countryFilled = 'true';
// Attendre que le modal soit complètement rendu
setTimeout(() => {
autoFillCountry();
}, 500);
}
}
});
}
});
});
// Fonction pour remplir automatiquement le pays
async function autoFillCountry() {
console.log('Recherche du champ pays...');
// Chercher le champ pays par son label
let countryInput = null;
countryInput = document.querySelector('#input-country');
if (!countryInput) {
const labels = document.querySelectorAll('label');
labels.forEach(label => {
if (label.textContent.includes('Pays')) {
const inputId = label.getAttribute('for');
if (inputId) {
countryInput = document.querySelector(`#${inputId}`);
}
}
});
}
// Chercher un input avec placeholder contenant "Fra"
if (!countryInput) {
const inputs = document.querySelectorAll('input[placeholder*="Fra"]');
if (inputs.length > 0) {
countryInput = inputs[0];
}
}
if (!countryInput) {
console.log(' Champ pays non trouvé');
return;
}
console.log('Champ pays trouvé:', countryInput);
// Vérifier si le champ est vide ou contient "Fra"
if (countryInput.value === '' || countryInput.value === 'Fra') {
console.log('Début du remplissage...');
// Focus sur le champ
countryInput.focus();
await delay(100);
// Effacer et taper "Fra"
countryInput.value = '';
await delay(50);
countryInput.value = 'Fra';
countryInput.dispatchEvent(new Event('input', { bubbles: true }));
countryInput.dispatchEvent(new Event('change', { bubbles: true }));
console.log('Attente de l\'apparition de la suggestion...');
await delay(300); // Attendre que la suggestion apparaisse
let clicked = false;
// Chercher spécifiquement les divs avec la classe de suggestion
const suggestions = document.querySelectorAll('div.amds-text.amds-font-body-body1.amds-color-basic-900');
for (const suggestion of suggestions) {
if (suggestion.textContent.trim() === 'France') {
console.log(' Suggestion "France" trouvée !');
// Essayer de cliquer sur la suggestion
suggestion.click();
// Si ça ne marche pas, essayer de cliquer sur le parent
if (suggestion.parentElement) {
suggestion.parentElement.click();
}
clicked = true;
console.log('Clic sur "France" effectué');
break;
}
}
// Si pas trouvé avec la classe exacte, chercher plus largement
if (!clicked) {
console.log(' Recherche élargie...');
const allDivs = document.querySelectorAll('div');
for (const div of allDivs) {
// Vérifier que c'est visible et contient exactement "France"
if (div.textContent.trim() === 'France' &&
div.offsetParent !== null &&
!div.querySelector('*')) { // S'assurer que c'est une feuille de l'arbre DOM
console.log('Suggestion alternative trouvée');
div.click();
// Essayer aussi le parent au cas où
if (div.parentElement && div.parentElement.tagName !== 'BODY') {
div.parentElement.click();
}
clicked = true;
break;
}
}
}
if (clicked) {
console.log('Pays "France" sélectionné avec succès !');
// showToast(' Pays défini sur France');
} else {
console.log('Impossible de cliquer sur la suggestion');
// Fallback: essayer de mettre France directement
countryInput.value = 'France';
countryInput.dispatchEvent(new Event('change', { bubbles: true }));
countryInput.blur();
}
}
}
// Helper pour les délais
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Observer aussi spécifiquement l'apparition du texte "France" après avoir tapé
const franceObserver = new MutationObserver((mutations) => {
// Ne réagir que si on a tapé "Fra" dans le champ
const countryInput = document.querySelector('#input-country');
if (!countryInput || countryInput.value !== 'Fra') return;
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
// Chercher France dans le nouveau noeud
if (node.textContent && node.textContent.trim() === 'France') {
console.log('🎯 Auto-détection de la suggestion France');
setTimeout(() => {
node.click();
}, 50);
}
// Chercher aussi dans les enfants
if (node.querySelectorAll) {
const franceDivs = node.querySelectorAll('div');
franceDivs.forEach(div => {
if (div.textContent.trim() === 'France' && !div.querySelector('*')) {
console.log('Auto-détection de France dans les enfants');
setTimeout(() => {
div.click();
}, 50);
}
});
}
}
});
}
});
});
// Démarrer les observateurs
modalObserver.observe(document.body, {
childList: true,
subtree: true
});
franceObserver.observe(document.body, {
childList: true,
subtree: true
});
console.log('Auto-remplissage du pays prescripteur activé');
}
// Fonction pour réorganiser les champs de kératométrie
function reorderKeratometryFields() {
// Sélectionner les deux sections de kératométrie (OD et OG)
const eyeContainers = document.querySelectorAll('app-file-information-eye');
eyeContainers.forEach((eyeContainer, eyeIndex) => {
const eyeName = eyeIndex === 0 ? 'OD' : 'OG';
// Trouver la section kératométrie
const keratometrySection = eyeContainer.querySelector('app-file-information-eye-keratometry');
if (!keratometrySection) {
console.log(`⚠️ Section kératométrie non trouvée pour ${eyeName}`);
return;
}
// Trouver le conteneur des champs (.fields ou app-file-form-group selon la structure)
const fieldsContainer = keratometrySection.querySelector('.fields app-file-form-group') ||
keratometrySection.querySelector('app-file-form-group:last-of-type') ||
keratometrySection.querySelector('.form app-file-form-group:last-of-type');
if (!fieldsContainer) {
console.log(`⚠️ Conteneur des champs non trouvé pour ${eyeName}`);
return;
}
// Identifier les champs par leur data-field ou leur ID
const fieldMapping = {
kFlat: fieldsContainer.querySelector('[data-field="kParameter"], [id*="kParameter"]:not([id*="steep"])')?.closest('app-store-field'),
excFlat: fieldsContainer.querySelector('[data-field="eccentricity"], [id*="eccentricity"]:not([id*="steep"])')?.closest('app-store-field'),
axisFlat: fieldsContainer.querySelector('[data-field="keratometryAxis"], [id*="keratometryAxis"]')?.closest('app-store-field'),
kSteep: fieldsContainer.querySelector('[data-field="steepKParameter"], [id*="steepKParameter"]')?.closest('app-store-field'),
excSteep: fieldsContainer.querySelector('[data-field="steepEccentricity"], [id*="steepEccentricity"]')?.closest('app-store-field'),
div30: fieldsContainer.querySelector('[data-field="visibleIrisDiameterAt30"], [id*="visibleIrisDiameterAt30"]')?.closest('app-store-field')
};
// Vérifier que tous les champs sont trouvés
const foundFields = Object.entries(fieldMapping).filter(([key, el]) => el !== null);
console.log(`📍 ${eyeName}: ${foundFields.length}/6 champs trouvés`);
if (foundFields.length === 0) {
console.log(`❌ Aucun champ trouvé pour ${eyeName}`);
return;
}
// Ordre : Ligne 1: K plat, K serré, Axe | Ligne 2: Exc plat, Exc serré, DIV 30
const desiredOrder = ['kFlat', 'kSteep', 'axisFlat', 'excFlat', 'excSteep', 'div30'];
// Créer un fragment pour réorganiser
const fragment = document.createDocumentFragment();
// Ajouter les champs dans l'ordre souhaité
desiredOrder.forEach(fieldName => {
const field = fieldMapping[fieldName];
if (field) {
fragment.appendChild(field);
}
});
// Vider et remplir le conteneur
while (fieldsContainer.firstChild) {
fieldsContainer.removeChild(fieldsContainer.firstChild);
}
fieldsContainer.appendChild(fragment);
console.log(`✅ Champs réorganisés pour ${eyeName}`);
});
// Ajuster le style pour une meilleure présentation
injectKeratometryStyles();
}
// Styles pour améliorer la présentation
function injectKeratometryStyles() {
if (document.getElementById('keratometry-reorder-styles')) return;
const styles = `
<style id="keratometry-reorder-styles">
/* Améliorer l'espacement des champs de kératométrie */
app-file-information-eye-keratometry .fields app-file-form-group,
app-file-information-eye-keratometry .form > app-file-form-group:last-of-type {
display: grid !important;
grid-template-columns: repeat(3, 1fr) !important;
gap: 0.8rem !important;
}
/* LIGNE 1 : K plat, K serré, Axe */
app-file-information-eye-keratometry [data-field="kParameter"] {
grid-column: 1;
grid-row: 1;
}
app-file-information-eye-keratometry [data-field="steepKParameter"] {
grid-column: 2;
grid-row: 1;
}
app-file-information-eye-keratometry [data-field="keratometryAxis"] {
grid-column: 3;
grid-row: 1;
}
/* LIGNE 2 : Exc plat, Exc serré, DIV 30 */
app-file-information-eye-keratometry [data-field="eccentricity"] {
grid-column: 1;
grid-row: 2;
}
app-file-information-eye-keratometry [data-field="steepEccentricity"] {
grid-column: 2;
grid-row: 2;
}
app-file-information-eye-keratometry [data-field="visibleIrisDiameterAt30"] {
grid-column: 3;
grid-row: 2;
}
/* Couleurs douces pour différencier plat et serré */
/* Bleu doux pour les champs "plat" */
app-file-information-eye-keratometry [data-field="kParameter"] .amds-input,
app-file-information-eye-keratometry [data-field="eccentricity"] .amds-input {
background-color: rgba(59, 130, 246, 0.05) !important;
border: 1px solid rgba(59, 130, 246, 0.2) !important;
}
app-file-information-eye-keratometry [data-field="kParameter"] .amds-input:hover,
app-file-information-eye-keratometry [data-field="eccentricity"] .amds-input:hover {
background-color: rgba(59, 130, 246, 0.08) !important;
border-color: rgba(59, 130, 246, 0.3) !important;
}
app-file-information-eye-keratometry [data-field="kParameter"] .amds-input:focus,
app-file-information-eye-keratometry [data-field="eccentricity"] .amds-input:focus {
background-color: rgba(59, 130, 246, 0.1) !important;
border-color: rgba(59, 130, 246, 0.4) !important;
}
/* Rouge doux pour les champs "serré" */
app-file-information-eye-keratometry [data-field="steepKParameter"] .amds-input,
app-file-information-eye-keratometry [data-field="steepEccentricity"] .amds-input {
background-color: rgba(239, 68, 68, 0.05) !important;
border: 1px solid rgba(239, 68, 68, 0.2) !important;
}
app-file-information-eye-keratometry [data-field="steepKParameter"] .amds-input:hover,
app-file-information-eye-keratometry [data-field="steepEccentricity"] .amds-input:hover {
background-color: rgba(239, 68, 68, 0.08) !important;
border-color: rgba(239, 68, 68, 0.3) !important;
}
app-file-information-eye-keratometry [data-field="steepKParameter"] .amds-input:focus,
app-file-information-eye-keratometry [data-field="steepEccentricity"] .amds-input:focus {
background-color: rgba(239, 68, 68, 0.1) !important;
border-color: rgba(239, 68, 68, 0.4) !important;
}
/* Style neutre pour l'axe */
app-file-information-eye-keratometry [data-field="keratometryAxis"] .amds-input {
background-color: rgba(107, 114, 128, 0.05) !important;
border: 1px solid rgba(107, 114, 128, 0.2) !important;
}
app-file-information-eye-keratometry [data-field="keratometryAxis"] .amds-input:hover {
background-color: rgba(107, 114, 128, 0.08) !important;
border-color: rgba(107, 114, 128, 0.3) !important;
}
app-file-information-eye-keratometry [data-field="keratometryAxis"] .amds-input:focus {
background-color: rgba(107, 114, 128, 0.1) !important;
border-color: rgba(107, 114, 128, 0.4) !important;
}
/* Style vert doux pour DIV 30 */
app-file-information-eye-keratometry [data-field="visibleIrisDiameterAt30"] .amds-input {
background-color: rgba(34, 197, 94, 0.05) !important;
border: 1px solid rgba(34, 197, 94, 0.2) !important;
}
app-file-information-eye-keratometry [data-field="visibleIrisDiameterAt30"] .amds-input:hover {
background-color: rgba(34, 197, 94, 0.08) !important;
border-color: rgba(34, 197, 94, 0.3) !important;
}
app-file-information-eye-keratometry [data-field="visibleIrisDiameterAt30"] .amds-input:focus {
background-color: rgba(34, 197, 94, 0.1) !important;
border-color: rgba(34, 197, 94, 0.4) !important;
}
/* Améliorer la lisibilité des labels */
app-file-information-eye-keratometry .amds-form-element label {
font-weight: 500;
}
/* Transition douce sur tous les inputs */
app-file-information-eye-keratometry .amds-input {
transition: all 0.2s ease;
}
/* S'assurer que les inputs ont la même hauteur */
app-file-information-eye-keratometry .amds-input input {
height: 38px;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', styles);
}
// Réorganisation des champs de kératométrie avec observation des changements
function setupKeratometryReorder() {
console.log('🎨 Configuration du réorganisateur de kératométrie...');
// Application initiale
setTimeout(() => {
if (window.location.pathname.includes('/file/')) {
reorderKeratometryFields();
}
}, 2500); // Un peu après la réorganisation des sections
// Observer les changements
const observer = new MutationObserver((mutations) => {
// Vérifier si des champs de kératométrie sont ajoutés
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1 && // Element node
(node.matches && node.matches('app-file-information-eye-keratometry')) ||
(node.querySelector && node.querySelector('app-file-information-eye-keratometry'))) {
setTimeout(reorderKeratometryFields, 500);
return;
}
}
}
}
});
// Observer le body pour les changements
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Fonction de raccourcis clavier
function setupKeyboardShortcuts() {
console.log('Activation des raccourcis F1-F4...');
document.addEventListener('keydown', async (event) => {
// F1 - Coller réfraction depuis le presse-papier
if (event.key === 'F1') {
console.log('Raccourci F1 détecté - Collage réfraction');
event.preventDefault();
event.stopPropagation();
await pasteRefractionFromClipboard();
}
// F2 - Dupliquer OD→OG
else if (event.key === 'F2') {
console.log('Raccourci F2 détecté - Duplication OD→OG');
event.preventDefault();
event.stopPropagation();
// Vérifier qu'on est sur une page avec les champs
const odSphere = document.querySelector('#input-rightsphere');
const ogSphere = document.querySelector('#input-leftsphere');
if (odSphere && ogSphere) {
duplicateODtoOG();
} else {
console.log('Champs de réfraction non trouvés sur cette page');
showToast('⚠️ Champs de réfraction non disponibles');
}
}
// F3 - Calcul OrthoK
else if (event.key === 'F3') {
console.log('Raccourci F3 détecté - Calcul OrthoK');
event.preventDefault();
event.stopPropagation();
await performOrthoKCalculation();
}
// F4 - Calcul LRPG
else if (event.key === 'F4') {
console.log('Raccourci F4 détecté - Calcul LRPG');
event.preventDefault();
event.stopPropagation();
await performLRPGCalculation();
}
}, true); // Capture phase pour intercepter avant tout autre handler
console.log('✅ Raccourcis activés :');
console.log(' F1 : Coller réfraction depuis presse-papier');
console.log(' F2 : Dupliquer OD→OG');
console.log(' F3 : Calcul OrthoK');
console.log(' F4 : Calcul LRPG');
}
// Fonction pour capturer et mémoriser les données de réfraction/kératométrie
function captureRefractionData() {
const data = {
od: {
sphere: null,
cylinder: null,
axis: null,
kerato: {
kFlat: null,
kSteep: null,
axisFlat: null
}
},
og: {
sphere: null,
cylinder: null,
axis: null,
kerato: {
kFlat: null,
kSteep: null,
axisFlat: null
}
}
};
// Capturer les valeurs de réfraction OD
const odSphere = document.querySelector('#input-rightsphere');
const odCylinder = document.querySelector('#input-rightcylinder');
const odAxis = document.querySelector('#input-rightrefractionAxis');
if (odSphere && odSphere.value) data.od.sphere = odSphere.value;
if (odCylinder && odCylinder.value) data.od.cylinder = odCylinder.value;
if (odAxis && odAxis.value) data.od.axis = odAxis.value;
// Capturer les valeurs de kératométrie OD
const odKFlat = document.querySelector('#input-rightkParameter');
const odKSteep = document.querySelector('#input-rightsteepKParameter');
const odAxisFlat = document.querySelector('#input-rightkeratometryAxis');
if (odKFlat && odKFlat.value) data.od.kerato.kFlat = odKFlat.value;
if (odKSteep && odKSteep.value) data.od.kerato.kSteep = odKSteep.value;
if (odAxisFlat && odAxisFlat.value) data.od.kerato.axisFlat = odAxisFlat.value;
// Capturer les valeurs de réfraction OG
const ogSphere = document.querySelector('#input-leftsphere');
const ogCylinder = document.querySelector('#input-leftcylinder');
const ogAxis = document.querySelector('#input-leftrefractionAxis');
if (ogSphere && ogSphere.value) data.og.sphere = ogSphere.value;
if (ogCylinder && ogCylinder.value) data.og.cylinder = ogCylinder.value;
if (ogAxis && ogAxis.value) data.og.axis = ogAxis.value;
// Capturer les valeurs de kératométrie OG
const ogKFlat = document.querySelector('#input-leftkParameter');
const ogKSteep = document.querySelector('#input-leftsteepKParameter');
const ogAxisFlat = document.querySelector('#input-leftkeratometryAxis');
if (ogKFlat && ogKFlat.value) data.og.kerato.kFlat = ogKFlat.value;
if (ogKSteep && ogKSteep.value) data.og.kerato.kSteep = ogKSteep.value;
if (ogAxisFlat && ogAxisFlat.value) data.og.kerato.axisFlat = ogAxisFlat.value;
// Stocker dans sessionStorage
sessionStorage.setItem('clickfit_refraction_data', JSON.stringify(data));
console.log(' Données capturées:', data);
return data;
}
// Fonction pour formater les données complètes (réfraction + kérato)
function formatCompleteData(eye) {
let result = '';
// Partie réfraction : sphère (cylindre) axe°
if (eye.sphere || eye.cylinder) {
// Sphère
if (eye.sphere) {
const sphere = parseFloat(eye.sphere);
result += sphere > 0 ? '+' : '';
result += sphere.toFixed(2);
} else {
result += 'Plan';
}
// Cylindre et axe
if (eye.cylinder && parseFloat(eye.cylinder) !== 0) {
const cylinder = parseFloat(eye.cylinder);
result += ' (';
result += cylinder > 0 ? '+' : '';
result += cylinder.toFixed(2);
result += ')';
if (eye.axis) {
result += ' ' + parseInt(eye.axis) + '°';
}
}
} else {
result += 'Non renseigné';
}
// Partie kératométrie : Kérato plate x Kérato serrée @ Axe plat°
if (eye.kerato.kFlat && eye.kerato.kSteep) {
result += ' / ';
result += eye.kerato.kFlat + ' x ' + eye.kerato.kSteep;
if (eye.kerato.axisFlat) {
result += ' @ ' + eye.kerato.axisFlat + '°';
}
}
return result;
}
// Fonction pour afficher les données sur la page des lentilles avec encadré et légende
function displayRefractionOnLensPage() {
console.log('� Tentative d\'affichage des données de réfraction...');
// Récupérer les données depuis sessionStorage
const storedData = sessionStorage.getItem('clickfit_refraction_data');
if (!storedData) {
console.log(' Aucune donnée de réfraction en mémoire');
return;
}
const data = JSON.parse(storedData);
console.log('� Données récupérées:', data);
// Chercher les divs contenant "Œil droit" et "Œil gauche"
const findEyeContainers = () => {
console.log('🔍 Recherche des conteneurs œil droit/gauche...');
// Chercher par classe spécifique
const allDivs = document.querySelectorAll('div.amds-text.amds-font-headline-h6.amds-color-basic-900');
console.log(`📊 ${allDivs.length} divs trouvés avec la classe`);
let odContainer = null;
let ogContainer = null;
for (let div of allDivs) {
// Nettoyage du texte
const text = div.textContent.replace(/\s+/g, ' ').trim();
console.log(`Texte trouvé: "${text}"`);
if (text === 'Œil droit' || text === 'Oeil droit') {
odContainer = div;
console.log(' Œil droit trouvé:', div);
} else if (text === 'Œil gauche' || text === 'Oeil gauche') {
ogContainer = div;
console.log(' Œil gauche trouvé:', div);
}
}
console.log('📊 Résultat recherche:', { odContainer, ogContainer });
return { odContainer, ogContainer };
};
const { odContainer, ogContainer } = findEyeContainers();
// Fonction pour créer un encadré formaté avec légende (version horizontale)
const createInfoBox = (data, eye) => {
const eyeData = eye === 'od' ? data.od : data.og;
const isOD = eye === 'od';
// Couleurs différenciées OD (violet) / OG (bleu)
const colors = isOD ? {
background: '#faf8ff', // Violet très clair
border: '#d4c5e8', // Bordure violet doux
shadow: 'rgba(146, 75, 146, 0.08)', // Ombre violette douce
textPrimary: '#4a1e7c', // Texte violet foncé
textSecondary: '#6c4a8d', // Violet moyen pour les labels
separator: '#e4d5f0' // Séparateur violet clair
} : {
background: '#f8fbff', // Bleu clair (actuel pour OG)
border: '#d0d7e5', // Bordure bleue
shadow: 'rgba(30, 75, 146, 0.08)', // Ombre bleue
textPrimary: '#1a3d7c', // Texte bleu foncé
textSecondary: '#1e4b92', // Bleu moyen pour les labels
separator: '#d0d7e5' // Séparateur bleu clair
};
// Formater la réfraction
let refractionText = '';
if (eyeData.sphere || eyeData.cylinder) {
// Sphère
if (eyeData.sphere) {
const sphere = parseFloat(eyeData.sphere);
refractionText += sphere > 0 ? '+' : '';
refractionText += sphere.toFixed(2).replace('.', ',');
} else {
refractionText += 'Plan';
}
// Cylindre et axe
if (eyeData.cylinder && parseFloat(eyeData.cylinder) !== 0) {
const cylinder = parseFloat(eyeData.cylinder);
refractionText += ' (';
refractionText += cylinder > 0 ? '+' : '';
refractionText += cylinder.toFixed(2).replace('.', ',');
refractionText += ')';
if (eyeData.axis) {
refractionText += ' ' + parseInt(eyeData.axis) + '°';
}
}
} else {
refractionText = 'Non renseignée';
}
// Formater la kératométrie
let keratoText = '';
if (eyeData.kerato.kFlat && eyeData.kerato.kSteep) {
keratoText = eyeData.kerato.kFlat.replace('.', ',') + ' × ' + eyeData.kerato.kSteep.replace('.', ',');
if (eyeData.kerato.axisFlat) {
keratoText += ' @ ' + eyeData.kerato.axisFlat + '°';
}
} else {
keratoText = 'Non renseignée';
}
// Créer l'élément encadré
const infoBox = document.createElement('div');
infoBox.className = `refraction-info refraction-info-${eye}`;
infoBox.style.cssText = `
border: 1px solid ${colors.border};
border-radius: 6px;
padding: 10px 14px;
margin: 8px 15px 8px 0;
background-color: ${colors.background};
box-shadow: 0 1px 3px ${colors.shadow};
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
max-width: 700px;
`;
infoBox.innerHTML = `
<div style="
font-weight: 600;
color: #6c757d;
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.5px;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
">
<span>📋</span>
<span>Données initiales</span>
</div>
<div style="
color: ${colors.textSecondary};
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
">
<div style="display: flex; align-items: center; gap: 6px;">
<span style="font-weight: 500;">Réfraction :</span>
<span style="font-weight: 600; color: ${colors.textPrimary};">${refractionText}</span>
</div>
<span style="color: ${colors.separator}; font-weight: 300;">|</span>
<div style="display: flex; align-items: center; gap: 6px;">
<span style="font-weight: 500;">Kératométrie :</span>
<span style="font-weight: 600; color: ${colors.textPrimary};">${keratoText}</span>
</div>
</div>
`;
return infoBox;
};
// Ajouter les informations pour OD
if (odContainer) {
// Chercher si l'info existe déjà dans le parent ou à côté
const parent = odContainer.parentElement;
let existingInfo = null;
// Chercher plus largement dans le parent du parent
const grandParent = parent ? parent.parentElement : null;
if (grandParent) {
existingInfo = grandParent.querySelector('.refraction-info-od');
}
if (!existingInfo) {
const odInfoBox = createInfoBox(data, 'od');
// Insérer après le parent du titre pour que ce soit bien en dessous
if (parent && parent.parentElement) {
parent.parentElement.insertBefore(odInfoBox, parent.nextSibling);
} else if (odContainer.nextSibling) {
odContainer.parentNode.insertBefore(odInfoBox, odContainer.nextSibling);
} else {
odContainer.appendChild(odInfoBox);
}
console.log(' Info OD ajoutée avec encadré');
} else {
// Remplacer complètement l'info existante
const newInfoBox = createInfoBox(data, 'od');
existingInfo.replaceWith(newInfoBox);
console.log('🔄 Info OD mise à jour avec encadré');
}
} else {
console.log('Conteneur OD non trouvé');
}
// Ajouter les informations pour OG
if (ogContainer) {
// Chercher si l'info existe déjà dans le parent ou à côté
const parent = ogContainer.parentElement;
let existingInfo = null;
// Chercher plus largement dans le parent du parent
const grandParent = parent ? parent.parentElement : null;
if (grandParent) {
existingInfo = grandParent.querySelector('.refraction-info-og');
}
if (!existingInfo) {
const ogInfoBox = createInfoBox(data, 'og');
// Insérer après le parent du titre pour que ce soit bien en dessous
if (parent && parent.parentElement) {
parent.parentElement.insertBefore(ogInfoBox, parent.nextSibling);
} else if (ogContainer.nextSibling) {
ogContainer.parentNode.insertBefore(ogInfoBox, ogContainer.nextSibling);
} else {
ogContainer.appendChild(ogInfoBox);
}
console.log('✅ Info OG ajoutée avec encadré');
} else {
// Remplacer complètement l'info existante
const newInfoBox = createInfoBox(data, 'og');
existingInfo.replaceWith(newInfoBox);
console.log('🔄 Info OG mise à jour avec encadré');
}
} else {
console.log(' Conteneur OG non trouvé');
}
}
function setupRefractionPolling() {
console.log('🔄 Activation du polling mémoire réfraction (robuste)');
window.captureRefractionData = captureRefractionData;
window.displayRefractionOnLensPage = displayRefractionOnLensPage;
// Reset à chaque changement de fiche patient (via l’URL)
let lastPatientId = null;
setInterval(() => {
const match = location.pathname.match(/\/file\/([A-Za-z0-9]+)/);
const patientId = match ? match[1] : null;
if (patientId !== lastPatientId) {
sessionStorage.removeItem('clickfit_refraction_data');
lastPatientId = patientId;
console.log('Nouvelle fiche patient détectée, reset mémoire');
}
}, 500);
// Polling toutes les 500ms sur la page info patient pour capture
setInterval(() => {
// Teste la présence de TOUS les champs nécessaires (OD + OG)
const odSphere = document.querySelector('#input-rightsphere');
const odCyl = document.querySelector('#input-rightcylinder');
const odAxis = document.querySelector('#input-rightrefractionAxis');
const ogSphere = document.querySelector('#input-leftsphere');
const ogCyl = document.querySelector('#input-leftcylinder');
const ogAxis = document.querySelector('#input-leftrefractionAxis');
if (odSphere && odCyl && odAxis && ogSphere && ogCyl && ogAxis) {
const hasAnyValue = [odSphere.value, odCyl.value, odAxis.value, ogSphere.value, ogCyl.value, ogAxis.value]
.some(val => val && val.trim() !== '');
if (hasAnyValue) {
// Capture et sauvegarde si changement
const data = captureRefractionData();
const oldData = sessionStorage.getItem('clickfit_refraction_data');
if (JSON.stringify(data) !== oldData) {
sessionStorage.setItem('clickfit_refraction_data', JSON.stringify(data));
console.log('Données de réfraction mises à jour');
}
}
}
}, 500);
// Affichage automatique sur la page lentilles
setInterval(() => {
if (typeof window.displayRefractionOnLensPage === 'function') {
window.displayRefractionOnLensPage();
}
}, 1500);
}
// Module protection tabulation
// ========================================
// FIX ANTI-DÉFOCUS ET NAVIGATION TAB POUR PREMIÈRES LENTILLES
// ========================================
function fixLensPageAutoSaveAndTab() {
console.log('🛡️ Activation du fix anti-défocus pour Premières lentilles...');
// Variables globales pour le module
let currentFocusedElement = null;
let shouldMaintainFocus = false;
let focusProtectionActive = false;
let lastInputTime = 0;
const DEBOUNCE_DELAY = 300; // Délai avant auto-save
// Fonction pour détecter si on est sur la page lentilles
function isLensPage() {
const indicators = [
document.querySelector('.tab.lens-0-tab.active'),
document.querySelector('app-file-first-lens'),
document.querySelector('#input-righttype'),
document.querySelector('.lenses')
];
return indicators.some(el => el !== null);
}
if (!isLensPage()) {
console.log('❌ Pas sur la page Premières lentilles');
return;
}
// Éviter la double activation
if (window.lensAutoSaveFixActive) {
console.log('🔄 Fix anti-défocus déjà actif');
return;
}
console.log('✅ Fix anti-défocus activé pour la page lentilles');
window.lensAutoSaveFixActive = true;
// ========================================
// PARTIE 1: NEUTRALISATION DE L'AUTO-SAVE
// ========================================
// Intercepter et bloquer les blur causés par l'auto-save
function protectFocus() {
if (focusProtectionActive) return;
focusProtectionActive = true;
// Sauvegarder les méthodes originales
const originalBlur = HTMLElement.prototype.blur;
const originalFocus = HTMLElement.prototype.focus;
// Override de blur pour empêcher le défocus non désiré
HTMLElement.prototype.blur = function() {
// Si on est dans un input de la page lentilles et qu'on veut maintenir le focus
if (shouldMaintainFocus && this === currentFocusedElement) {
console.log('🛡️ Blur bloqué sur:', this.id || this.className);
// Ne pas exécuter le blur
return;
}
// Sinon, exécuter normalement
return originalBlur.apply(this, arguments);
};
// Intercepter les événements blur au niveau document
document.addEventListener('blur', function(e) {
if (!shouldMaintainFocus) return;
const target = e.target;
// Vérifier si c'est un input de la page lentilles
if (target && target === currentFocusedElement &&
(target.tagName === 'INPUT' || target.tagName === 'SELECT')) {
console.log('🔄 Tentative de défocus détectée, restauration...');
// Empêcher la propagation
e.stopImmediatePropagation();
e.preventDefault();
// Restaurer le focus immédiatement
setTimeout(() => {
if (currentFocusedElement && currentFocusedElement !== document.activeElement) {
currentFocusedElement.focus();
// Pour les inputs numériques, resélectionner le contenu
if (currentFocusedElement.type === 'number' || currentFocusedElement.type === 'text') {
currentFocusedElement.select();
}
}
}, 0);
}
}, true); // Capture phase
console.log('✅ Protection focus activée');
}
// Tracker le focus actuel
function trackFocus() {
document.addEventListener('focusin', function(e) {
const target = e.target;
// Si c'est un input/select de la page lentilles
if (isLensPage() &&
(target.tagName === 'INPUT' || target.tagName === 'SELECT')) {
currentFocusedElement = target;
shouldMaintainFocus = true;
console.log('🎯 Focus tracké sur:', target.id || target.className);
// Désactiver la protection après un délai (pour permettre la navigation normale)
clearTimeout(window.focusProtectionTimeout);
window.focusProtectionTimeout = setTimeout(() => {
shouldMaintainFocus = false;
}, 5000); // 5 secondes de protection
}
}, true);
// Détecter les changements d'input pour réactiver la protection
document.addEventListener('input', function(e) {
if (isLensPage() && e.target === currentFocusedElement) {
shouldMaintainFocus = true;
lastInputTime = Date.now();
// Réactiver la protection
clearTimeout(window.focusProtectionTimeout);
window.focusProtectionTimeout = setTimeout(() => {
shouldMaintainFocus = false;
}, 2000); // 2 secondes après la dernière frappe
}
}, true);
}
// ========================================
// PARTIE 2: AMÉLIORATION DE LA NAVIGATION TAB
// ========================================
function enhanceTabNavigation() {
let isNavigating = false;
document.addEventListener('keydown', function(e) {
if (e.key !== 'Tab' || !isLensPage()) return;
const activeElement = document.activeElement;
if (!activeElement || activeElement.tagName === 'BODY') return;
// Marquer qu'on est en navigation pour ne pas interférer
isNavigating = true;
shouldMaintainFocus = false; // Permettre le changement de focus
// Obtenir tous les inputs/selects visibles
const allInputs = getAllNavigableElements();
const currentIndex = allInputs.indexOf(activeElement);
if (currentIndex === -1) {
isNavigating = false;
return;
}
e.preventDefault();
e.stopPropagation();
// Calculer l'index suivant
let nextIndex;
if (e.shiftKey) {
nextIndex = currentIndex - 1;
if (nextIndex < 0) nextIndex = allInputs.length - 1;
} else {
nextIndex = currentIndex + 1;
if (nextIndex >= allInputs.length) nextIndex = 0;
}
// Naviguer vers le prochain élément
const nextElement = allInputs[nextIndex];
if (nextElement) {
console.log(`➡️ Navigation vers:`, nextElement.id || nextElement.className);
// Désactiver temporairement la protection
shouldMaintainFocus = false;
setTimeout(() => {
nextElement.focus();
// Réactiver la protection sur le nouvel élément
currentFocusedElement = nextElement;
shouldMaintainFocus = true;
// Sélectionner le contenu pour les inputs
if (nextElement.tagName === 'INPUT' &&
(nextElement.type === 'text' || nextElement.type === 'number')) {
nextElement.select();
}
isNavigating = false;
// Protection pendant 3 secondes
clearTimeout(window.focusProtectionTimeout);
window.focusProtectionTimeout = setTimeout(() => {
shouldMaintainFocus = false;
}, 3000);
}, 10);
}
}, true);
}
// Obtenir tous les éléments navigables dans l'ordre
function getAllNavigableElements() {
const allElements = [];
// IMPORTANT: D'abord TOUS les inputs OD, puis TOUS les inputs OG
// 1. Récupérer TOUS les inputs OD (pas les selects)
const odContainer = document.querySelector('app-file-first-lens:nth-child(1)');
if (odContainer) {
const odInputs = odContainer.querySelectorAll(`
input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([disabled]):not([readonly])
`);
const visibleOdInputs = [];
odInputs.forEach(input => {
if (input.offsetParent !== null && input.tabIndex !== -1) {
visibleOdInputs.push(input);
}
});
// Trier par position verticale puis horizontale
visibleOdInputs.sort((a, b) => {
const rectA = a.getBoundingClientRect();
const rectB = b.getBoundingClientRect();
// D'abord par position verticale
if (Math.abs(rectA.top - rectB.top) > 10) {
return rectA.top - rectB.top;
}
// Puis par position horizontale
return rectA.left - rectB.left;
});
allElements.push(...visibleOdInputs);
console.log(`📊 OD: ${visibleOdInputs.length} inputs trouvés`);
// Log pour debug
visibleOdInputs.forEach((el, index) => {
const label = el.getAttribute('aria-label') || el.id || el.placeholder || 'inconnu';
console.log(` OD ${index + 1}: ${label}`);
});
}
// 2. Récupérer TOUS les inputs OG (pas les selects)
const ogContainer = document.querySelector('app-file-first-lens:nth-child(2)');
if (ogContainer) {
const ogInputs = ogContainer.querySelectorAll(`
input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]):not([disabled]):not([readonly])
`);
const visibleOgInputs = [];
ogInputs.forEach(input => {
if (input.offsetParent !== null && input.tabIndex !== -1) {
visibleOgInputs.push(input);
}
});
// Trier par position verticale puis horizontale
visibleOgInputs.sort((a, b) => {
const rectA = a.getBoundingClientRect();
const rectB = b.getBoundingClientRect();
// D'abord par position verticale
if (Math.abs(rectA.top - rectB.top) > 10) {
return rectA.top - rectB.top;
}
// Puis par position horizontale
return rectA.left - rectB.left;
});
allElements.push(...visibleOgInputs);
console.log(`📊 OG: ${visibleOgInputs.length} inputs trouvés`);
// Log pour debug
visibleOgInputs.forEach((el, index) => {
const label = el.getAttribute('aria-label') || el.id || el.placeholder || 'inconnu';
console.log(` OG ${index + 1}: ${label}`);
});
}
console.log(`📋 Total: ${allElements.length} inputs navigables (selects exclus)`);
return allElements;
}
// ========================================
// PARTIE 3: INTERCEPTION DES SAVES ANGULAR
// ========================================
function interceptAngularSaves() {
// Intercepter les requêtes XHR pour détecter les saves
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
// Si c'est une requête de save sur la page lentilles
if (this.responseURL && this.responseURL.includes('/api/') && isLensPage()) {
// Sauvegarder l'élément actuellement focus
const elementToRestore = currentFocusedElement;
this.addEventListener('loadend', function() {
// Après la requête, restaurer le focus
if (elementToRestore && shouldMaintainFocus) {
console.log('🔄 Restauration focus post-save');
setTimeout(() => {
if (elementToRestore !== document.activeElement) {
elementToRestore.focus();
if (elementToRestore.type === 'number' || elementToRestore.type === 'text') {
elementToRestore.select();
}
}
}, 100);
}
});
}
return originalXHRSend.apply(this, arguments);
};
}
// ========================================
// PARTIE 4: OBSERVER POUR RÉACTIVATION
// ========================================
function observePageChanges() {
const observer = new MutationObserver(() => {
// Vérifier si on a changé de page
if (!isLensPage() && window.lensAutoSaveFixActive) {
console.log('📤 Sortie de la page lentilles');
window.lensAutoSaveFixActive = false;
shouldMaintainFocus = false;
currentFocusedElement = null;
} else if (isLensPage() && !window.lensAutoSaveFixActive) {
console.log('📥 Retour sur la page lentilles');
fixLensPageAutoSaveAndTab();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// ========================================
// INITIALISATION
// ========================================
protectFocus();
trackFocus();
enhanceTabNavigation();
interceptAngularSaves();
observePageChanges();
console.log('✨ Fix anti-défocus et navigation Tab activé avec succès !');
console.log('📝 Instructions:');
console.log(' - Tab/Shift+Tab pour naviguer');
console.log(' - Le focus est maintenu pendant la frappe');
console.log(' - Protection auto pendant 2-3 secondes après chaque action');
}
// ========================================
// ACTIVATION AUTOMATIQUE
// ========================================
// Vérifier et activer toutes les secondes
setInterval(() => {
const lensTab = document.querySelector('.tab.lens-0-tab.active');
if (lensTab && !window.lensAutoSaveFixActive) {
console.log('📍 Page Premières lentilles détectée - activation du fix');
fixLensPageAutoSaveAndTab();
}
}, 1000);
// Activation initiale si déjà sur la page
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(fixLensPageAutoSaveAndTab, 1000);
});
} else {
setTimeout(fixLensPageAutoSaveAndTab, 1000);
}
console.log('🚀 Module Anti-Défocus pour Premières Lentilles chargé');
// ========== MODULE VIDEO EXPLICATIVE ==========
const VideoExplanationModule = {
// Configuration des vidéos par modèle
videos: {
'expert prog': {
url: 'https://www.youtube.com/embed/P9Kmm2fg6wk?si=YTkBsPOXq50fzJAI',
title: 'Guide Expert Prog'
}
// Ajout d'autres modèles/vidéos ici si besoin
},
// État du module
currentButtons: new Map(),
modalOpen: false,
// Initialisation
init() {
console.log('📹 Initialisation du module vidéo explicative');
this.injectStyles();
this.setupObservers();
this.checkExistingSelects();
},
// Injection des styles CSS
injectStyles() {
if (document.getElementById('video-explanation-styles')) return;
const styles = `
/* Bouton vidéo */
.video-help-button {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 12px;
padding: 6px 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
vertical-align: middle;
}
.video-help-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
}
.video-help-button.pulse {
animation: pulse-video 2s infinite;
}
@keyframes pulse-video {
0% { box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); }
50% { box-shadow: 0 2px 16px rgba(102, 126, 234, 0.6); }
100% { box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); }
}
/* Modal vidéo */
.video-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 100000;
opacity: 0;
transition: opacity 0.3s ease;
backdrop-filter: blur(5px);
}
.video-modal-overlay.show {
opacity: 1;
}
.video-modal-container {
position: relative;
width: 90%;
max-width: 900px;
background: #1a1a1a;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
transform: scale(0.9);
transition: transform 0.3s ease;
}
.video-modal-overlay.show .video-modal-container {
transform: scale(1);
}
.video-modal-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.video-modal-title {
color: white;
font-size: 18px;
font-weight: 600;
margin: 0;
}
.video-modal-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s;
}
.video-modal-close:hover {
background: rgba(255, 255, 255, 0.3);
}
.video-modal-body {
position: relative;
padding-bottom: 56.25%; /* Ratio 16:9 */
height: 0;
background: #000;
}
.video-modal-body iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
`;
const styleEl = document.createElement('style');
styleEl.id = 'video-explanation-styles';
styleEl.textContent = styles;
document.head.appendChild(styleEl);
},
// Vérifier si un modèle nécessite une vidéo
needsVideo(modelText) {
if (!modelText) return null;
const lowerText = modelText.toLowerCase();
// Chercher dans la config des vidéos
for (const [key, videoData] of Object.entries(this.videos)) {
if (lowerText.includes(key)) {
return videoData;
}
}
return null;
},
// Créer le bouton vidéo
createVideoButton(videoData) {
const button = document.createElement('button');
button.className = 'video-help-button pulse';
button.innerHTML = `
<span>📹</span>
<span>Aide vidéo</span>
`;
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.openVideoModal(videoData);
button.classList.remove('pulse');
});
return button;
},
// Ouvrir le modal vidéo
openVideoModal(videoData) {
if (this.modalOpen) return;
this.modalOpen = true;
// Créer l'overlay et le modal
const overlay = document.createElement('div');
overlay.className = 'video-modal-overlay';
overlay.innerHTML = `
<div class="video-modal-container">
<div class="video-modal-header">
<h3 class="video-modal-title">${videoData.title}</h3>
<button class="video-modal-close">×</button>
</div>
<div class="video-modal-body">
<iframe
src="${videoData.url}"
title="${videoData.title}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen>
</iframe>
</div>
</div>
`;
document.body.appendChild(overlay);
// Animation d'ouverture
setTimeout(() => overlay.classList.add('show'), 10);
// Fermeture
const closeModal = () => {
overlay.classList.remove('show');
setTimeout(() => {
overlay.remove();
this.modalOpen = false;
}, 300);
};
// Event listeners pour fermer
overlay.querySelector('.video-modal-close').addEventListener('click', closeModal);
// Fermer en cliquant en dehors
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeModal();
}
});
// Fermer avec Echap
const escHandler = (e) => {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
console.log(`📹 Vidéo ouverte: ${videoData.title}`);
},
// Gérer l'ajout/suppression du bouton
handleSelectChange(selectElement) {
const selectId = selectElement.id;
const selectedOption = selectElement.options[selectElement.selectedIndex];
const modelText = selectedOption ? selectedOption.textContent : '';
// Retirer l'ancien bouton s'il existe
const existingButton = this.currentButtons.get(selectId);
if (existingButton) {
existingButton.remove();
this.currentButtons.delete(selectId);
}
// Vérifier si on doit ajouter un bouton
const videoData = this.needsVideo(modelText);
if (videoData) {
// Créer et ajouter le nouveau bouton
const button = this.createVideoButton(videoData);
// Trouver où insérer le bouton (après le select ou son conteneur)
const container = selectElement.closest('.amds-form-element') ||
selectElement.closest('.form-group') ||
selectElement.parentElement;
if (container) {
container.appendChild(button);
this.currentButtons.set(selectId, button);
// Retirer l'animation après 5 secondes
setTimeout(() => button.classList.remove('pulse'), 5000);
}
}
},
// Observer les changements sur les selects
setupObservers() {
// Observer les changements de valeur
const observeSelect = (selectId) => {
const select = document.querySelector(selectId);
if (select && !select.dataset.videoObserved) {
select.dataset.videoObserved = 'true';
// Écouter les changements
select.addEventListener('change', () => {
this.handleSelectChange(select);
});
// Vérification initiale
this.handleSelectChange(select);
}
};
// Observer les deux selects
setInterval(() => {
observeSelect('#input-rightmodel');
observeSelect('#input-leftmodel');
}, 1000);
},
// Vérifier les selects existants au chargement
checkExistingSelects() {
setTimeout(() => {
const rightSelect = document.querySelector('#input-rightmodel');
const leftSelect = document.querySelector('#input-leftmodel');
if (rightSelect) this.handleSelectChange(rightSelect);
if (leftSelect) this.handleSelectChange(leftSelect);
}, 2000);
}
};
// Barre d'espace pour navigation "Suivant"
function setupSpaceNext() {
console.log('Activation espace = suivant');
document.addEventListener('keydown', (e) => {
// Si c'est la barre espace
if (e.key === ' ' || e.code === 'Space') {
// Si on est dans un input, on ne fait rien
if (document.activeElement.tagName === 'INPUT' ||
document.activeElement.tagName === 'TEXTAREA') {
return;
}
// Chercher le bouton suivant
const nextButton = document.querySelector('#input-nextstep > button');
if (nextButton) {
e.preventDefault(); // Empêcher le scroll
nextButton.click();
console.log('Clic sur Suivant');
}
}
});
}
// Système de fix pour la navigation Tab avec auto-save
// Remplacer la fonction fixTabNavigationWithAutoSave existante (ligne ~3570)
function fixTabNavigationWithAutoSave() {
console.log('✅ Activation du fix de navigation Tab avec détection de page...');
// Variables pour tracker le focus
let lastFocusedElement = null;
let lastFocusedSelector = null;
let isAutoSaving = false;
let tabQueue = [];
const fieldsOrder = [
// Réfraction OD
'#input-rightsphere',
'#input-rightcylinder',
'#input-rightrefractionAxis',
'#input-rightaddition',
// Réfraction OG
'#input-leftsphere',
'#input-leftcylinder',
'#input-leftrefractionAxis',
'#input-leftaddition',
// Kératométrie OD
'#input-rightkParameter',
'#input-rightsteepKParameter',
'#input-rightkeratometryAxis',
'#input-righteccentricity',
'#input-rightsteepEccentricity',
'#input-rightvisibleIrisDiameterAt30',
// Kératométrie OG
'#input-leftkParameter',
'#input-leftsteepKParameter',
'#input-leftkeratometryAxis',
'#input-lefteccentricity',
'#input-leftsteepEccentricity',
'#input-leftvisibleIrisDiameterAt30'
];
// NOUVELLE FONCTION : Vérifier si on est sur la page information
function isOnInformationPage() {
return !!document.querySelector('#wrapper > main > app-file-layout > div > app-file-tab-information');
}
// NOUVELLE FONCTION : Vérifier si on est sur la page first-lens
function isOnFirstLensPage() {
return !!document.querySelector('#wrapper > main > app-file-layout > div > app-file-tab-first-lens');
}
// Fonctions helper existantes
function getElementSelector(element) {
if (element.id) return `#${element.id}`;
let selector = element.tagName.toLowerCase();
if (element.className) {
selector += '.' + element.className.split(' ').join('.');
}
const siblings = element.parentElement?.querySelectorAll(selector);
if (siblings && siblings.length > 1) {
const index = Array.from(siblings).indexOf(element);
selector += `:nth-of-type(${index + 1})`;
}
return selector;
}
function saveCurrentFocus() {
const activeElement = document.activeElement;
if (activeElement && activeElement.tagName === 'INPUT') {
lastFocusedElement = activeElement;
lastFocusedSelector = getElementSelector(activeElement);
if (activeElement.selectionStart !== undefined) {
lastFocusedElement.dataset.lastCursorPos = activeElement.selectionStart;
}
}
}
function restoreFocus() {
if (lastFocusedSelector) {
const element = document.querySelector(lastFocusedSelector);
if (element && element !== document.activeElement) {
console.log(`🎯 Restauration du focus sur ${lastFocusedSelector}`);
setTimeout(() => {
element.focus();
if (element.dataset.lastCursorPos) {
const pos = parseInt(element.dataset.lastCursorPos);
element.setSelectionRange(pos, pos);
delete element.dataset.lastCursorPos;
}
}, 50);
}
}
}
// Override de la navigation Tab MODIFIÉ
function overrideTabNavigation() {
document.addEventListener('keydown', (e) => {
// NOUVEAU : Vérifier qu'on est sur la page information
if (isOnFirstLensPage()) {
// Sur la page first-lens, ne PAS intercepter Tab
console.log('🚫 Page first-lens détectée - navigation Tab native');
return;
}
if (!isOnInformationPage()) {
// Si on n'est pas sur la page information, ne rien faire
return;
}
// Ne traiter que Tab sans Shift (navigation avant)
if (e.key === 'Tab' && !e.shiftKey) {
const activeElement = document.activeElement;
// Vérifier si on est dans un champ de notre liste
const currentIndex = fieldsOrder.findIndex(selector =>
activeElement === document.querySelector(selector)
);
if (currentIndex !== -1) {
// On est dans un champ géré
e.preventDefault();
e.stopPropagation();
console.log(`📍 Navigation depuis: ${fieldsOrder[currentIndex]}`);
saveCurrentFocus();
// Trouver le prochain champ
let nextIndex = currentIndex + 1;
let nextElement = null;
// Chercher le prochain élément disponible
while (nextIndex < fieldsOrder.length && !nextElement) {
nextElement = document.querySelector(fieldsOrder[nextIndex]);
if (!nextElement || nextElement.disabled || nextElement.readOnly) {
console.log(`⭕ ${fieldsOrder[nextIndex]} non disponible`);
nextElement = null;
nextIndex++;
}
}
// Si on est à la fin, boucler au début
if (!nextElement && nextIndex >= fieldsOrder.length) {
console.log('🔄 Fin de la liste, retour au début');
nextIndex = 0;
while (nextIndex < currentIndex && !nextElement) {
nextElement = document.querySelector(fieldsOrder[nextIndex]);
if (!nextElement || nextElement.disabled || nextElement.readOnly) {
nextElement = null;
nextIndex++;
}
}
}
if (nextElement) {
console.log(`➡️ Navigation vers: ${fieldsOrder[nextIndex]}`);
setTimeout(() => {
nextElement.focus();
nextElement.select();
}, isAutoSaving ? 100 : 0);
}
}
}
// Gérer aussi Shift+Tab (navigation arrière)
else if (e.key === 'Tab' && e.shiftKey) {
// NOUVEAU : Même vérification pour Shift+Tab
if (isOnFirstLensPage() || !isOnInformationPage()) {
return;
}
const activeElement = document.activeElement;
const currentIndex = fieldsOrder.findIndex(selector =>
activeElement === document.querySelector(selector)
);
if (currentIndex !== -1) {
e.preventDefault();
e.stopPropagation();
// Navigation arrière
let prevIndex = currentIndex - 1;
let prevElement = null;
while (prevIndex >= 0 && !prevElement) {
prevElement = document.querySelector(fieldsOrder[prevIndex]);
if (!prevElement || prevElement.disabled || prevElement.readOnly) {
prevElement = null;
prevIndex--;
}
}
if (!prevElement && prevIndex < 0) {
prevIndex = fieldsOrder.length - 1;
while (prevIndex > currentIndex && !prevElement) {
prevElement = document.querySelector(fieldsOrder[prevIndex]);
if (!prevElement || prevElement.disabled || prevElement.readOnly) {
prevElement = null;
prevIndex--;
}
}
}
if (prevElement) {
console.log(`⬅️ Navigation arrière vers: ${fieldsOrder[prevIndex]}`);
setTimeout(() => {
prevElement.focus();
prevElement.select();
}, isAutoSaving ? 100 : 0);
}
}
}
}, true);
}
// Détection de l'autoSave
function detectAutoSave() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.target.tagName === 'BUTTON') {
const button = mutation.target;
if (button.textContent?.includes('Enregistrer') ||
button.className?.includes('save')) {
console.log('💾 Auto-save détecté');
isAutoSaving = true;
saveCurrentFocus();
setTimeout(() => {
isAutoSaving = false;
restoreFocus();
}, 500);
}
}
});
});
observer.observe(document.body, {
attributes: true,
subtree: true,
attributeFilter: ['disabled', 'class']
});
}
// Initialisation
detectAutoSave();
overrideTabNavigation();
console.log('✅ Fix de navigation Tab activé avec détection de page');
console.log(`📊 ${fieldsOrder.length} champs dans l'ordre de navigation`);
console.log('📄 Actif uniquement sur app-file-tab-information');
}
// Fonction pour forcer l'enregistrement de tous les yeux
async function forceAutoSaveAllEyes() {
console.log('🔄 Forçage de l\'enregistrement pour les deux yeux...');
// Sélecteurs des boutons d'enregistrement
const saveButtonSelectors = [
// OD
"#wrapper > main > app-file-layout > div > app-file-tab-information > div.eyes-container > div > app-file-information-eye:nth-child(1) > div > div.header > div.header__actions > amds-button > button",
// OG
"#wrapper > main > app-file-layout > div > app-file-tab-information > div.eyes-container > div > app-file-information-eye:nth-child(2) > div > div.header > div.header__actions > amds-button > button"
];
let savedCount = 0;
for (const selector of saveButtonSelectors) {
const btn = document.querySelector(selector);
if (btn && !btn.disabled) {
console.log(`💾 Clic sur bouton enregistrer ${savedCount === 0 ? 'OD' : 'OG'}`);
// Montrer temporairement si caché
const wasHidden = btn.style.display === 'none';
if (wasHidden) btn.style.display = '';
btn.click();
// Re-cacher si nécessaire
if (wasHidden) {
setTimeout(() => btn.style.display = 'none', 100);
}
savedCount++;
// Attendre que l'enregistrement se fasse
await new Promise(resolve => setTimeout(resolve, 500));
}
}
if (savedCount > 0) {
console.log(`✅ ${savedCount} œil(x) enregistré(s)`);
// Attendre un peu plus pour être sûr
await new Promise(resolve => setTimeout(resolve, 1000));
}
return savedCount;
}
// Modifier les fonctions de calcul OrthoK et LRPG
async function performOrthoKCalculation() {
showToast('🔬 Démarrage du calcul OrthoK...');
console.log('🔬 Début du calcul OrthoK');
try {
// Forcer l'enregistrement d'abord
await forceAutoSaveAllEyes();
// Aller sur l'onglet lentille
const lensTab = document.querySelector('#wrapper > main > app-file-layout > div > app-tabs-list > div.tabs-container.has-actions > div.tabs.theme--classic.has-separators > div.tab.lens-0-tab.clickable.ng-star-inserted');
if (lensTab) {
lensTab.click();
console.log('✅ Clic sur l\'onglet lentille');
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Sélection OrthoK pour OD
const rightTypeSelect = document.querySelector('#input-righttype');
if (rightTypeSelect) {
rightTypeSelect.value = 'lens:type:orthok';
rightTypeSelect.dispatchEvent(new Event('change', { bubbles: true }));
rightTypeSelect.dispatchEvent(new Event('input', { bubbles: true }));
console.log('✅ OrthoK sélectionné pour OD');
}
await new Promise(resolve => setTimeout(resolve, 500));
// Sélection OrthoK pour OG
const leftTypeSelect = document.querySelector('#input-lefttype');
if (leftTypeSelect) {
leftTypeSelect.value = 'lens:type:orthok';
leftTypeSelect.dispatchEvent(new Event('change', { bubbles: true }));
leftTypeSelect.dispatchEvent(new Event('input', { bubbles: true }));
console.log('✅ OrthoK sélectionné pour OG');
}
await new Promise(resolve => setTimeout(resolve, 500));
showToast('✅ Calcul OrthoK terminé avec succès !');
console.log('✅ Calcul OrthoK complété');
} catch (error) {
console.error('❌ Erreur lors du calcul OrthoK:', error);
showToast('❌ Erreur lors du calcul OrthoK');
}
}
// Ajout styles split view
function injectSplitViewStyles() {
if (document.getElementById('clickfit-splitview-styles')) return;
const style = document.createElement('style');
style.id = 'clickfit-splitview-styles';
style.textContent = `
.clickfit-splitview-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 100000;
background: #181a1b;
display: flex;
align-items: stretch;
justify-content: stretch;
transition: opacity 0.3s;
}
.clickfit-splitview-iframe {
flex: 1 1 0;
border: none;
width: 50vw;
height: 100vh;
min-width: 0;
min-height: 0;
background: white;
}
.clickfit-splitview-close {
position: absolute;
top: 14px;
right: 18px;
z-index: 100001;
background: rgba(255,255,255,0.8);
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
font-size: 24px;
color: #181a1b;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
transition: background 0.2s;
opacity: 0.8;
}
.clickfit-splitview-close:hover {
background: #fff;
opacity: 1;
}
`;
document.head.appendChild(style);
}
// Cache le body sauf split view
let splitViewHiddenNodes = [];
function activateSplitView() {
if (document.querySelector('.clickfit-splitview-overlay')) return;
injectSplitViewStyles();
// Masquer tout le contenu du body sauf split view
splitViewHiddenNodes = [];
Array.from(document.body.children).forEach(node => {
if (
node.classList &&
node.classList.contains('clickfit-splitview-overlay')
) {
// ne rien faire
} else if (
node.tagName === 'SCRIPT' ||
node.tagName === 'STYLE' ||
node.id === 'clickfit-splitview-styles'
) {
// laisser styles/scripts
} else {
// masquer
splitViewHiddenNodes.push({
node: node,
prevDisplay: node.style.display
});
node.style.display = 'none';
}
});
// Créer le panneau split view
const overlay = document.createElement('div');
overlay.className = 'clickfit-splitview-overlay';
// Bouton fermer
const closeBtn = document.createElement('button');
closeBtn.className = 'clickfit-splitview-close';
closeBtn.innerHTML = '×';
closeBtn.title = 'Fermer Split View';
closeBtn.addEventListener('click', deactivateSplitView);
overlay.appendChild(closeBtn);
// 2 iframes côte à côte
const url = window.location.href;
const iframe1 = document.createElement('iframe');
iframe1.className = 'clickfit-splitview-iframe';
iframe1.src = url;
iframe1.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-modals');
const iframe2 = document.createElement('iframe');
iframe2.className = 'clickfit-splitview-iframe';
iframe2.src = url;
iframe2.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-modals');
overlay.appendChild(iframe1);
overlay.appendChild(iframe2);
document.body.appendChild(overlay);
}
function deactivateSplitView() {
// Supprimer split view
const overlay = document.querySelector('.clickfit-splitview-overlay');
if (overlay) overlay.remove();
// Restaurer le body
if (splitViewHiddenNodes && splitViewHiddenNodes.length > 0) {
splitViewHiddenNodes.forEach(({node, prevDisplay}) => {
node.style.display = prevDisplay || '';
});
splitViewHiddenNodes = [];
}
}
// Modal dragable et redimensionnable
function makeModalDraggableAndResizable() {
// Ajoute le style CSS pour le grip (coin bas droit)
if (!document.getElementById('cf-modal-draggable-resizable-style')) {
const style = document.createElement('style');
style.id = 'cf-modal-draggable-resizable-style';
style.textContent = `
.modal.modal--size-medium {
position: fixed !important;
/* Pour éviter le scroll du parent */
}
.modal.modal--size-medium .modal__header {
cursor: move;
user-select: none;
}
.modal.modal--size-medium .cf-modal-grip {
position: absolute;
width: 22px;
height: 22px;
right: 0;
bottom: 0;
cursor: nwse-resize;
z-index: 10;
}
.modal.modal--size-medium .cf-modal-grip::after {
content: "";
display: block;
width: 100%;
height: 100%;
background:
linear-gradient(135deg, transparent 65%, #bbb 70%, #bbb 100%);
opacity: 0.7;
border-bottom-right-radius: 6px;
}
.modal.modal--size-medium.cf-draggable-resizing {
pointer-events: none;
/* Empêche les clics pendant le resize */
}
`;
document.head.appendChild(style);
}
// Pour chaque modal medium natif Angular
document.querySelectorAll('.modal.modal--size-medium').forEach(modal => {
// Éviter de patcher deux fois
if (modal.dataset.cfDraggableResized === 'true') return;
modal.dataset.cfDraggableResized = 'true';
// --- DRAG ---
const header = modal.querySelector('.modal__header');
if (header && !header.dataset.cfDraggablePatched) {
header.dataset.cfDraggablePatched = 'true';
let dragging = false, offsetX = 0, offsetY = 0, startX = 0, startY = 0;
let origLeft, origTop;
const onMouseDown = (e) => {
// Ne draguer que bouton gauche
if (e.button !== 0) return;
dragging = true;
startX = e.clientX;
startY = e.clientY;
// Calcul de la position initiale
const rect = modal.getBoundingClientRect();
origLeft = rect.left;
origTop = rect.top;
document.body.style.userSelect = 'none';
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
};
const onMouseMove = (e) => {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
modal.style.left = (origLeft + dx) + 'px';
modal.style.top = (origTop + dy) + 'px';
modal.style.margin = '0';
};
const onMouseUp = () => {
dragging = false;
document.body.style.userSelect = '';
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
header.addEventListener('mousedown', onMouseDown);
}
// --- RESIZE ---
if (!modal.querySelector('.cf-modal-grip')) {
const grip = document.createElement('div');
grip.className = 'cf-modal-grip';
modal.appendChild(grip);
let resizing = false, startWidth = 0, startHeight = 0, startX = 0, startY = 0;
const minWidth = 400, minHeight = 200;
const onGripMouseDown = (e) => {
if (e.button !== 0) return;
e.stopPropagation();
resizing = true;
modal.classList.add('cf-draggable-resizing');
const rect = modal.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
startX = e.clientX;
startY = e.clientY;
document.body.style.userSelect = 'none';
window.addEventListener('mousemove', onGripMouseMove);
window.addEventListener('mouseup', onGripMouseUp);
};
const onGripMouseMove = (e) => {
if (!resizing) return;
let newW = Math.max(minWidth, startWidth + (e.clientX - startX));
let newH = Math.max(minHeight, startHeight + (e.clientY - startY));
modal.style.width = newW + 'px';
modal.style.height = newH + 'px';
modal.style.maxWidth = 'none';
modal.style.maxHeight = 'none';
};
const onGripMouseUp = () => {
resizing = false;
modal.classList.remove('cf-draggable-resizing');
document.body.style.userSelect = '';
window.removeEventListener('mousemove', onGripMouseMove);
window.removeEventListener('mouseup', onGripMouseUp);
};
grip.addEventListener('mousedown', onGripMouseDown);
}
// --- Initial position: center if not already positioned
if (!modal.style.left && !modal.style.top) {
// Centrer à l'ouverture
const rect = modal.getBoundingClientRect();
const winW = window.innerWidth, winH = window.innerHeight;
modal.style.left = Math.max(0, (winW - rect.width) / 2) + 'px';
modal.style.top = Math.max(0, (winH - rect.height) / 2) + 'px';
modal.style.margin = '0';
}
});
}
function autoCheckObservanceDefaults() {
console.log('🔍 Recherche de la page observance...');
// Garde-fou : vérifier qu'on est sur la bonne page
const observanceTitle = document.querySelector('h1, h2, h3, .title');
const isObservancePage = observanceTitle && observanceTitle.textContent.includes('Commun aux 2 yeux');
// Vérifier aussi la présence du composant observance
const observanceComponent = document.querySelector('app-observance');
if (!isObservancePage && !observanceComponent) {
console.log('❌ Pas sur la page observance');
return;
}
console.log('✅ Page observance détectée');
// Définir les options par défaut à cocher
const defaultSelections = [
{
name: 'Oxydant',
selector: 'app-store-field:nth-child(1) .input-radio-group > div:nth-child(1) input[type="radio"]'
},
{
name: 'Hebdomadaire',
selector: 'app-store-field:nth-child(2) .input-radio-group > div:nth-child(1) input[type="radio"]'
},
{
name: 'Aquadrop',
selector: 'app-store-field:nth-child(3) .input-radio-group > div:nth-child(1) input[type="radio"]'
},
{
name: 'Ventouse',
selector: 'app-store-field:nth-child(4) .input-radio-group > div:nth-child(2) input[type="radio"]'
}
];
// Pour chaque option par défaut
defaultSelections.forEach(selection => {
const radioButton = document.querySelector(`app-observance ${selection.selector}`);
if (radioButton && !radioButton.checked) {
// Cocher uniquement si pas déjà coché
radioButton.checked = true;
radioButton.dispatchEvent(new Event('change', { bubbles: true }));
console.log(`✅ ${selection.name} coché`);
}
});
}
// Observer pour détecter l'apparition de la page
const observanceObserver = new MutationObserver(() => {
// Vérifier si on trouve le texte caractéristique
const hasObservanceText = Array.from(document.querySelectorAll('*')).some(el =>
el.textContent && el.textContent.includes('Commun aux 2 yeux') &&
el.textContent.includes('Produit utilisé')
);
if (hasObservanceText) {
// Attendre un peu que tous les éléments soient chargés
setTimeout(() => {
autoCheckObservanceDefaults();
}, 500);
}
});
// Lancer l'observation
observanceObserver.observe(document.body, {
childList: true,
subtree: true
});
// Vérifier aussi immédiatement au cas où la page est déjà chargée
autoCheckObservanceDefaults();
//Sauvagarde dans la page Calcule lentilles
function interceptNextButton() {
// Observer pour détecter quand le bouton Suivant apparaît
const nextButtonObserver = new MutationObserver(() => {
const nextButton = document.querySelector('#wrapper > main > app-file-layout > div > app-tabs-list > div.tabs-container.has-actions > div.actions.ng-star-inserted > amds-button > button');
if (nextButton && !nextButton.dataset.intercepted) {
setupNextButtonInterceptor(nextButton);
}
});
// Observer le body pour détecter l'apparition du bouton
nextButtonObserver.observe(document.body, {
childList: true,
subtree: true
});
// Vérifier aussi immédiatement
const nextButton = document.querySelector('#wrapper > main > app-file-layout > div > app-tabs-list > div.tabs-container.has-actions > div.actions.ng-star-inserted > amds-button > button');
if (nextButton) {
setupNextButtonInterceptor(nextButton);
}
}
function setupNextButtonInterceptor(nextButton) {
// Marquer comme déjà intercepté
nextButton.dataset.intercepted = 'true';
// Sauvegarder la fonction click originale
const originalClick = nextButton.onclick;
// Créer notre propre handler
const interceptedClick = async function(event) {
console.log('🔄 Interception du clic sur Suivant...');
// Empêcher l'action par défaut temporairement
event.preventDefault();
event.stopPropagation();
// Chercher et cliquer sur les boutons Enregistrer des lentilles
const saveButtons = await findAndClickLensSaveButtons();
if (saveButtons.length > 0) {
console.log(`⏳ Attente de l'enregistrement de ${saveButtons.length} lentille(s)...`);
// Attendre un peu que les sauvegardes se fassent
await new Promise(resolve => setTimeout(resolve, 1000));
// Vérifier que les boutons sont redevenus disabled (signe que c'est sauvegardé)
let allSaved = false;
let attempts = 0;
while (!allSaved && attempts < 10) {
allSaved = saveButtons.every(btn => btn.disabled);
if (!allSaved) {
await new Promise(resolve => setTimeout(resolve, 500));
attempts++;
}
}
console.log('✅ Enregistrement terminé');
}
// Maintenant, exécuter le clic original
console.log('➡️ Exécution du clic sur Suivant');
// Retirer temporairement notre intercepteur pour éviter la boucle
nextButton.removeEventListener('click', interceptedClick, true);
// Cliquer pour de vrai
nextButton.click();
// Remettre l'intercepteur après un délai
setTimeout(() => {
nextButton.addEventListener('click', interceptedClick, true);
}, 100);
};
// Ajouter notre intercepteur (en capture pour être sûr d'être appelé en premier)
nextButton.addEventListener('click', interceptedClick, true);
// Ajouter un indicateur visuel (optionnel)
nextButton.title = 'Enregistrera automatiquement les lentilles avant de continuer';
console.log('✅ Bouton Suivant intercepté avec succès');
}
async function findAndClickLensSaveButtons() {
console.log('🔍 Recherche des boutons Enregistrer des lentilles...');
const saveButtons = [];
// Méthode 1 : Chercher par structure (page lentilles)
const lensContainers = document.querySelectorAll('.lens-container .header__actions amds-button');
lensContainers.forEach(amdsBtn => {
const btn = amdsBtn.querySelector('button');
if (btn && !btn.disabled) {
// Vérifier que c'est bien un bouton Enregistrer
const text = btn.textContent?.toLowerCase() || '';
const ariaLabel = btn.getAttribute('aria-label')?.toLowerCase() || '';
if (text.includes('enregistr') || ariaLabel.includes('save') || ariaLabel.includes('enregistr')) {
saveButtons.push(btn);
}
}
});
// Méthode 2 : Chercher par texte si méthode 1 ne trouve rien
if (saveButtons.length === 0) {
document.querySelectorAll('amds-button button').forEach(btn => {
if (!btn.disabled) {
const text = btn.textContent?.toLowerCase() || '';
if (text.includes('enregistr')) {
// Vérifier que c'est dans un contexte de lentille
const container = btn.closest('.lens-container, [class*="lens"]');
if (container) {
saveButtons.push(btn);
}
}
}
});
}
// Cliquer sur tous les boutons trouvés
saveButtons.forEach((btn, index) => {
console.log(`💾 Clic sur le bouton Enregistrer ${index + 1}`);
// Montrer temporairement le bouton s'il est caché
const wasHidden = btn.style.display === 'none';
if (wasHidden) {
btn.style.display = '';
}
btn.click();
// Re-cacher si nécessaire
if (wasHidden) {
setTimeout(() => {
btn.style.display = 'none';
}, 100);
}
});
return saveButtons;
}
// Alternative : Intercepter aussi avec la touche Espace
function enhanceSpaceNext() {
document.addEventListener('keydown', async (e) => {
if (e.key === ' ' || e.code === 'Space') {
// Si on n'est pas dans un input
if (document.activeElement.tagName !== 'INPUT' &&
document.activeElement.tagName !== 'TEXTAREA') {
const nextButton = document.querySelector('#wrapper > main > app-file-layout > div > app-tabs-list > div.tabs-container.has-actions > div.actions.ng-star-inserted > amds-button > button');
if (nextButton && !nextButton.disabled) {
e.preventDefault();
// Sauvegarder d'abord
console.log('Espace détecté - Sauvegarde avant Suivant...');
const saveButtons = await findAndClickLensSaveButtons();
if (saveButtons.length > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Puis cliquer sur Suivant
nextButton.click();
}
}
}
});
}
// Fonction pour réorganiser les sections
function reorderSectionsOnly() {
console.log('� Réorganisation simple des sections...');
// Attendre que les sections soient chargées
const eyeContainers = document.querySelectorAll('app-file-information-eye');
if (eyeContainers.length < 2) {
console.log('⚠ Les deux yeux ne sont pas encore chargés');
return;
}
// Pour chaque œil
eyeContainers.forEach((eyeContainer, eyeIndex) => {
const eyeName = eyeIndex === 0 ? 'OD' : 'OG';
console.log(`👁️ Réorganisation de l'œil ${eyeName}`);
// Trouver le conteneur principal des sections
const contentContainer = eyeContainer.querySelector('.eye > .content');
if (!contentContainer) {
console.log(`⚠️ Conteneur .content non trouvé pour ${eyeName}`);
return;
}
// Identifier toutes les sections
const sections = {
refraction: contentContainer.querySelector('app-file-information-eye-refraction'),
keratometry: contentContainer.querySelector('app-file-information-eye-keratometry'),
visualAcuity: contentContainer.querySelector('app-file-information-eye-visual-acuity'),
biometry: contentContainer.querySelector('app-file-information-eye-biometry')
};
// Vérifier que toutes les sections sont présentes
const foundSections = Object.entries(sections).filter(([key, el]) => el !== null);
console.log(`📍 ${eyeName}: ${foundSections.length}/4 sections trouvées`);
if (foundSections.length === 0) {
console.log(`❌ Aucune section trouvée pour ${eyeName}`);
return;
}
// Créer un fragment pour réorganiser
const fragment = document.createDocumentFragment();
if (eyeIndex === 0) {
// OD : Réfraction, Kératométrie, Acuité visuelle, Biométrie
if (sections.refraction) fragment.appendChild(sections.refraction);
if (sections.keratometry) fragment.appendChild(sections.keratometry);
if (sections.visualAcuity) fragment.appendChild(sections.visualAcuity);
if (sections.biometry) fragment.appendChild(sections.biometry);
} else {
// OG : Kératométrie, Réfraction, Biométrie, Acuité visuelle
if (sections.keratometry) fragment.appendChild(sections.keratometry);
if (sections.refraction) fragment.appendChild(sections.refraction);
if (sections.biometry) fragment.appendChild(sections.biometry);
if (sections.visualAcuity) fragment.appendChild(sections.visualAcuity);
}
// Vider et remplir le conteneur
while (contentContainer.firstChild) {
contentContainer.removeChild(contentContainer.firstChild);
}
contentContainer.appendChild(fragment);
console.log(` Sections réorganisées pour ${eyeName}`);
});
// Ajouter un style minimal juste pour la grille si nécessaire
if (!document.getElementById('simple-grid-fix')) {
const style = document.createElement('style');
style.id = 'simple-grid-fix';
style.textContent = `
/* Fix simple pour la grille sans changer le design */
app-file-information-eye > .eye > .content {
display: grid !important;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
gap: 16px;
}
/* Position dans la grille pour OD */
app-file-information-eye:nth-child(1) app-file-information-eye-refraction {
grid-column: 1;
grid-row: 1;
order: 1;
}
app-file-information-eye:nth-child(1) app-file-information-eye-keratometry {
grid-column: 2;
grid-row: 1;
order: 2;
}
app-file-information-eye:nth-child(1) app-file-information-eye-visual-acuity {
grid-column: 1;
grid-row: 2;
order: 3;
}
app-file-information-eye:nth-child(1) app-file-information-eye-biometry {
grid-column: 2;
grid-row: 2;
order: 4;
}
/* Position dans la grille pour OG */
app-file-information-eye:nth-child(2) app-file-information-eye-keratometry {
grid-column: 1;
grid-row: 1;
order: 1;
}
app-file-information-eye:nth-child(2) app-file-information-eye-refraction {
grid-column: 2;
grid-row: 1;
order: 2;
}
app-file-information-eye:nth-child(2) app-file-information-eye-biometry {
grid-column: 1;
grid-row: 2;
order: 3;
}
app-file-information-eye:nth-child(2) app-file-information-eye-visual-acuity {
grid-column: 2;
grid-row: 2;
order: 4;
}
/* Responsive : quand la fenêtre est réduite */
@media (max-width: 1800px) {
app-file-information-eye > .eye > .content {
display: flex !important;
flex-direction: column !important;
gap: 16px;
}
/* Pour OD en mode mobile : Réfraction, Kératométrie, Biométrie, Acuité visuelle */
app-file-information-eye:nth-child(1) app-file-information-eye-refraction {
order: 1 !important;
}
app-file-information-eye:nth-child(1) app-file-information-eye-keratometry {
order: 2 !important;
}
app-file-information-eye:nth-child(1) app-file-information-eye-biometry {
order: 3 !important;
}
app-file-information-eye:nth-child(1) app-file-information-eye-visual-acuity {
order: 4 !important;
}
/* Pour OG en mode mobile : même ordre que OD pour la cohérence */
app-file-information-eye:nth-child(2) app-file-information-eye-refraction {
order: 1 !important;
}
app-file-information-eye:nth-child(2) app-file-information-eye-keratometry {
order: 2 !important;
}
app-file-information-eye:nth-child(2) app-file-information-eye-biometry {
order: 3 !important;
}
app-file-information-eye:nth-child(2) app-file-information-eye-visual-acuity {
order: 4 !important;
}
}
`;
document.head.appendChild(style);
}
}
// Fonction pour réorganiser les champs de kératométrie
function setupSimpleSectionReorderSafe() {
console.log('🔄 Configuration du réorganisateur intelligent v2...');
let currentPatientId = null;
function getCurrentPatientId() {
const match = location.pathname.match(/\/file\/([A-Za-z0-9]+)/);
return match ? match[1] : null;
}
// Fonction de réorganisation robuste
async function performReorganization() {
console.log('⏳ Tentative de réorganisation...');
// Attendre que TOUTES les 8 sections soient présentes (4 par œil)
let attempts = 0;
const maxAttempts = 50; // 5 secondes max
while (attempts < maxAttempts) {
const sections = {
odRefraction: document.querySelector('app-file-information-eye:nth-child(1) app-file-information-eye-refraction'),
odKeratometry: document.querySelector('app-file-information-eye:nth-child(1) app-file-information-eye-keratometry'),
odVisualAcuity: document.querySelector('app-file-information-eye:nth-child(1) app-file-information-eye-visual-acuity'),
odBiometry: document.querySelector('app-file-information-eye:nth-child(1) app-file-information-eye-biometry'),
ogRefraction: document.querySelector('app-file-information-eye:nth-child(2) app-file-information-eye-refraction'),
ogKeratometry: document.querySelector('app-file-information-eye:nth-child(2) app-file-information-eye-keratometry'),
ogVisualAcuity: document.querySelector('app-file-information-eye:nth-child(2) app-file-information-eye-visual-acuity'),
ogBiometry: document.querySelector('app-file-information-eye:nth-child(2) app-file-information-eye-biometry')
};
const allSectionsLoaded = Object.values(sections).every(section => section !== null);
if (allSectionsLoaded) {
console.log('✅ Toutes les sections trouvées !');
// Attendre un tout petit peu que Angular finisse le rendu
await new Promise(resolve => setTimeout(resolve, 200));
// Réorganiser
reorderSectionsOnly();
// Puis réorganiser les champs de kératométrie
setTimeout(() => {
reorderKeratometryFields();
}, 100);
console.log('✅ Réorganisation complétée');
return true;
}
attempts++;
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log('⚠️ Timeout - toutes les sections non trouvées');
return false;
}
// Surveiller les changements d'URL (navigation SPA)
setInterval(async () => {
const newPatientId = getCurrentPatientId();
// Si on a changé de patient ou qu'on arrive sur un patient
if (newPatientId && newPatientId !== currentPatientId) {
console.log(`📋 Nouveau patient détecté: ${newPatientId}`);
currentPatientId = newPatientId;
// Lancer la réorganisation
await performReorganization();
}
}, 250); // Check toutes les 250ms
// Observer pour les changements dynamiques (au cas où)
const observer = new MutationObserver(() => {
// Si on est sur une fiche patient et qu'on n'a pas encore réorganisé
const patientId = getCurrentPatientId();
if (patientId && patientId === currentPatientId) {
// Ne rien faire, déjà traité
} else if (patientId) {
currentPatientId = patientId;
performReorganization();
}
});
// Observer le conteneur principal avec SUBTREE
const container = document.querySelector('#wrapper');
if (container) {
observer.observe(container, {
childList: true,
subtree: true
});
}
// Tentative initiale si déjà sur une fiche
if (getCurrentPatientId()) {
currentPatientId = getCurrentPatientId();
setTimeout(() => performReorganization(), 500);
}
}
function makeSpecificSectionsCollapsible() {
console.log('🔄 Patch sections rétractables...');
// CSS pour les sections rétractables
const styleId = 'simple-collapsible-patch';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.collapsible-header {
cursor: pointer !important;
user-select: none !important;
position: relative !important;
}
.section-chevron {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
transition: transform 0.3s ease;
font-size: 14px;
color: #666;
pointer-events: none;
}
.collapsed-section .section-chevron {
transform: translateY(-50%) rotate(-90deg);
}
.collapsed-section .accordion > .content {
display: none !important;
}
`;
document.head.appendChild(style);
}
function makeCollapsible(sectionElement, sectionName, eyeName) {
const accordion = sectionElement.querySelector('.accordion');
if (!accordion) {
console.log(`❌ Pas d'accordion trouvé pour ${sectionName} ${eyeName}`);
return;
}
const header = accordion.querySelector('.header');
if (!header || header.dataset.collapsible === 'true') {
// log seulement si header absent
if (!header) console.log(`❌ Header non trouvé pour ${sectionName} ${eyeName}`);
return;
}
header.dataset.collapsible = 'true';
header.classList.add('collapsible-header');
const oldChevron = header.querySelector('.section-chevron');
if (oldChevron) oldChevron.remove();
const chevron = document.createElement('span');
chevron.className = 'section-chevron';
chevron.innerHTML = '▼';
header.appendChild(chevron);
const isCollapsed = GM_getValue(`${sectionName}_${eyeName}_collapsed`, false);
if (isCollapsed) accordion.classList.add('collapsed-section');
else accordion.classList.remove('collapsed-section');
header.onclick = function(e) {
e.stopPropagation();
const wasCollapsed = accordion.classList.contains('collapsed-section');
if (wasCollapsed) {
accordion.classList.remove('collapsed-section');
} else {
accordion.classList.add('collapsed-section');
}
GM_setValue(`${sectionName}_${eyeName}_collapsed`, !wasCollapsed);
console.log(`${sectionName} ${eyeName}: ${!wasCollapsed ? 'fermé' : 'ouvert'}`);
};
console.log(`✅ ${sectionName} ${eyeName} rendue rétractable`);
}
function applyToSpecificSections() {
const eyeContainers = document.querySelectorAll('app-file-information-eye');
if (eyeContainers.length === 0) {
console.log('❌ Aucun conteneur œil trouvé');
return;
}
eyeContainers.forEach((container, index) => {
const eyeName = index === 0 ? 'OD' : 'OG';
const biometry = container.querySelector('app-file-information-eye-biometry');
if (biometry) makeCollapsible(biometry, 'biometry', eyeName);
const visualAcuity = container.querySelector('app-file-information-eye-visual-acuity');
if (visualAcuity) makeCollapsible(visualAcuity, 'visual-acuity', eyeName);
});
}
// Appliquer maintenant
applyToSpecificSections();
// Observer les changements dynamiques
if (!window._cfCollapsibleObserver) {
window._cfCollapsibleObserver = new MutationObserver(() => {
// Si aucune section rétractable n'est présente, réappliquer
if (document.querySelectorAll('.collapsible-header').length === 0) {
setTimeout(applyToSpecificSections, 300);
}
});
window._cfCollapsibleObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
// Variable pour éviter les lancements multiples
let isLaunched = false;
// Lancement global
function launch() {
if (isLaunched) return;
isLaunched = true;
console.log('Lancement du script ClickFit Assistant...');
// Module navigation patients
setTimeout(() => {
if (typeof SimplePatientNav !== 'undefined') {
SimplePatientNav.init();
}
}, 2000);
//Eléments rétractables
setTimeout(() => {
makeSpecificSectionsCollapsible();
// Réappliquer périodiquement pour les changements de page
setInterval(() => {
const needsReapply = document.querySelectorAll('.collapsible-header').length === 0;
if (needsReapply && window.location.pathname.includes('/file/')) {
makeSpecificSectionsCollapsible();
}
}, 2000);
}, 2000); // Attendre 2s au lieu de 500ms
// Module de réorganisation des sections
setTimeout(() => {
setupSectionReorganizer();
console.log(' Module de réorganisation des sections activé');
}, 2000);
// Injecter les styles CSS
if (typeof injectStyles === 'function') {
injectStyles();
}
// Module Video
if (typeof VideoExplanationModule !== 'undefined') {
setTimeout(() => {
VideoExplanationModule.init();
console.log('📹 Module vidéo explicative activé');
}, 2000);
}
// Autres initialisations
if (typeof interceptNextButton === 'function') interceptNextButton();
if (typeof enhanceSpaceNext === 'function') enhanceSpaceNext();
if (typeof setupRefractionPolling === 'function') setupRefractionPolling();
if (typeof setupSpaceNext === 'function') setupSpaceNext();
if (typeof createFloatingButton === 'function') createFloatingButton();
if (typeof setupKeyboardShortcuts === 'function') setupKeyboardShortcuts();
if (typeof setupSimpleSectionReorderSafe === 'function') setupSimpleSectionReorderSafe();
if (typeof reorderSectionsOnly === 'function') reorderSectionsOnly();
if (typeof setupKeratometryReorder === 'function') setupKeratometryReorder();
if (typeof fixTabNavigationWithAutoSave === 'function') fixTabNavigationWithAutoSave();
if (typeof autoFillPrescripteurCountry === 'function') autoFillPrescripteurCountry();
if (typeof autoCheckObservanceDefaults === 'function') autoCheckObservanceDefaults();
if (typeof scanAndObserveButtons === 'function') {
scanAndObserveButtons();
setInterval(() => {
scanAndObserveButtons();
}, 2000);
}
if (typeof setupButtonObserver === 'function') setupButtonObserver();
if (typeof setupLensPageObserver === 'function') setupLensPageObserver();
if (typeof applyCustomStepToAll === 'function') {
setTimeout(() => {
applyCustomStepToAll();
setInterval(() => {
applyCustomStepToAll();
}, 2000);
}, 1000);
}
if (typeof patchColorAutoChange === 'function') patchColorAutoChange();
console.log(' Tous les modules sont initialisés');
}
// Démarrage initial
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", launch);
} else {
setTimeout(launch, 1000);
}
})();