From 18e305a36f104bb30bd638d413c237e639668540 Mon Sep 17 00:00:00 2001 From: "Efrain (Helgen)" Date: Tue, 20 Oct 2026 13:45:30 -0600 Subject: [PATCH] Revert "Historial: nombre del vendedor desde el directorio de Google de helgen.tech" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se decidió no publicar la búsqueda en el directorio (servicio People): el nombre de un vendedor viejo (p. ej. leo@helgen.tech) se corrige solo cuando guarde una cotización con sesión. appsscript.json vuelve a quedar sin servicios avanzados y Code.js igual a lo publicado en @5. Nunca se hizo clasp push de c6c4b93. This reverts commit c6c4b93. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 13 ++------ Code.js | 74 ++------------------------------------------ appsscript.json | 10 +----- tests/sesion.test.js | 34 -------------------- 4 files changed, 6 insertions(+), 125 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 55ce90a..881d074 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ para que un `clasp push` desde ella no pise la API con código viejo; no volver | `Code.js` | Todo el backend: API `doPost` con sesión + lógica (ver §4). Se sube a Apps Script. | | `Logo.js` | Logos en base64: `LOGO_HELGEN_AZUL` (PDF, fondo blanco, 700×200) y `LOGO_HELGEN_BLANCO`. Se sube. | | `Index.html` | Interfaz vieja de HtmlService. Se sube pero **no se usa** (`doGet` ya no la sirve; su `API_URL` es un marcador). La interfaz real es `web/cotizador/index.html`. | -| `appsscript.json` | Manifiesto: zona `America/Mexico_City`, V8, web app `executeAs: USER_DEPLOYING`, `access: ANYONE_ANONYMOUS`, servicio avanzado `People` (v1, directorio). Se sube. | +| `appsscript.json` | Manifiesto: zona `America/Mexico_City`, V8, web app `executeAs: USER_DEPLOYING`, `access: ANYONE_ANONYMOUS`. Se sube. | | `.clasp.json` | Liga la carpeta al proyecto `1GsH8Dja…`. **No cambiar el scriptId.** | | `.claspignore` | Lista blanca: solo se suben `Code.js`, `Logo.js`, `Index.html`, `appsscript.json` (excluye `anterior/`). | | `web/` | Portada (`index.html`, `logo-helgen-blanco.png`) y la interfaz real `web/cotizador/index.html` (§10). No se sube a Apps Script. `.assetsignore` excluye `Icon`/`.DS_Store`. | @@ -120,10 +120,7 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e - `getClientes()` *(UI)* — clientes que ya cotizaron, calculados desde la hoja `Cotizaciones` (`clientesDesdeFilas`). - `listarCotizaciones(busqueda)` *(UI)* — últimas 100 cotizaciones, filtro por folio/cliente/empresa/proyecto; `vendedor` = `vendedorDeFila`: nombre + apellido de la fila; en filas viejas (sin Q:S), el nombre que ese correo tiene en otras filas - (`nombresDeVendedores`, gana la más reciente); si tampoco y es @helgen.tech, el nombre del **directorio de Google** - (`completarNombresDirectorio_` → `nombreEnDirectorio_`, People API, caché 6 h `DIR_`, `-` = no encontrado; un error no - se guarda en caché); si no, el correo (`vendedor_email` o "Elaboró"). No modifica la hoja. -- `probarDirectorio()` — correr desde el editor: autoriza el permiso del directorio y muestra el nombre de quien lo ejecuta. + (`nombresDeVendedores`, gana la más reciente); si no se conoce, el correo (`vendedor_email` o "Elaboró"). No modifica la hoja. ### 4.5 Guardar cotización - `guardarCotizacion(payload)` *(UI)* — exige vendedor verificado (si no, error de sesión); normaliza, agrega clave/unidad SAT @@ -160,8 +157,7 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e normalizados. `normalizarProyecto(p)`, `nombreCarpetaProyecto(folio, cliente, nombre)` (quita `/ \ : * ? " < > |`), `proyectosDesdeFilas`, `filtrarProyectos`. - `normalizarProducto(p)`, `indiceDeClave(claves, clave)`. -- `nombresDeVendedores(filas)` (correo en minúsculas → "Nombre Apellido"), `vendedorDeFila(r, nombres)`, - `nombreDePersonaDirectorio(personas, correo)` (solo la persona con el correo exacto; nombre + apellido o `displayName`). +- `nombresDeVendedores(filas)` (correo en minúsculas → "Nombre Apellido"), `vendedorDeFila(r, nombres)`. - `terminosParaPdf(cfg, marca)` — renglones de T&C de la marca; agrega la línea de vigencia si falta. - `textoCelda(s)` — antepone `'` a texto que empiece con `= + - @` para que Sheets no lo lea como fórmula. - `validarClaimsToken(claims, clientId, dominio, ahora)` — ver §10.3. @@ -286,9 +282,6 @@ Actualizado, Actualizó, Clave SAT, Unidad SAT. A:C y G:H en formato texto. quita la configuración de web app). Ya pasó y hubo que borrar un proyecto duplicado. - Publicar siempre sobre el mismo deployment (misma `API_URL`). Nunca `clasp deploy` sin `--deploymentId`. - Si se agrega un servicio nuevo de Google, el dueño debe autorizar el permiso una vez desde el editor (`UrlFetchApp` ya está autorizado). - **Orden obligatorio:** `clasp push` → el dueño corre una función en el editor y acepta el permiso → recién entonces `clasp deploy`. - Si se publica antes de autorizar, la web app (que corre como el dueño) falla en **todas** las llamadas hasta que se autorice. - People (directorio) se agregó el 20/10/2026; se autoriza con `probarDirectorio()`. ### 6.9 Proyectos - **Opcionales al cotizar** ("Sin proyecto" por omisión). Solo se pueden elegir proyectos `Activo`; el servidor lo vuelve a revisar. diff --git a/Code.js b/Code.js index 458a817..ea21231 100644 --- a/Code.js +++ b/Code.js @@ -464,59 +464,6 @@ function probarIaSat() { }); } -// ---- Directorio de Google (nombre de vendedores sin nombre en la hoja) ---- - -/** - * Agrega al mapa correo → nombre los correos @helgen.tech que no tienen nombre en la hoja, buscándolos en el - * directorio de la organización (People API). No escribe nada en la hoja. Si algo falla, el correo se queda como está. - */ -function completarNombresDirectorio_(correos, nombres) { - const pendientes = {}; - (correos || []).forEach(c => { - const correo = String(c || '').trim().toLowerCase(); - if (correo.endsWith('@' + DOMINIO_HELGEN) && !nombres[correo]) pendientes[correo] = true; - }); - Object.keys(pendientes).forEach(correo => { - const nombre = nombreEnDirectorio_(correo); - if (nombre) nombres[correo] = nombre; - }); -} - -/** - * "Nombre Apellido" de una cuenta @helgen.tech según el directorio de Google; '' si no se encuentra o falla. - * Se guarda 6 h en caché (también cuando no se encuentra, para no consultar en cada carga del Historial). - * Requiere el servicio avanzado People (appsscript.json) y que el dueño autorice el permiso del directorio una vez. - */ -function nombreEnDirectorio_(correo) { - const cache = CacheService.getScriptCache(); - const clave = 'DIR_' + correo; - const previo = cache.get(clave); - if (previo !== null) return previo === '-' ? '' : previo; // '-' = no está en el directorio - let nombre = ''; - try { - if (typeof People === 'undefined') return ''; - const r = People.People.searchDirectoryPeople({ - query: correo, readMask: 'names,emailAddresses', sources: ['DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE'], - }); - nombre = nombreDePersonaDirectorio((r && r.people) || [], correo); - } catch (e) { - console.warn('Directorio sin respuesta para ' + correo + ': ' + e.message); - return ''; // no se guarda en caché: se reintenta en la siguiente carga - } - cache.put(clave, nombre || '-', 21600); - return nombre; -} - -/** Correr desde el editor: autoriza el permiso del directorio y muestra el nombre de la cuenta que lo ejecuta. */ -function probarDirectorio() { - const correo = Session.getEffectiveUser().getEmail().toLowerCase(); - CacheService.getScriptCache().remove('DIR_' + correo); - const r = People.People.searchDirectoryPeople({ - query: correo, readMask: 'names,emailAddresses', sources: ['DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE'], - }); - console.log(correo + ' → "' + nombreDePersonaDirectorio((r && r.people) || [], correo) + '"'); -} - /** Pide a Claude que elija y ordene entre los candidatos. null si no hay API key o algo falla. */ function ordenarConIaSat_(texto, candidatos) { const apiKey = PropertiesService.getScriptProperties().getProperty('ANTHROPIC_API_KEY'); @@ -640,12 +587,9 @@ function listarCotizaciones(busqueda) { const nombres = nombresDeVendedores(filas); const datos = filas.reverse(); const q = String(busqueda || '').toLowerCase().trim(); - const visibles = datos + return datos .filter(r => !q || (r[0] + ' ' + r[2] + ' ' + r[3] + ' ' + (r[19] || '')).toLowerCase().includes(q)) - .slice(0, 100); - // Correos @helgen.tech que siguen sin nombre: se buscan en el directorio de Google de la organización - completarNombresDirectorio_(visibles.map(r => r[16] || r[10]), nombres); - return visibles + .slice(0, 100) .map(r => ({ folio: r[0], fecha: r[1], cliente: r[2], empresa: r[3], total: r[9], elaboro: r[10], pdf: r[11], vendedor: vendedorDeFila(r, nombres), @@ -1068,20 +1012,6 @@ function vendedorDeFila(r, nombres) { return (nombres || {})[correo.toLowerCase()] || correo; } -/** - * Nombre de la persona del directorio cuyo correo coincide exacto (la búsqueda de Google también trae parecidos). - * Nombre de pila + apellido; si faltan, el nombre completo que muestra Google; '' si no hay coincidencia. - */ -function nombreDePersonaDirectorio(personas, correo) { - const buscado = String(correo || '').trim().toLowerCase(); - const p = (personas || []).find(x => (x.emailAddresses || []) - .some(e => String(e.value || '').trim().toLowerCase() === buscado)); - const n = p && (p.names || [])[0]; - if (!n) return ''; - return [n.givenName, n.familyName].map(x => String(x || '').trim()).filter(Boolean).join(' ') || - String(n.displayName || '').trim(); -} - /** Texto sin acentos, en minúsculas y con espacios simples (para comparar nombres). */ function textoNormal(s) { return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '') diff --git a/appsscript.json b/appsscript.json index c96ab77..2040776 100644 --- a/appsscript.json +++ b/appsscript.json @@ -1,14 +1,6 @@ { "timeZone": "America/Mexico_City", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "People", - "serviceId": "peopleapi", - "version": "v1" - } - ] - }, + "dependencies": {}, "exceptionLogging": "STACKDRIVER", "runtimeVersion": "V8", "webapp": { diff --git a/tests/sesion.test.js b/tests/sesion.test.js index 2c69b7a..4142958 100644 --- a/tests/sesion.test.js +++ b/tests/sesion.test.js @@ -97,40 +97,6 @@ const Q=(email,n,a)=>{const r=Array(19).fill('');r[16]=email;r[17]=n;r[18]=a;ret assert.equal(ctx.nombresDeVendedores([Q('a@helgen.tech','Ana','Viejo'),Q('A@helgen.tech','Ana','Nuevo'),Q('b@helgen.tech','','')])['a@helgen.tech'],'Ana Nuevo'); assert.equal(ctx.nombresDeVendedores([Q('b@helgen.tech','','')])['b@helgen.tech'],undefined,'sin nombre no entra al mapa'); assert.equal(ctx.vendedorDeFila(Q('','',''),{}),'','fila sin datos de vendedor'); - -// --- Directorio de Google (People API): correos @helgen.tech sin nombre en la hoja --- -let consultas=[], directorioFalla=false; -const PERSONAS={'leo@helgen.tech':{names:[{givenName:'Leonardo',familyName:'Ruiz Mora',displayName:'Leonardo Ruiz Mora'}],emailAddresses:[{value:'Leo@helgen.tech'}]}, - 'sinapellido@helgen.tech':{names:[{displayName:'Equipo Ventas'}],emailAddresses:[{value:'sinapellido@helgen.tech'}]}}; -ctx.People={People:{searchDirectoryPeople:o=>{consultas.push(o.query);if(directorioFalla)throw new Error('403 sin permiso'); - // Google también regresa personas parecidas: solo cuenta la que coincide exacto - return{people:[{names:[{givenName:'Leo',familyName:'Parecido'}],emailAddresses:[{value:'leo.p@helgen.tech'}]}].concat(PERSONAS[o.query]?[PERSONAS[o.query]]:[])}}}}; -const filaLeo=['HLG-2026-0010','x','Cliente','','','','',1,0.16,1.16,'leo@helgen.tech','u','[]','','Helgen','']; -const filaExt=['HLG-2026-0011','x','Cliente','','','','',1,0.16,1.16,'alguien@gmail.com','u','[]','','Helgen','']; -const filaNadie=['HLG-2026-0012','x','Cliente','','','','',1,0.16,1.16,'nadie@helgen.tech','u','[]','','Helgen','']; -cot.m.push(filaLeo.slice(),filaExt.slice(),filaNadie.slice(),filaLeo.slice()); -const antesHoja=JSON.stringify(cot.m); -const h3=post({funcion:'listarCotizaciones',args:[''],token:'t-bueno'}).datos; -const vend=f=>h3.filter(c=>c.folio===f).map(c=>c.vendedor); -assert.deepEqual(vend('HLG-2026-0010'),['Leonardo Ruiz Mora','Leonardo Ruiz Mora'],'nombre + apellido del directorio'); -assert.deepEqual(vend('HLG-2026-0011'),['alguien@gmail.com'],'correo de fuera de helgen.tech no se busca'); -assert.deepEqual(vend('HLG-2026-0012'),['nadie@helgen.tech'],'no está en el directorio = correo'); -assert.deepEqual(consultas.sort(),['antes@helgen.tech','leo@helgen.tech','nadie@helgen.tech'],'una consulta por correo, sin gmail ni correos con nombre'); -assert.equal(JSON.stringify(cot.m),antesHoja,'el directorio no escribe en la hoja'); -// Caché: la segunda carga no vuelve a consultar (tampoco los no encontrados) -consultas=[]; post({funcion:'listarCotizaciones',args:[''],token:'t-bueno'}); assert.deepEqual(consultas,[]); -// Solo displayName -assert.equal(ctx.nombreDePersonaDirectorio([PERSONAS['sinapellido@helgen.tech']],'sinapellido@helgen.tech'),'Equipo Ventas'); -assert.equal(ctx.nombreDePersonaDirectorio([],'x@helgen.tech'),''); -// Si el directorio falla (sin permiso), se muestra el correo y no se guarda en caché (se reintenta) -for(const k of Object.keys(cache)) if(k.startsWith('DIR_')) delete cache[k]; -directorioFalla=true; consultas=[]; -assert.deepEqual(post({funcion:'listarCotizaciones',args:['HLG-2026-0010'],token:'t-bueno'}).datos.map(c=>c.vendedor),['leo@helgen.tech','leo@helgen.tech']); -assert.equal(cache['DIR_leo@helgen.tech'],undefined,'un error no se guarda en caché'); -// Sin servicio People (antes de habilitarlo): correo -delete ctx.People; directorioFalla=false; -assert.deepEqual(post({funcion:'listarCotizaciones',args:['HLG-2026-0010'],token:'t-bueno'}).datos.map(c=>c.vendedor),['leo@helgen.tech','leo@helgen.tech']); -cot.m.splice(cot.m.length-4,4); // Datos iniciales: usuario = correo del token const d=post({funcion:'getDatosIniciales',args:[],token:'t-bueno'}).datos; assert.equal(d.usuario,'ana.lopez@helgen.tech'); assert.equal(d.vendedor.nombre,'Ana');