mirror of
https://github.com/efrainsosa27/cotizador-helgen.git
synced 2026-10-24 05:16:48 +00:00
Historial: nombre del vendedor desde el directorio de Google de helgen.tech
Las cotizaciones de vendedores que nunca guardaron con sesión (p. ej. leo@helgen.tech) seguían mostrando el correo. Ahora, para correos @helgen.tech sin nombre en la hoja, listarCotizaciones busca el nombre y apellido en el directorio de la organización (servicio avanzado People, searchDirectoryPeople con perfiles del dominio), solo acepta la persona con el correo exacto y guarda el resultado 6 h en caché. Si el directorio falla o no hay permiso, se muestra el correo como antes. No escribe en la hoja. probarDirectorio() autoriza el permiso desde el editor. Pruebas en sesion.test.js. CLAUDE.md §3, §4.4, §4.6 y §6.8 (orden: push, autorizar, deploy). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fc621d23b4
commit
c6c4b93da3
13
CLAUDE.md
13
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`. Se sube. |
|
||||
| `appsscript.json` | Manifiesto: zona `America/Mexico_City`, V8, web app `executeAs: USER_DEPLOYING`, `access: ANYONE_ANONYMOUS`, servicio avanzado `People` (v1, directorio). 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,7 +120,10 @@ 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 no se conoce, el correo (`vendedor_email` o "Elaboró"). No modifica la hoja.
|
||||
(`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_<correo>`, `-` = 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.
|
||||
|
||||
### 4.5 Guardar cotización
|
||||
- `guardarCotizacion(payload)` *(UI)* — exige vendedor verificado (si no, error de sesión); normaliza, agrega clave/unidad SAT
|
||||
@ -157,7 +160,8 @@ 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)`.
|
||||
- `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`).
|
||||
- `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.
|
||||
@ -282,6 +286,9 @@ 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.
|
||||
|
||||
74
Code.js
74
Code.js
@ -464,6 +464,59 @@ 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');
|
||||
@ -587,9 +640,12 @@ function listarCotizaciones(busqueda) {
|
||||
const nombres = nombresDeVendedores(filas);
|
||||
const datos = filas.reverse();
|
||||
const q = String(busqueda || '').toLowerCase().trim();
|
||||
return datos
|
||||
const visibles = datos
|
||||
.filter(r => !q || (r[0] + ' ' + r[2] + ' ' + r[3] + ' ' + (r[19] || '')).toLowerCase().includes(q))
|
||||
.slice(0, 100)
|
||||
.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
|
||||
.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),
|
||||
@ -1012,6 +1068,20 @@ 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, '')
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
{
|
||||
"timeZone": "America/Mexico_City",
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"enabledAdvancedServices": [
|
||||
{
|
||||
"userSymbol": "People",
|
||||
"serviceId": "peopleapi",
|
||||
"version": "v1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"exceptionLogging": "STACKDRIVER",
|
||||
"runtimeVersion": "V8",
|
||||
"webapp": {
|
||||
|
||||
@ -97,6 +97,40 @@ 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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user