mirror of
https://github.com/efrainsosa27/cotizador-helgen.git
synced 2026-10-24 05:16:48 +00:00
Revert "Historial: nombre del vendedor desde el directorio de Google de helgen.tech"
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 dec6c4b93. This reverts commitc6c4b93. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c6c4b93da3
commit
18e305a36f
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. |
|
| `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. |
|
| `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`. |
|
| `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.** |
|
| `.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/`). |
|
| `.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`. |
|
| `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`).
|
- `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` =
|
- `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
|
`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**
|
(`nombresDeVendedores`, gana la más reciente); si no se conoce, el correo (`vendedor_email` o "Elaboró"). No modifica la hoja.
|
||||||
(`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
|
### 4.5 Guardar cotización
|
||||||
- `guardarCotizacion(payload)` *(UI)* — exige vendedor verificado (si no, error de sesión); normaliza, agrega clave/unidad SAT
|
- `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 `/ \ : * ? " < > |`),
|
normalizados. `normalizarProyecto(p)`, `nombreCarpetaProyecto(folio, cliente, nombre)` (quita `/ \ : * ? " < > |`),
|
||||||
`proyectosDesdeFilas`, `filtrarProyectos`.
|
`proyectosDesdeFilas`, `filtrarProyectos`.
|
||||||
- `normalizarProducto(p)`, `indiceDeClave(claves, clave)`.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
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`.
|
- 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).
|
- 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
|
### 6.9 Proyectos
|
||||||
- **Opcionales al cotizar** ("Sin proyecto" por omisión). Solo se pueden elegir proyectos `Activo`; el servidor lo vuelve a revisar.
|
- **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,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. */
|
/** Pide a Claude que elija y ordene entre los candidatos. null si no hay API key o algo falla. */
|
||||||
function ordenarConIaSat_(texto, candidatos) {
|
function ordenarConIaSat_(texto, candidatos) {
|
||||||
const apiKey = PropertiesService.getScriptProperties().getProperty('ANTHROPIC_API_KEY');
|
const apiKey = PropertiesService.getScriptProperties().getProperty('ANTHROPIC_API_KEY');
|
||||||
@ -640,12 +587,9 @@ function listarCotizaciones(busqueda) {
|
|||||||
const nombres = nombresDeVendedores(filas);
|
const nombres = nombresDeVendedores(filas);
|
||||||
const datos = filas.reverse();
|
const datos = filas.reverse();
|
||||||
const q = String(busqueda || '').toLowerCase().trim();
|
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))
|
.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 => ({
|
.map(r => ({
|
||||||
folio: r[0], fecha: r[1], cliente: r[2], empresa: r[3], total: r[9], elaboro: r[10], pdf: r[11],
|
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),
|
vendedor: vendedorDeFila(r, nombres),
|
||||||
@ -1068,20 +1012,6 @@ function vendedorDeFila(r, nombres) {
|
|||||||
return (nombres || {})[correo.toLowerCase()] || correo;
|
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). */
|
/** Texto sin acentos, en minúsculas y con espacios simples (para comparar nombres). */
|
||||||
function textoNormal(s) {
|
function textoNormal(s) {
|
||||||
return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
|
return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
|
||||||
|
|||||||
@ -1,14 +1,6 @@
|
|||||||
{
|
{
|
||||||
"timeZone": "America/Mexico_City",
|
"timeZone": "America/Mexico_City",
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
"enabledAdvancedServices": [
|
|
||||||
{
|
|
||||||
"userSymbol": "People",
|
|
||||||
"serviceId": "peopleapi",
|
|
||||||
"version": "v1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"exceptionLogging": "STACKDRIVER",
|
"exceptionLogging": "STACKDRIVER",
|
||||||
"runtimeVersion": "V8",
|
"runtimeVersion": "V8",
|
||||||
"webapp": {
|
"webapp": {
|
||||||
|
|||||||
@ -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('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.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');
|
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
|
// Datos iniciales: usuario = correo del token
|
||||||
const d=post({funcion:'getDatosIniciales',args:[],token:'t-bueno'}).datos;
|
const d=post({funcion:'getDatosIniciales',args:[],token:'t-bueno'}).datos;
|
||||||
assert.equal(d.usuario,'ana.lopez@helgen.tech'); assert.equal(d.vendedor.nombre,'Ana');
|
assert.equal(d.usuario,'ana.lopez@helgen.tech'); assert.equal(d.vendedor.nombre,'Ana');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user