Historial: nombre del vendedor también en cotizaciones viejas

Las filas anteriores a la sesión con Google no tienen vendedor_nombre/apellido y mostraban el correo.
listarCotizaciones ahora arma un mapa correo → "Nombre Apellido" con las filas que sí lo tienen
(nombresDeVendedores, gana la más reciente) y lo usa para las viejas (vendedorDeFila); si el correo no
se conoce, sigue mostrando el correo. Solo cambia la respuesta: la hoja no se modifica.

Pruebas en sesion.test.js. CLAUDE.md: §4.4, §4.6 y registros de prueba en la hoja real
(HLG-2026-0005, HLG-2026-0006, PRY-2026-0001) para excluirlos de reportes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Efrain (Helgen) 2026-10-20 12:58:24 -06:00
parent 3b7f5ec953
commit 3c410b46ad
3 changed files with 53 additions and 5 deletions

View File

@ -118,8 +118,9 @@ o son lógica pura (sección 5 del archivo, sin servicios de Google, probables e
### 4.4 Datos para la interfaz
- `getDatosIniciales()` *(UI, al cargar)* — IVA, vigencia, marcas, catálogo, clientes, `usuario` (correo) y `vendedor` (`{email, nombre, apellido}`).
- `getClientes()` *(UI)* — clientes que ya cotizaron, calculados desde la hoja `Cotizaciones` (`clientesDesdeFilas`).
- `listarCotizaciones(busqueda)` *(UI)* — últimas 100 cotizaciones, filtro por folio/cliente/empresa; `vendedor` = nombre + apellido
(filas viejas: `vendedor_email` o "Elaboró").
- `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.
### 4.5 Guardar cotización
- `guardarCotizacion(payload)` *(UI)* — exige vendedor verificado (si no, error de sesión); normaliza, agrega clave/unidad SAT
@ -156,6 +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)`.
- `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.
@ -196,6 +198,12 @@ del catálogo, claveSat/unidadSat), Notas, Marca (Helgen / Helgen Energy), Direc
filas 0001–0004 no las tienen llenas. La T se agregó con Proyectos (10/2026): las filas anteriores quedan vacías y no se mueven.
`guardarCotizacion` reescribe la fila de encabezados si no coincide con `ENCABEZADOS_COTIZACIONES`.
**Registros de prueba en la hoja real (excluir de reportes y totales; identificarlos por folio, no por texto):**
- `HLG-2026-0005` — cliente "PRUEBA - CANCELADA" (prueba de la migración, 19/10/2026).
- `HLG-2026-0006` — cliente renombrado a "PRUEBA – CANCELADA" (con guion largo; prueba del primer proyecto, 20/10/2026).
- `PRY-2026-0001` — "Proyecto de Prueba oficial", estado `Cancelado` (20/10/2026). Su carpeta en `Proyectos/2026` es de prueba.
- Las filas HLG-2026-0001 a 0004 son anteriores a la sesión con Google y también parecen pruebas, pero no se marcaron.
**Pestaña `Config`** (Clave | Valor, columna B en formato texto): `EMPRESA_NOMBRE` (Helgen Servicios Especializados),
`EMPRESA_RFC` (HSE240304PC1), `EMPRESA_DIRECCION`, `EMPRESA_TELEFONO` (+52 442 644 5391), `EMPRESA_EMAIL`, `EMPRESA_WEB`,
`PREFIJO_FOLIO` (HLG), `IVA_TASA` (0.16), `VIGENCIA_DIAS` (15), `MONEDA`, `LOGO_FILE_ID` (vacío = logo de `Logo.js`),

33
Code.js
View File

@ -582,15 +582,17 @@ function getClientes() {
/** Historial (últimas 100, con búsqueda opcional). */
function listarCotizaciones(busqueda) {
const hoja = db_().getSheetByName('Cotizaciones');
const datos = hoja.getDataRange().getDisplayValues().slice(1).reverse();
const filas = hoja.getDataRange().getDisplayValues().slice(1);
// Correo → "Nombre Apellido" con todas las filas (antes de filtrar), para ponerle nombre a las filas viejas
const nombres = nombresDeVendedores(filas);
const datos = filas.reverse();
const q = String(busqueda || '').toLowerCase().trim();
return datos
.filter(r => !q || (r[0] + ' ' + r[2] + ' ' + r[3] + ' ' + (r[19] || '')).toLowerCase().includes(q))
.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],
// Filas anteriores a la sesión con Google no tienen vendedor: se muestra el correo de "Elaboró"
vendedor: [r[17], r[18]].filter(Boolean).join(' ') || r[16] || r[10] || '',
vendedor: vendedorDeFila(r, nombres),
// Filas anteriores a Proyectos (o cotizadas sin proyecto)
proyecto: r[19] || 'Sin proyecto',
}));
@ -985,6 +987,31 @@ function clientesDesdeFilas(filas) {
return lista;
}
/**
* Correo (minúsculas) → "Nombre Apellido", tomado de las filas de Cotizaciones que sí tienen vendedor_nombre /
* vendedor_apellido (columnas Q:S). Filas en orden de la hoja: si un correo aparece con nombres distintos, gana la más reciente.
*/
function nombresDeVendedores(filas) {
const mapa = {};
(filas || []).forEach(r => {
const correo = String(r[16] || '').trim().toLowerCase();
const nombre = [r[17], r[18]].map(x => String(x || '').trim()).filter(Boolean).join(' ');
if (correo && nombre) mapa[correo] = nombre;
});
return mapa;
}
/**
* Vendedor para el Historial: nombre y apellido de la fila; si no tiene (filas anteriores a la sesión con Google),
* el nombre que ese correo tiene en otras filas; si no se conoce, el correo (vendedor_email o "Elaboró").
*/
function vendedorDeFila(r, nombres) {
const propio = [r[17], r[18]].map(x => String(x || '').trim()).filter(Boolean).join(' ');
if (propio) return propio;
const correo = String(r[16] || r[10] || '').trim();
return (nombres || {})[correo.toLowerCase()] || correo;
}
/** Texto sin acentos, en minúsculas y con espacios simples (para comparar nombres). */
function textoNormal(s) {
return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '')

View File

@ -84,6 +84,19 @@ assert.equal(post({funcion:'guardarCotizacion',args:[{...base,idSolicitud:'ok1'}
// --- Historial con el nombre del vendedor; filas viejas muestran el correo de "Elaboró" ---
const h=post({funcion:'listarCotizaciones',args:[''],token:'t-bueno'}).datos;
assert.equal(h[0].vendedor,'Ana López Díaz'); assert.equal(h[1].vendedor,'antes@helgen.tech');
// Filas viejas sin nombre: si su correo aparece con nombre en otra fila, se muestra ese nombre (sin tocar la hoja)
const vieja=['HLG-2026-0000','x','Vieja','','','','',1,0.16,1.16,'ANA.LOPEZ@helgen.tech','u','[]','','Helgen',''];
cot.m.splice(1,0,vieja.slice());
const h2=post({funcion:'listarCotizaciones',args:[''],token:'t-bueno'}).datos;
assert.equal(h2.find(c=>c.folio==='HLG-2026-0000').vendedor,'Ana López Díaz','correo conocido (sin importar mayúsculas) = nombre');
assert.equal(h2.find(c=>c.folio==='HLG-2026-0001').vendedor,'antes@helgen.tech','correo sin nombre conocido = correo');
assert.deepEqual(cot.m[1],vieja,'la hoja no se modifica');
cot.m.splice(1,1);
// Pura: si un correo tiene nombres distintos, gana la fila más reciente (la de más abajo)
const Q=(email,n,a)=>{const r=Array(19).fill('');r[16]=email;r[17]=n;r[18]=a;return r};
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');
// 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');