Keep Exploring Workstation

👤 -- ▾
👤
Sélectionnez ou créez un client
⚙️ COMPTE FRANCE-VISAS (AGENCE)
📍 Ville dépôt
🔄 WORKFLOW
🇫🇷
1. France-Visas
En attente
En attente...
📧
2. Email + Inscription TLS
En attente
En attente...
🔑
3. Activation email
En attente
Auto après inscription : attente 5s puis poll toutes les 10s (2min)
En attente...
🛂
4. Formulaire TLS
En attente
En attente...
'; w.document.write(out); w.document.close(); } function printDocsList() { var rows = document.querySelectorAll('#docs-modal-overlay table tr'); var lines = []; rows.forEach(function(r){ var lbl = r.querySelector('label'); if(lbl) lines.push('\u2610 ' + lbl.textContent); }); var w = window.open('','_blank','width=600,height=700'); var nom = (document.getElementById('f-nom')||{}).value||''; var prenom = (document.getElementById('f-prenom')||{}).value||''; w.document.write('Documents'); w.document.write('

\ud83d\udccb Documents requis

' + nom + ' ' + prenom + '

'); w.document.close(); } // Sync #f-typeVisa -> select injecte #ke-type-visa-select (inject_main.js) // \u26a0\ufe0f Appele par onchange de #f-typeVisa pour garder les deux UIs coherentes. function _syncInjectedVisaSel() { var frm = document.getElementById('f-typeVisa'); var inj = document.getElementById('ke-type-visa-select'); if (!frm || !inj || inj.value === frm.value) return; inj.value = frm.value; var badge = document.getElementById('ke-type-visa-badge'); if (badge) { badge.textContent = frm.value ? ('OK ' + frm.value) : '\u26a0\ufe0f Aucun type selectionne'; badge.style.cssText = 'margin-top:8px;font-size:11px;font-family:monospace;padding:6px 10px;border-radius:6px;' + (frm.value ? 'background:#7c3aed22;color:#a78bfa;border:1px solid #7c3aed44' : 'background:#1e293b;color:#64748b'); } } function saveCurrentClient() { if (selectedIdx === null) return; _saveDataOnly(); setTopbarStatus('\u{1F4BE} Sauvegarde'); lockForm(); } function deleteCurrentClient() { if (selectedIdx === null) return; if (!confirm('Supprimer ce client ?')) return; clients.splice(selectedIdx, 1); selectedIdx = null; document.getElementById('no-client').style.display = 'block'; document.getElementById('client-form').style.display = 'none'; saveState(); } // -- Add / New --------------------------------------------- function addNewClient() { var newClient = { nom:'', prenom:'', dateNaissance:'', lieuNaissance:'RABAT', paysNaissance:'MAR', sexe:'M', etatCivil:'CEL', telephone:'', activite:'83001', adresse:'', passeport:'', dateDelivrance:'', dateExpiration:'', visaPrecedent:'non', empreinteDigitale:'non', refFranceVisa:'', typeVisa:'Tourisme - Premiere demande', nbSejours:'3', hotelNom:'HOTEL DE PARIS', hotelAdresse:'10 RUE DE RIVOLI', hotelCodePostal:'75001', hotelVille:'PARIS', hotelTelephone:'0033100000000', typeClient:'individuel', groupSize:1, membres:[], profession:'', professionGarant:'', statuses:{}, logs:{} }; clients.push(newClient); selectedIdx = clients.length - 1; saveState(); selectClient(selectedIdx); // Nouveau client : déverrouiller directement sans confirmation _formLocked = false; document.getElementById('client-form').querySelectorAll('input, select, textarea').forEach(function(el) { el.disabled = false; el.style.opacity = ''; }); var btnMod = document.getElementById('btn-modifier'); var btnSave = document.getElementById('btn-sauvegarder'); if (btnMod) btnMod.style.display = 'none'; if (btnSave) btnSave.style.display = 'inline-flex'; switchTab('visa'); setTimeout(function(){ document.getElementById('f-nom').focus(); }, 100); } // -- Tabs -------------------------------------------------- function switchTab(name) { document.querySelectorAll('.tab,.tab-panel').forEach(function(el){ el.classList.remove('active'); }); var tabs = document.querySelectorAll('.tab'); var map = ['visa','identite','passeport','voyage','groupe']; tabs[map.indexOf(name)] && tabs[map.indexOf(name)].classList.add('active'); var panel = document.getElementById('tab-' + name); if (panel) panel.classList.add('active'); } // -- Groupe / Membres -------------------------------------- function toggleGroupe() { var type = document.getElementById('f-typeClient').value; document.getElementById('btn-add-membre').style.display = type === 'groupe' ? 'inline-flex' : 'none'; document.getElementById('btn-scan-all').style.display = type === 'groupe' ? 'inline-flex' : 'none'; renderMembres(clients[selectedIdx]); } function renderMembres(c) { var list = document.getElementById('membres-list'); var scanAllBtn = document.getElementById('btn-scan-all'); if (!c || c.typeClient !== 'groupe') { list.innerHTML = ''; if (scanAllBtn) scanAllBtn.style.display = 'none'; return; } if (scanAllBtn) scanAllBtn.style.display = 'inline-flex'; var membres = c.membres || []; list.innerHTML = membres.map(function(m, i) { return '
' + '
Membre ' + (i+1) + ' \u00b7 ' + (m.nom||'?') + ' ' + (m.prenom||'') + '' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
'; }).join(''); } function addMembre() { if (selectedIdx === null) return; var c = clients[selectedIdx]; if (!c.membres) c.membres = []; c.membres.push({ nom:'', prenom:'', dateNaissance:'', passeport:'', cin:'', dateDelivrance:'', dateExpiration:'', sexe:'M', lieuNaissance:'RABAT', telephone:'', refFranceVisa:'' }); c.groupSize = c.membres.length + 1; document.getElementById('f-groupSize').value = c.groupSize; saveState(); renderMembres(c); } function deleteMembre(i) { if (selectedIdx === null) return; var c = clients[selectedIdx]; c.membres.splice(i, 1); c.groupSize = c.membres.length + 1; document.getElementById('f-groupSize').value = c.groupSize; saveState(); renderMembres(c); } function updateMembre(i, key, val) { if (selectedIdx === null) return; var c = clients[selectedIdx]; if (!c.membres || !c.membres[i]) return; c.membres[i][key] = key === 'nom' || key === 'prenom' ? val.toUpperCase() : val; clearTimeout(autoSaveTimer); autoSaveTimer = setTimeout(saveState, 600); } // -- Status rendering -------------------------------------- var STEPS = [ { id:'fv', step:'france_visa', label:'1. France-Visas' }, { id:'tls-reg', step:'tls_register', label:'2. Email + Inscription TLS' }, { id:'tls-act', step:'tls_activation', label:'3. Activation email' }, { id:'tls-form',step:'tls_form', label:'4. Formulaire TLS' }, ]; function renderStatuses(c) { var s = (c && c.statuses) || {}; var logs = (c && c.logs) || {}; STEPS.forEach(function(st) { var el = document.getElementById('step-' + st.id); if (!el) return; var status = s[st.step] || 'pending'; el.className = 'step s-' + status; var statusEl = el.querySelector('.step-status'); if (statusEl) statusEl.textContent = statusLabel(status); var logBox = document.getElementById('log-' + st.id); if (logBox) renderLogBox(logBox, logs[st.step]); }); } function statusLabel(s) { return {pending:'En attente', running:'\u27f3 En cours...', done:'OK Termine', error:'ERR Erreur'}[s] || s; } function renderLogBox(box, logs) { if (!logs || !logs.length) { box.innerHTML = 'En attente...'; return; } box.innerHTML = logs.map(function(l) { var cls = /OK|OK|done|cree/i.test(l) ? 'log-ok' : /ERR|Erreur/i.test(l) ? 'log-err' : /\u26a0/i.test(l) ? 'log-warn' : ''; return '' + l + ''; }).join(''); box.scrollTop = box.scrollHeight; } function setStatus(ci, step, status) { if (ci === null || ci === undefined) ci = selectedIdx; if (!clients[ci]) return; if (!clients[ci].statuses) clients[ci].statuses = {}; clients[ci].statuses[step] = status; saveState(); if (ci === selectedIdx) renderStatuses(clients[ci]); // Notifier waitForStatus sans polling (evite le throttling Chrome en arriere-plan) document.dispatchEvent(new CustomEvent('ke_status_change', {detail: {ci: ci, step: step, status: status}})); } function addLog(ci, step, msg) { if (ci === null || ci === undefined) ci = selectedIdx; if (!clients[ci]) return; if (!clients[ci].logs) clients[ci].logs = {}; if (!clients[ci].logs[step]) clients[ci].logs[step] = []; clients[ci].logs[step].push(msg); if (clients[ci].logs[step].length > 50) clients[ci].logs[step].shift(); if (ci === selectedIdx) { var stepMap = {france_visa:'fv', email:'tls-reg', tls_register:'tls-reg', tls_activation:'tls-act', tls_form:'tls-form'}; var boxId = stepMap[step]; if (boxId) renderLogBox(document.getElementById('log-' + boxId), clients[ci].logs[step]); } } function resetStatuses() { if (selectedIdx === null) return; if (!confirm('Reinitialiser tous les statuts ?')) return; clients[selectedIdx].statuses = {}; clients[selectedIdx].logs = {}; saveState(); renderStatuses(clients[selectedIdx]); } function toggleStep(id) { var el = document.getElementById(id); if (el) el.classList.toggle('open'); } // -- Build full client for extension ----------------------------------- function buildClient() { if (selectedIdx === null) return null; saveCurrentClient(); var c = clients[selectedIdx]; if (!c) return null; var config = {}; try { config = JSON.parse(localStorage.getItem('ke_config') || '{}'); } catch(e) {} var fv = config.fv || {}; return Object.assign({}, c, { email: fv.email || '', password: fv.password || '', }); } // -- sendToExtension (compat inject_main.js) --------------------- window.sendToExtension = function(action, data, cb) { var payload = { source: 'workstation', action: action }; if (data && data.client) payload.client = data.client; else if (data) payload.client = data; window.postMessage(payload, '*'); if (typeof cb === 'function') setTimeout(function(){ cb({}); }, 0); return Promise.resolve({}); }; // -- Launchers ---------------------------------------- function launchFV() { var cl = buildClient(); if (!cl) { alert('Selectionnez un client'); return; } // Sauvegarder l'index du client actif pour retrouver la ref FV m\u00eame si le matching par nom \u00e9choue try { localStorage.setItem('ke_fv_pending_idx', String(selectedIdx)); } catch(e) {} setStatus(selectedIdx, 'france_visa', 'running'); addLog(selectedIdx, 'france_visa', '\ud83d\ude80 Lancement France-Visas...'); window.postMessage({ source: 'workstation', action: 'france_visas', client: cl }, '*'); } function launchTLSReg() { if (selectedIdx === null) { alert('Selectionnez un client'); return; } saveCurrentClient(); // keInscription = etape 2 (creation email @monemail.it) + etape 3 (inscription TLS) if (typeof window.keInscription === 'function') { window.keInscription(selectedIdx); } else { var cl = buildClient(); if (!cl) return; setStatus(selectedIdx, 'tls_register', 'running'); addLog(selectedIdx, 'tls_register', '\ud83d\udce7 Ouverture page inscription TLS...'); window.postMessage({ source: 'workstation', action: 'tls_register', client: cl }, '*'); } } function launchTLSAct() { if (selectedIdx === null) { alert('Selectionnez un client'); return; } if (typeof window.keActivation === 'function') { window.keActivation(selectedIdx); } else { addLog(selectedIdx, 'tls_activation', '\u26a0\ufe0f keActivation non disponible'); } } function launchTLSForm() { var cl = buildClient(); if (!cl) { alert('Selectionnez un client'); return; } setStatus(selectedIdx, 'tls_form', 'running'); addLog(selectedIdx, 'tls_form', '\ud83d\udec2 Ouverture formulaire TLS...'); window.postMessage({ source: 'workstation', action: 'tls_form', client: cl }, '*'); } // -- Ecoute messages retour extension ------------------------ function listenExtension() { window.addEventListener('message', function(ev) { if (!ev.data || ev.data.source !== 'ke_extension') return; var msg = ev.data; var ci = selectedIdx; if (msg.type === 'ke_status' && msg.step && msg.status) { setStatus(ci, msg.step, msg.status); if (msg.log) addLog(ci, msg.step, msg.log); } else if (msg.type === 'ke_log' && msg.step && msg.msg) { addLog(ci, msg.step, msg.msg); } else if (msg.type === 'ke_fv_ref' && msg.ref) { // Ancien format - utiliser selectedIdx if (ci !== null && clients[ci]) { clients[ci].refFranceVisa = msg.ref; saveState(); var el = document.getElementById('f-refFranceVisa'); if (el) el.value = msg.ref; addLog(ci, 'france_visa', 'OK Reference FV : ' + msg.ref); } } else if (msg.type === 'ke_fv_ref_update' && msg.ref) { // Si vient des refs en attente (_fromPending), recharger clients depuis localStorage if (msg._fromPending) { loadState(); renderClientList(); if (selectedIdx !== null && clients[selectedIdx]) { var el3 = document.getElementById('f-refFranceVisa'); if (el3 && clients[selectedIdx].refFranceVisa) el3.value = clients[selectedIdx].refFranceVisa; } setTopbarStatus('✅ Référence(s) France-Visas en attente appliquées'); return; } // Nouveau format : idx identifie par nom+prenom - fiable en multi-client var targetIdx = (typeof msg.idx === 'number' && msg.idx >= 0) ? msg.idx : ci; if (targetIdx !== null && targetIdx >= 0 && clients[targetIdx]) { if (typeof msg.memberIndex === 'number' && msg.memberIndex >= 0 && clients[targetIdx].membres && clients[targetIdx].membres[msg.memberIndex]) { clients[targetIdx].membres[msg.memberIndex].refFranceVisa = msg.ref; } else { clients[targetIdx].refFranceVisa = msg.ref; } saveState(); if (targetIdx === selectedIdx) { var el2 = document.getElementById('f-refFranceVisa'); if (el2) el2.value = msg.ref; } addLog(targetIdx, 'france_visa', 'OK Reference FV : ' + msg.ref + (msg.nom ? ' (' + msg.nom + ')' : '')); renderClientList(); } } else if (msg.type === 'ke_fv_done') { // Workflow France-Visas termine -> mettre a jour le statut pour debloquer runMultiStep if (ci !== null && clients[ci]) { var curFvSt = clients[ci].statuses && clients[ci].statuses.france_visa; if (curFvSt !== 'done' && curFvSt !== 'error') { setStatus(ci, 'france_visa', 'done'); addLog(ci, 'france_visa', 'OK Workflow France-Visas termine'); } } } else if (msg.type === 'ke_notification' && msg.data) { var d = msg.data; // Logs TLS en temps reel if (d.action === 'tls_log' && d.msg) { setTopbarStatus(d.msg); if (ci !== null) addLog(ci, 'tls_form', d.msg); } // Erreurs TLS visibles if (d.action === 'tls_error' && d.msg) { setTopbarStatus('\u26a0\ufe0f ' + d.msg); if (ci !== null) { addLog(ci, d.step || 'tls_form', d.msg); setStatus(ci, d.step || 'tls_form', 'error'); } } // TLS form termine if (d.action === 'tls_step_done' && d.step === 'tls_form') { var ok = d.data && d.data.success; setTopbarStatus(ok ? 'OK Dossier TLS confirme !' : '\u26a0\ufe0f TLS form termine'); if (ci !== null) setStatus(ci, 'tls_form', ok ? 'done' : 'error'); } } else if (msg.type === 'ke_clients_restored' && Array.isArray(msg.clients) && msg.clients.length > 0) { // Restore depuis serveur -> recharger la liste clients clients = msg.clients.map(function(c) { return Object.assign({ statuses:{}, logs:[] }, c); }); saveState(); renderClientList(); setTopbarStatus('OK ' + clients.length + ' client(s) restaure(s) depuis le serveur'); console.log('[KE WS] ke_clients_restored -> ' + clients.length + ' client(s)'); } }); } // -- Topbar status ------------------------------------------ function setTopbarStatus(msg) { var el = document.getElementById('topbar-status'); if (el) { el.textContent = msg; el.style.opacity = '1'; } clearTimeout(window._topbarTimer); window._topbarTimer = setTimeout(function() { if (el) el.style.opacity = '0.4'; }, 4000); } // -- toggleVisaPrec ------------------------------------------ function toggleVisaPrec() { var v = document.getElementById('f-visaPrecedent'); var row = document.getElementById('visa-prec-fields'); if (row) row.style.display = (v && v.value === 'oui') ? '' : 'none'; } // -- applyDateMask (JJ/MM/AAAA) ------------------------------ function applyDateMask(input) { var v = input.value.replace(/\D/g, ''); if (v.length >= 5) { v = v.substring(0,2) + '/' + v.substring(2,4) + '/' + v.substring(4,8); } else if (v.length >= 3) { v = v.substring(0,2) + '/' + v.substring(2); } input.value = v; } // -- autoFillExpiration : delivrance + 5 ans -> expiration ---- // expEl = element DOM du champ expiration (ou null). function autoFillExpiration(delivrEl, expEl) { if (!delivrEl || !expEl) return; var v = delivrEl.value; if (v.length < 10) return; var p = v.split('/'); if (p.length !== 3 || p[2].length !== 4) return; var y = parseInt(p[2], 10); if (isNaN(y) || y < 2000 || y > 2060) return; expEl.value = p[0] + '/' + p[1] + '/' + (y + 5); expEl.dispatchEvent(new Event('input', { bubbles: true })); } // ============================================================== // -- Scanner passeport (Claude Vision via proxy.php) ----------- // ============================================================== // Input fichier cache - partage client principal + membres groupe var _scanInput = null; var _scanTarget = null; // null = client principal | number = index membre var _multiScanFiles = null; // File[] pour scan groupe multi-fichiers var _multiScanIdx = 0; // index courant dans _multiScanFiles function getScanInput(multi) { // Recreer si on change de mode (single \u2194 multi) if (_scanInput && !!_scanInput.multiple !== !!multi) { document.body.removeChild(_scanInput); _scanInput = null; } if (!_scanInput) { _scanInput = document.createElement('input'); _scanInput.type = 'file'; _scanInput.accept = 'image/*,application/pdf'; _scanInput.multiple = !!multi; _scanInput.style.display = 'none'; document.body.appendChild(_scanInput); _scanInput.addEventListener('change', function(e) { var files = Array.from(e.target.files || []); e.target.value = ''; if (!files.length) return; if (_scanInput.multiple && files.length > 1) { // -- Mode multi : assigner fichier[i] -> membre[i] dans l\u2019ordre -- var c = selectedIdx !== null ? clients[selectedIdx] : null; var startIdx = typeof _scanTarget === 'number' ? _scanTarget : 0; _multiScanFiles = files; _multiScanIdx = 0; _runMultiScan(); } else { // -- Mode simple -- _doScan(files[0], _scanTarget); } }); } return _scanInput; } // Traiter les fichiers multi en sequence (recursif sur promesse) // _multiScanIdx === 0 -> demandeur principal (null) // _multiScanIdx === 1 -> membres[0], _multiScanIdx === 2 -> membres[1], etc. function _runMultiScan() { if (_multiScanIdx >= _multiScanFiles.length) { _setQueueStatus('OK ' + _multiScanFiles.length + ' passeport(s) importe(s) !'); setTimeout(_clearQueueStatus, 4000); _multiScanFiles = null; return; } var file = _multiScanFiles[_multiScanIdx]; var memberIdx = _multiScanIdx === 0 ? null : _multiScanIdx - 1; // 0->principal, 1->membre[0], 2->membre[1]... var total = _multiScanFiles.length; var current = _multiScanIdx + 1; var label = memberIdx === null ? 'Demandeur principal' : 'Membre ' + (memberIdx + 1); _setQueueStatus('\u23f3 Scan ' + current + '/' + total + ' - ' + label); _doScan(file, memberIdx, function onDone() { _multiScanIdx++; _runMultiScan(); }); } // Lancer le scan : memberIdx = undefined/null -> client principal, number -> membre function scanPasseport(memberIdx) { _scanTarget = (memberIdx !== undefined && memberIdx !== null) ? memberIdx : null; getScanInput(false).click(); } // -- OCR.space - lecture passeport sans MRZ ni API key -------------------- // Cle publique gratuite : 25 000 requetes/mois var OCR_SPACE_KEY = 'helloworld'; function _doScan(file, memberIdx, onDone) { var statusEl = document.getElementById('scan-status'); function setStatus(msg, cls) { if (!statusEl) return; statusEl.textContent = msg; statusEl.className = 'scan-status' + (cls ? ' ' + cls : ''); } function finish(ok, label) { if (typeof onDone === 'function') onDone(); else if (ok) _offerNextScan(memberIdx); else _clearQueueStatus(); setTimeout(function() { setStatus(''); }, 5000); } setStatus('\u23f3 Lecture du passeport...'); var reader = new FileReader(); reader.onload = function(e) { var b64 = e.target.result; // data:image/...;base64,... var fd = new FormData(); fd.append('base64Image', b64); fd.append('language', 'eng'); fd.append('isOverlayRequired', 'false'); fd.append('detectOrientation', 'true'); fd.append('scale', 'true'); fd.append('OCREngine', '2'); fd.append('apikey', OCR_SPACE_KEY); fetch('https://api.ocr.space/parse/image', { method: 'POST', body: fd }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.IsErroredOnProcessing) throw new Error(data.ErrorMessage || 'Erreur OCR'); var text = (data.ParsedResults || []).map(function(r) { return r.ParsedText || ''; }).join('\n'); // 1. Essai MRZ (2 lignes \u00d7 44 chars) var passport = _parseMRZ(text); // 2. Si pas de MRZ -> essai lecture libre de la page biographique if (!passport || !passport.passeport) passport = _parsePassportFree(text); if (passport && (passport.passeport || passport.nom)) { _applyScanData(passport, memberIdx); setStatus('OK ' + (passport.nom || passport.passeport || '') + ' importe !', 'ok'); finish(true); } else { setStatus('ERR Passeport non lisible - verifiez la qualite de la photo', 'err'); finish(false); } }) .catch(function(e) { setStatus('ERR ' + e.message, 'err'); finish(false); }); }; reader.onerror = function() { setStatus('ERR Impossible de lire le fichier', 'err'); finish(false); }; reader.readAsDataURL(file); } // -- Parseur libre (page biographique) ------------------------------------ function _parsePassportFree(text) { var r = {}; var lines = text.split(/[\n\r]+/).map(function(l) { return l.trim(); }).filter(Boolean); function afterLabel(labelRe) { for (var i = 0; i < lines.length; i++) { if (labelRe.test(lines[i].toUpperCase())) { var inLine = lines[i].replace(/^[^:]*:/, '').trim(); if (inLine && inLine.length > 1) return inLine; for (var j = i + 1; j < lines.length && j <= i + 2; j++) { if (lines[j] && lines[j].length > 1) return lines[j]; } } } return null; } function normDate(s) { if (!s) return ''; s = s.trim(); if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { var p = s.split('-'); return p[2]+'/'+p[1]+'/'+p[0]; } return s.replace(/\./g, '/'); } function cleanName(s) { return (s || '').toUpperCase().replace(/[^A-ZAA\u00c4EEE\u00cbI\u00cfOUU\u00dc\u00c7\s\-]/g, '').trim(); } // -- Numero passeport -- var mNum = text.toUpperCase().match(/\b([A-Z]{1,2}[0-9]{6,8})\b/); if (mNum) r.passeport = mNum[1]; // -- Nom -- var nom = afterLabel(/\b(NOM|SURNAME|LAST\s*NAME|\u0627\u0633\u0645 \u0627\u0644\u0639\u0627\u0626\u0644\u0629)\b/i); if (nom) r.nom = cleanName(nom); // -- Prenom -- var prenom = afterLabel(/\b(PR[EE]NOM|GIVEN\s*NAME|FIRST\s*NAME|\u0627\u0644\u0627\u0633\u0645 \u0627\u0644\u0634\u062e\u0635\u064a)\b/i); if (prenom) r.prenom = cleanName(prenom); // -- Dates : collecter toutes les dates du document -- var allDates = []; var reDates = /\b(\d{1,2}[\/\.\-]\d{1,2}[\/\.\-]\d{4}|\d{4}[\/\.\-]\d{2}[\/\.\-]\d{2})\b/g; var mD; while ((mD = reDates.exec(text)) !== null) allDates.push(normDate(mD[1])); // -- Date naissance -- var dob = afterLabel(/\b(DATE\s*(DE\s*)?NAISSANCE|DATE\s*OF\s*BIRTH|\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0627\u0632\u062f\u064a\u0627\u062f)\b/i); var dobM = dob && dob.match(/\d{1,2}[\/\.\-]\d{1,2}[\/\.\-]\d{4}|\d{4}[\/\.\-]\d{2}[\/\.\-]\d{2}/); if (dobM) r.dateNaissance = normDate(dobM[0]); else if (allDates[0]) r.dateNaissance = allDates[0]; // -- Date delivrance -- var del = afterLabel(/\b(DATE\s*(DE\s*)?(D[EE]LIVRANCE|D[EE]LIVRATION|D[EE]LIVR)|DATE\s*OF\s*ISSUE|\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0625\u0635\u062f\u0627\u0631)\b/i); var delM = del && del.match(/\d{1,2}[\/\.\-]\d{1,2}[\/\.\-]\d{4}|\d{4}[\/\.\-]\d{2}[\/\.\-]\d{2}/); if (delM) r.dateDelivrance = normDate(delM[0]); else if (allDates[1]) r.dateDelivrance = allDates[1]; // -- Date expiration -- var exp = afterLabel(/\b(DATE\s*(D[\'']?|DE\s*)?(EXPIRATION|EXPIRY|VALIDIT[EE])|\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0627\u0646\u062a\u0647\u0627\u0621)\b/i); var expM = exp && exp.match(/\d{1,2}[\/\.\-]\d{1,2}[\/\.\-]\d{4}|\d{4}[\/\.\-]\d{2}[\/\.\-]\d{2}/); if (expM) r.dateExpiration = normDate(expM[0]); else if (allDates[2]) r.dateExpiration = allDates[2]; // -- Adresse -- var adr = afterLabel(/\b(ADRESSE|ADDRESS|DOMICILE|\u0639\u0646\u0648\u0627\u0646)\b/i); if (adr) r.adresse = adr.toUpperCase().trim(); // -- Sexe -- var sexLine = afterLabel(/\b(SEXE|SEX|GENRE|\u0627\u0644\u062c\u0646\u0633)\b/i); if (sexLine && /^F/i.test(sexLine)) r.sexe = 'F'; else if (sexLine && /^M/i.test(sexLine)) r.sexe = 'M'; return (r.passeport || r.nom) ? r : null; } // -- Scan sequentiel groupe ------------------------------------------------ var _scanQueue = []; function startGroupScan() { if (selectedIdx === null) return; var c = clients[selectedIdx]; if (!c || !c.membres || c.membres.length === 0) { alert('Ajoutez d\'abord les membres.'); return; } // _scanTarget = null = titulaire principal (file[0] en multi, 1er clic en sequentiel) _scanTarget = null; // Queue sequentielle : null=titulaire principal, puis 0,1,2... pour les membres _scanQueue = [null].concat(c.membres.map(function(_, i) { return i; })); getScanInput(true).click(); } function _scanNextInQueue() { if (_scanQueue.length === 0) { _clearQueueStatus(); return; } var idx = _scanQueue[0]; var c = clients[selectedIdx]; var total = c ? (c.membres ? c.membres.length + 1 : 1) : '?'; var label = (idx === null) ? 'Demandeur principal' : 'Membre ' + (idx + 1) + '/' + (total - 1); _setQueueStatus('\ud83d\udcf7 Pret pour ' + label + ' - selectionnez la photo'); scanPasseport(idx); // null = titulaire, number = membre[idx] } function _offerNextScan(memberIdx) { // Retirer l'element courant de la queue (null ou number) if (_scanQueue.length > 0 && _scanQueue[0] === memberIdx) { _scanQueue.shift(); } if (_scanQueue.length === 0) { _setQueueStatus('OK Tous scannes !'); setTimeout(_clearQueueStatus, 4000); return; } var next = _scanQueue[0]; var c = clients[selectedIdx]; var total = c ? (c.membres ? c.membres.length + 1 : 1) : '?'; var label = (next === null) ? 'Demandeur principal' : 'Membre ' + (next + 1) + '/' + (total - 1); var qs = document.getElementById('scan-queue-status'); if (qs) { qs.innerHTML = ' -> '; } } function _setQueueStatus(msg) { var qs = document.getElementById('scan-queue-status'); if (qs) qs.textContent = msg; } function _clearQueueStatus() { _scanQueue = []; var qs = document.getElementById('scan-queue-status'); if (qs) qs.innerHTML = ''; } // -- Parseur MRZ passeport (TD3 : 2 lignes de 44 caracteres) -------------- function _parseMRZ(text) { var lines = text.split(/[\n\r]+/).map(function(l) { return l.toUpperCase().replace(/[^A-Z0-9<]/g, ''); }).filter(function(l) { return l.length >= 40; }); var line1 = null, line2 = null; for (var i = 0; i < lines.length; i++) { if (/^P[A-Z<][A-Z]{3}/.test(lines[i]) && lines[i].length >= 44) { line1 = lines[i].substring(0, 44); for (var j = i + 1; j < lines.length; j++) { if (lines[j].length >= 44) { line2 = lines[j].substring(0, 44); break; } } break; } } if (!line1 || !line2) return null; var countryCode = line1.substring(2, 5).replace(/= 0 ? namePart.substring(0, dblIdx) : namePart).replace(/= 0 ? namePart.substring(dblIdx + 2) : ''; var givennames = givenRaw.replace(/<<.*/,'').replace(/= 30 ? 1900 : 2000) + yy; } else { yyyy = (yy >= 50 ? 1900 : 2000) + yy; } return dd + '/' + mm + '/' + yyyy; } return { nom: surname, prenom: givennames, countryCode: countryCode, passeport: rawPasseport, dateNaissance: mrzDate(rawDOB, true), dateExpiration:mrzDate(rawExp, false), sexe: (sexeRaw === 'F') ? 'F' : 'M' }; } // -- Import / Export ------------------------------------------------------ function openBulkModal() { var m = document.getElementById('bulk-modal'); if (m) m.style.display = 'flex'; } function closeBulkModal() { var m = document.getElementById('bulk-modal'); if (m) m.style.display = 'none'; var ta = document.getElementById('bulk-json'); if (ta) ta.value = ''; var r = document.getElementById('bulk-result'); if (r) r.textContent = ''; } function doBulkImport() { var raw = (document.getElementById('bulk-json') || {}).value || ''; var result = document.getElementById('bulk-result'); raw = raw.trim(); if (!raw) { result.style.color='var(--red)'; result.textContent='\u274c Collez du JSON valide'; return; } var imported; try { imported = JSON.parse(raw); } catch(e) { result.style.color='var(--red)'; result.textContent='\u274c JSON invalide : '+e.message; return; } if (!Array.isArray(imported)) imported = [imported]; var added = 0, updated = 0; imported.forEach(function(nc) { if (!nc || typeof nc !== 'object') return; var ei = -1; if (nc.passeport) ei = clients.findIndex(function(c){ return c.passeport === nc.passeport; }); if (ei >= 0) { var ex = clients[ei]; var prevStatuses = ex.statuses, prevLogs = ex.logs; Object.assign(ex, nc); if (!nc.statuses) ex.statuses = prevStatuses || {}; if (!nc.logs) ex.logs = prevLogs || {}; updated++; } else { clients.push(Object.assign({ statuses:{ france_visa:'pending',email:'pending',tls_register:'pending',tls_activation:'pending',tls_form:'pending' }, logs: { france_visa:[],email:[],tls_register:[],tls_activation:[],tls_form:[] } }, nc)); added++; } }); saveState(); renderSidebar(); result.style.color = 'var(--green)'; result.textContent = '\u2705 ' + added + ' ajout\u00e9(s), ' + updated + ' mis \u00e0 jour'; setTimeout(closeBulkModal, 2500); } function exportClients() { var json = JSON.stringify(clients, null, 2); var blob = new Blob([json], { type: 'application/json' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'clients-keepexploring-' + new Date().toISOString().slice(0,10) + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // -- Appliquer les donnees scannees aux champs ----------------------------- function _applyScanData(p, memberIdx) { function fmtName(s) { return (s || '').toUpperCase(); } if (typeof memberIdx === 'number') { var c = (selectedIdx !== null) ? clients[selectedIdx] : null; var m = c && Array.isArray(c.membres) ? c.membres[memberIdx] : null; if (!m) { console.warn('[KE SCAN] Membre', memberIdx, 'introuvable'); return; } if (p.nom) m.nom = fmtName(p.nom); if (p.prenom) m.prenom = fmtName(p.prenom); if (p.passeport) m.passeport = p.passeport; if (p.dateNaissance) m.dateNaissance = p.dateNaissance; if (p.dateDelivrance) m.dateDelivrance = p.dateDelivrance; if (p.dateExpiration) m.dateExpiration = p.dateExpiration; if (p.adresse) m.adresse = p.adresse.toUpperCase(); if (p.sexe) m.sexe = p.sexe; renderMembres(c); saveState(); return; } function setF(id, val) { if (!val) return; var el = document.getElementById(id); if (!el) return; el.value = val; el.dispatchEvent(new Event('input', { bubbles: true })); } setF('f-nom', fmtName(p.nom)); setF('f-prenom', fmtName(p.prenom)); setF('f-passeport', p.passeport); setF('f-dateNaissance', p.dateNaissance); setF('f-dateDelivrance',p.dateDelivrance); setF('f-dateExpiration',p.dateExpiration); setF('f-adresse', p.adresse ? p.adresse.toUpperCase() : ''); if (p.sexe) { var s = document.getElementById('f-sexe'); if (s) s.value = p.sexe; } autoSave(); } // -- Navigation clavier entre clients (^ / v) ------------ document.addEventListener('keydown', function(e) { if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; var ae = document.activeElement; var tag = ae && ae.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA') return; // Bloquer SELECT + tout autre élément e.preventDefault(); e.stopPropagation(); if (!clients || !clients.length) return; var cards = document.querySelectorAll('#client-list .client-card'); if (!cards.length) return; var visibleIdx = []; cards.forEach(function(card) { var idx = parseInt(card.getAttribute('data-idx')); if (!isNaN(idx)) visibleIdx.push(idx); }); if (!visibleIdx.length) return; var pos = visibleIdx.indexOf(selectedIdx); if (pos === -1) pos = 0; var newPos = pos + (e.key === 'ArrowDown' ? 1 : -1); if (newPos < 0) newPos = 0; if (newPos >= visibleIdx.length) newPos = visibleIdx.length - 1; var nextIdx = visibleIdx[newPos]; if (nextIdx === selectedIdx) return; selectClient(nextIdx); // Scroll sidebar var card = cards[newPos]; var sidebar = document.getElementById('ke-sidebar'); if (card && sidebar) { var cardTop = card.offsetTop; var cardBot = cardTop + card.offsetHeight; var margin = 50; if (cardTop - margin < sidebar.scrollTop) { sidebar.scrollTop = cardTop - margin; } else if (cardBot + margin > sidebar.scrollTop + sidebar.clientHeight) { sidebar.scrollTop = cardBot + margin - sidebar.clientHeight; } } }, true); function creerEmailClient() { var nom = (document.getElementById('f-nom').value || '').trim(); var prenom = (document.getElementById('f-prenom').value || '').trim(); if (!nom || !prenom) { alert('Remplis le nom et prenom d\'abord'); return; } var btn = document.getElementById('btn-creer-email'); var status = document.getElementById('email-creer-status'); btn.disabled = true; btn.textContent = '\u23f3 Creation en cours...'; status.style.display = 'none'; // world:MAIN -> passer par postMessage -> content_workstation.js -> background function _onEmailResp(ev) { if (!ev.data || ev.data.source !== 'ke_extension' || ev.data.action !== 'create_client_email_result') return; window.removeEventListener('message', _onEmailResp); var resp = ev.data; btn.disabled = false; btn.textContent = '\u2709 Creer email @monemail.it'; if (resp && resp.ok) { // f-emailFranceVisa = email agence saisi manuellement - NE PAS ecraser document.getElementById('f-tlsEmail').value = resp.email; document.getElementById('f-emailPassword').value = resp.password; document.getElementById('fg-email-password').style.display = ''; status.style.display = ''; status.style.color = '#22c55e'; status.textContent = '\u2713 ' + resp.email + ' cree'; autoSave(); } else { status.style.display = ''; status.style.color = '#ef4444'; status.textContent = '\u2717 ' + (resp && resp.error ? resp.error : 'Erreur inconnue'); } } window.addEventListener('message', _onEmailResp); window.postMessage({ source: 'workstation', action: 'create_client_email', prenom: prenom, nom: nom, agent: _activeAgent() }, '*'); } Idx = selectedIdx;\n\n\n