Agregar República Dominicana y ajustes finales del portal
This commit is contained in:
@@ -55,3 +55,8 @@ VITE_SUPABASE_ANON_KEY="TU_SUPABASE_ANON_KEY"
|
|||||||
# Usuario temporal solo si VITE_ENABLE_SUPABASE_AUTH="false".
|
# Usuario temporal solo si VITE_ENABLE_SUPABASE_AUTH="false".
|
||||||
VITE_OPERATOR_NAME="Isaac Aracena"
|
VITE_OPERATOR_NAME="Isaac Aracena"
|
||||||
VITE_OPERATOR_EMAIL="iaracena@gomezleemarketing.com"
|
VITE_OPERATOR_EMAIL="iaracena@gomezleemarketing.com"
|
||||||
|
|
||||||
|
# República Dominicana: cruce, históricos y resolución.
|
||||||
|
VITE_N8N_REPUBLICA_DOMINICANA_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominard-bamboo-test"
|
||||||
|
VITE_HISTORICOS_RD_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-rd-historicos"
|
||||||
|
VITE_MARCAR_RESUELTO_RD_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-rd-marcar-resuelto"
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# Cruce de Cuentas GLM — República Dominicana (RD)
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Dejar República Dominicana integrada al Portal de Cruce de Cuentas con una lógica reutilizable cada quincena, todos los meses y años, usando la misma arquitectura multi-país ya existente.
|
||||||
|
|
||||||
|
Configuración RD:
|
||||||
|
|
||||||
|
- Código: `RD`
|
||||||
|
- Moneda: `DOP`
|
||||||
|
- Locale: `es-DO`
|
||||||
|
- Zona horaria: `America/Santo_Domingo`
|
||||||
|
- RRHH destinataria de reportes: **Noralmy Laureano** — `nlaureano@gomezleemarketing.com`
|
||||||
|
- Bandera: `https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/Flag_of_the_Dominican_Republic.svg.webp`
|
||||||
|
|
||||||
|
## Archivos incluidos
|
||||||
|
|
||||||
|
### n8n
|
||||||
|
|
||||||
|
Importar desde `workflows_n8n/`:
|
||||||
|
|
||||||
|
1. `Portal de Verificación de Nómina - República Dominicana.json`
|
||||||
|
2. `Portal de Verificación de Nómina República Dominicana - Históricos.json`
|
||||||
|
3. `Portal de Verificación de Nómina República Dominicana - Envío de Reporte.json`
|
||||||
|
4. `Portal de Verificación de Nómina - Analizar Nómina con Gemini GT TT PA SV HN PR RD.json`
|
||||||
|
5. `Portal de Verificación de Nómina - Reportar corrección BambooHR a RRHH - GT TT PA SV HN PR RD.json`
|
||||||
|
|
||||||
|
Los workflows se entregan **inactivos** para que puedan importarse/configurarse sin disparar una ejecución accidental. Los dos workflows compartidos (Gemini y correcciones BambooHR) sustituyen las versiones equivalentes sin RD una vez validados.
|
||||||
|
|
||||||
|
### Supabase
|
||||||
|
|
||||||
|
Ejecutar:
|
||||||
|
|
||||||
|
`SUPABASE_Republica_Dominicana_READY.sql`
|
||||||
|
|
||||||
|
Este script habilita `RD` en las restricciones multi-país, accesos, alias BambooHR e históricos/RPC existentes. La tabla `cruces_cuentas_gt_reportes` conserva su nombre legado, pero funciona como tabla multi-país.
|
||||||
|
|
||||||
|
## Webhooks RD
|
||||||
|
|
||||||
|
- Cruce principal: `/webhook/nominard-bamboo-test`
|
||||||
|
- Históricos: `/webhook/cruce-cuentas-rd-historicos`
|
||||||
|
- Marcar resuelto: `/webhook/cruce-cuentas-rd-marcar-resuelto`
|
||||||
|
- Analizar nómina con Gemini: webhook compartido `/webhook/cruce-cuentas-analizar-nomina`
|
||||||
|
- Reportar corrección BambooHR: webhook compartido `/webhook/cruce-cuentas-bamboo-correccion`
|
||||||
|
|
||||||
|
La app ya contiene las variables `VITE_N8N_REPUBLICA_DOMINICANA_WEBHOOK_URL`, `VITE_HISTORICOS_RD_URL` y `VITE_MARCAR_RESUELTO_RD_URL`.
|
||||||
|
|
||||||
|
## Entrada esperada cada quincena
|
||||||
|
|
||||||
|
### Nómina
|
||||||
|
|
||||||
|
RD requiere **exactamente un Excel de nómina** (`.xlsx`, `.xls` o `.xlsm`) por período. El flujo valida que el archivo tenga filas de empleados cuyo campo `PERÍODO` corresponda al rango seleccionado en la app; si detecta empleados de otro período, se detiene para evitar cruzar una nómina equivocada.
|
||||||
|
|
||||||
|
Hojas operativas observadas y soportadas tras analizar las 16 nóminas de enero a agosto de 2026:
|
||||||
|
|
||||||
|
- `GLM RD `
|
||||||
|
- `Whirlpool`
|
||||||
|
- `Bice`
|
||||||
|
- `P&G`
|
||||||
|
- `KRAFT`
|
||||||
|
- `Motorola`
|
||||||
|
- `PMI`
|
||||||
|
- `Caribe`
|
||||||
|
- `Latam `
|
||||||
|
- `Purina`
|
||||||
|
- `Claro`
|
||||||
|
- `Essity`
|
||||||
|
|
||||||
|
Se excluyen hojas auxiliares como `Resumen`, `Validacion TSS`, `Validacion ISR`, `ISR`, `Calculo Horas extras`, `NVScriptsProperties`, AutoCrat, copias auxiliares como `Copia de Latam ` y hojas genéricas que no forman parte de la nómina operativa.
|
||||||
|
|
||||||
|
Campos utilizados: nombre, cuenta, `NETO A PAGAR`, `PERÍODO` y, cuando existen, cédula, referencia y correo.
|
||||||
|
|
||||||
|
### Banco
|
||||||
|
|
||||||
|
Se aceptan **uno o varios archivos CSV y/o PDF** de la misma quincena. Esto es indispensable en RD porque una quincena puede incluir lotes normales, temporales, suplementos y ACH separados.
|
||||||
|
|
||||||
|
El parser CSV soporta los formatos observados en 2026 y nombres de archivos como:
|
||||||
|
|
||||||
|
- BICE
|
||||||
|
- CLARO
|
||||||
|
- GLM
|
||||||
|
- Kraft
|
||||||
|
- LATAM
|
||||||
|
- Motorola
|
||||||
|
- P&G
|
||||||
|
- PMI
|
||||||
|
- Purina
|
||||||
|
- Temp Purina
|
||||||
|
- Temp Moto
|
||||||
|
- Whirlpool
|
||||||
|
- Essity / Temp Essity
|
||||||
|
- Bono BICE / Bono BICE inactivos
|
||||||
|
|
||||||
|
También admite sufijos de lote como `.1.csv` y normaliza el texto `N#U00f3minas` que aparece en algunos nombres exportados.
|
||||||
|
|
||||||
|
Los PDF BHD se interpretan como ACH complementarios. El flujo extrae beneficiario, cuenta, fecha efectiva, monto, estado, motivo, número de instrucción y descripción. La fecha de cada CSV/PDF se valida contra el período seleccionado.
|
||||||
|
|
||||||
|
## Reglas de conciliación RD
|
||||||
|
|
||||||
|
La lógica final está construida a partir de los cuatro reportes de entrenamiento de junio/julio 2026.
|
||||||
|
|
||||||
|
1. Se normalizan cuentas eliminando formato visual/ceros iniciales solo para comparación; el valor visible se conserva cuando corresponde.
|
||||||
|
2. La prioridad de match es **cuenta + monto**.
|
||||||
|
3. Cuando nómina no trae cuenta, se permite match por **nombre fuerte + monto**, evitando forzar coincidencias ambiguas.
|
||||||
|
4. Se usa tolerancia monetaria de **RD$0.05 por transacción**.
|
||||||
|
5. Un pago rechazado se conserva como hallazgo y puede explicar una discrepancia Nómina > Banco.
|
||||||
|
6. Antes de clasificar `Banco sin Nómina`, se hace una segunda búsqueda para detectar una **posible cuenta mal digitada** usando nombre/referencia/correo + monto.
|
||||||
|
7. Los suplementos `Bono BICE` se integran como pago esperado adicional para que formen parte del total de nómina de conciliación, igual que en el reporte patrón de 15-jun-2026.
|
||||||
|
8. `Banco sin Bamboo` es una revisión paralela y **no se suma a `Pendientes cruce principal`**, tal como muestran los reportes oficiales de entrenamiento.
|
||||||
|
9. Las diferencias de nombre bancario se mantienen como `Pendiente revisión`; no se fuerzan como identidad confirmada.
|
||||||
|
10. Diferencias agregadas insignificantes de centavos no generan una discrepancia si todas las transacciones individuales conciliaron dentro de tolerancia.
|
||||||
|
11. Las transacciones bancarias duplicadas exactas entre dos archivos/lotes provocan error para evitar doble contabilización.
|
||||||
|
|
||||||
|
## BambooHR
|
||||||
|
|
||||||
|
La consulta usa:
|
||||||
|
|
||||||
|
`/api/v1/reports/custom?format=JSON&onlyCurrent=false`
|
||||||
|
|
||||||
|
Esto permite validar también empleados históricos que pudieron participar en una quincena anterior. El filtro RD reconoce país/localización como `RD`, `República Dominicana`, `Dominican Republic` y variantes de localización dominicana.
|
||||||
|
|
||||||
|
El flujo usa Employee Number cuando está disponible y trabaja con alias de nombres. Las correcciones manuales confirmadas pueden persistirse en Supabase mediante el workflow compartido de correcciones.
|
||||||
|
|
||||||
|
## Reporte de salida
|
||||||
|
|
||||||
|
El Google Sheet conserva siempre las seis pestañas patrón de RD:
|
||||||
|
|
||||||
|
1. `01 Nómina vs Banco`
|
||||||
|
2. `02 Banco sin Nómina`
|
||||||
|
3. `03 Banco sin Bamboo`
|
||||||
|
4. `04 Diferencias nombre banco`
|
||||||
|
5. `05 Cuenta Mal Digitada`
|
||||||
|
6. `06 Resumen`
|
||||||
|
|
||||||
|
La pestaña 05 se crea incluso cuando no existen casos, para que el formato sea estable entre quincenas.
|
||||||
|
|
||||||
|
Las hojas se protegen para preservar datos calculados. La columna `Resolución` queda reservada para RRHH/administración autorizada; Noralmy Laureano está incluida entre las personas autorizadas para RD. El reporte también se comparte automáticamente con los accesos internos configurados.
|
||||||
|
|
||||||
|
## Correo RD
|
||||||
|
|
||||||
|
El correo automático del reporte generado se envía a:
|
||||||
|
|
||||||
|
`nlaureano@gomezleemarketing.com`
|
||||||
|
|
||||||
|
Utiliza branding GLM, bandera RD y muestra los indicadores relevantes del cruce. El workflow de `Marcar resuelto` también está adaptado a RD y utiliza la zona horaria de Santo Domingo.
|
||||||
|
|
||||||
|
## Históricos
|
||||||
|
|
||||||
|
Los históricos se guardan con `country = RD` y una identidad de período del tipo:
|
||||||
|
|
||||||
|
`RD-2026-06-quincena_15`
|
||||||
|
|
||||||
|
Si se vuelve a procesar el mismo período, la arquitectura reutiliza/actualiza el reporte existente y conserva resoluciones compatibles en lugar de crear duplicados innecesarios.
|
||||||
|
|
||||||
|
## Detección automática de período
|
||||||
|
|
||||||
|
Se actualizó el analizador compartido Gemini y el detector local de la app. Para RD se reconocen:
|
||||||
|
|
||||||
|
- día 15 → `quincena_15`
|
||||||
|
- día 30/31 → `quincena_30`
|
||||||
|
- último día real del mes → `quincena_30` (por ejemplo, 28/29 de febrero)
|
||||||
|
|
||||||
|
Se validaron correctamente los 16 nombres reales de nómina de enero a agosto de 2026.
|
||||||
|
|
||||||
|
## Orden de despliegue recomendado
|
||||||
|
|
||||||
|
1. Hacer respaldo de los workflows actualmente publicados.
|
||||||
|
2. Ejecutar `SUPABASE_Republica_Dominicana_READY.sql`.
|
||||||
|
3. Importar el workflow principal RD.
|
||||||
|
4. Importar Históricos RD.
|
||||||
|
5. Importar Envío de Reporte RD.
|
||||||
|
6. Reemplazar/actualizar el analizador compartido por la versión `... PR RD`.
|
||||||
|
7. Reemplazar/actualizar el workflow de correcciones BambooHR por la versión `... PR RD`.
|
||||||
|
8. Confirmar que las credenciales de BambooHR, Google Sheets/Drive y Gmail quedaron asociadas tras la importación.
|
||||||
|
9. Desplegar el frontend desde el código fuente actualizado (`npm ci && npm run build`).
|
||||||
|
10. Activar los cinco workflows una vez revisados los paths de webhook.
|
||||||
|
11. Ejecutar una prueba controlada con una de las cuatro quincenas conocidas y comparar con `VALIDACION_REPUBLICA_DOMINICANA_FINAL.md`.
|
||||||
|
12. Ejecutar después una quincena nueva/no entrenada antes de declararlo operación mensual definitiva.
|
||||||
|
|
||||||
|
## Nota de despliegue frontend
|
||||||
|
|
||||||
|
El `dist` heredado del ZIP original no debe usarse porque fue generado antes de incorporar RD. El entregable final de RD se prepara sin ese `dist` antiguo; el servidor de despliegue debe instalar dependencias y generar un `dist` nuevo desde el código fuente actualizado.
|
||||||
File diff suppressed because one or more lines are too long
+515
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
|
|||||||
|
# RD READY — archivos que debes usar
|
||||||
|
|
||||||
|
Para desplegar República Dominicana, consulta primero:
|
||||||
|
|
||||||
|
- `IMPLEMENTACION_REPUBLICA_DOMINICANA_READY.md`
|
||||||
|
- `VALIDACION_REPUBLICA_DOMINICANA_FINAL.md`
|
||||||
|
- `SUPABASE_Republica_Dominicana_READY.sql`
|
||||||
|
- carpeta `workflows_n8n/`
|
||||||
|
|
||||||
|
Workflows nuevos/actualizados a importar:
|
||||||
|
|
||||||
|
- `Portal de Verificación de Nómina - República Dominicana.json`
|
||||||
|
- `Portal de Verificación de Nómina República Dominicana - Históricos.json`
|
||||||
|
- `Portal de Verificación de Nómina República Dominicana - Envío de Reporte.json`
|
||||||
|
- `Portal de Verificación de Nómina - Analizar Nómina con Gemini GT TT PA SV HN PR RD.json`
|
||||||
|
- `Portal de Verificación de Nómina - Reportar corrección BambooHR a RRHH - GT TT PA SV HN PR RD.json`
|
||||||
|
|
||||||
|
RRHH RD: Noralmy Laureano — `nlaureano@gomezleemarketing.com`.
|
||||||
|
|
||||||
|
No desplegar un `dist` viejo. Generar uno nuevo desde este código fuente con `npm ci && npm run build` en el entorno de despliegue.
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- CRUCE DE CUENTAS GLM · HABILITAR REPÚBLICA DOMINICANA (RD)
|
||||||
|
-- GT + TT + PA + SV + HN + PR + RD
|
||||||
|
-- ============================================================
|
||||||
|
-- Ejecutar una sola vez en el SQL Editor del Supabase empresarial.
|
||||||
|
-- Es idempotente respecto a RD: conserva datos existentes y puede
|
||||||
|
-- volver a ejecutarse si se interrumpe antes del COMMIT.
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
-- 1) Validar tablas requeridas y ampliar constraints de país.
|
||||||
|
do $$
|
||||||
|
declare r record;
|
||||||
|
begin
|
||||||
|
if to_regclass('public.cruce_cuentas_bamboo_aliases') is null then raise exception 'No existe public.cruce_cuentas_bamboo_aliases.'; end if;
|
||||||
|
if to_regclass('public.cruce_cuentas_bamboo_correcciones') is null then raise exception 'No existe public.cruce_cuentas_bamboo_correcciones.'; end if;
|
||||||
|
if to_regclass('public.cruces_cuentas_gt_reportes') is null then raise exception 'No existe public.cruces_cuentas_gt_reportes.'; end if;
|
||||||
|
if to_regclass('public.cruce_cuentas_usuarios_autorizados') is null then raise exception 'No existe public.cruce_cuentas_usuarios_autorizados.'; end if;
|
||||||
|
|
||||||
|
-- Alias BambooHR.
|
||||||
|
for r in
|
||||||
|
select conname
|
||||||
|
from pg_constraint
|
||||||
|
where conrelid='public.cruce_cuentas_bamboo_aliases'::regclass
|
||||||
|
and contype='c'
|
||||||
|
and pg_get_constraintdef(oid) ilike '%pais%'
|
||||||
|
and pg_get_constraintdef(oid) ilike '%GT%'
|
||||||
|
and pg_get_constraintdef(oid) not ilike '%RD%'
|
||||||
|
loop
|
||||||
|
execute format('alter table public.cruce_cuentas_bamboo_aliases drop constraint %I',r.conname);
|
||||||
|
end loop;
|
||||||
|
alter table public.cruce_cuentas_bamboo_aliases drop constraint if exists ck_cruce_cuentas_bamboo_aliases_pais;
|
||||||
|
alter table public.cruce_cuentas_bamboo_aliases
|
||||||
|
add constraint ck_cruce_cuentas_bamboo_aliases_pais
|
||||||
|
check (upper(trim(pais)) in ('GT','TT','PA','SV','HN','PR','RD'));
|
||||||
|
|
||||||
|
-- Solicitudes/correcciones BambooHR.
|
||||||
|
for r in
|
||||||
|
select conname
|
||||||
|
from pg_constraint
|
||||||
|
where conrelid='public.cruce_cuentas_bamboo_correcciones'::regclass
|
||||||
|
and contype='c'
|
||||||
|
and pg_get_constraintdef(oid) ilike '%pais%'
|
||||||
|
and pg_get_constraintdef(oid) ilike '%GT%'
|
||||||
|
and pg_get_constraintdef(oid) not ilike '%RD%'
|
||||||
|
loop
|
||||||
|
execute format('alter table public.cruce_cuentas_bamboo_correcciones drop constraint %I',r.conname);
|
||||||
|
end loop;
|
||||||
|
alter table public.cruce_cuentas_bamboo_correcciones drop constraint if exists ck_cruce_cuentas_bamboo_correcciones_pais;
|
||||||
|
alter table public.cruce_cuentas_bamboo_correcciones
|
||||||
|
add constraint ck_cruce_cuentas_bamboo_correcciones_pais
|
||||||
|
check (upper(trim(pais)) in ('GT','TT','PA','SV','HN','PR','RD'));
|
||||||
|
|
||||||
|
-- Históricos multi-país (el nombre de la tabla es legado).
|
||||||
|
for r in
|
||||||
|
select conname
|
||||||
|
from pg_constraint
|
||||||
|
where conrelid='public.cruces_cuentas_gt_reportes'::regclass
|
||||||
|
and contype='c'
|
||||||
|
and pg_get_constraintdef(oid) ilike '%country%'
|
||||||
|
and pg_get_constraintdef(oid) ilike '%GT%'
|
||||||
|
and pg_get_constraintdef(oid) not ilike '%RD%'
|
||||||
|
loop
|
||||||
|
execute format('alter table public.cruces_cuentas_gt_reportes drop constraint %I',r.conname);
|
||||||
|
end loop;
|
||||||
|
alter table public.cruces_cuentas_gt_reportes drop constraint if exists ck_cruces_cuentas_reportes_country;
|
||||||
|
alter table public.cruces_cuentas_gt_reportes
|
||||||
|
add constraint ck_cruces_cuentas_reportes_country
|
||||||
|
check (country is null or upper(trim(country)) in ('GT','TT','PA','SV','HN','PR','RD'));
|
||||||
|
|
||||||
|
-- Accesos: text[] de países habilitados.
|
||||||
|
alter table public.cruce_cuentas_usuarios_autorizados drop constraint if exists cruce_cuentas_paises_validos_check;
|
||||||
|
alter table public.cruce_cuentas_usuarios_autorizados
|
||||||
|
add constraint cruce_cuentas_paises_validos_check
|
||||||
|
check (paises is null or paises <@ array['GT','TT','PA','SV','HN','PR','RD']::text[]);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- 2) Dar RD a todos los usuarios actualmente autorizados, preservando permisos.
|
||||||
|
update public.cruce_cuentas_usuarios_autorizados
|
||||||
|
set paises = case
|
||||||
|
when paises is null then array['GT','TT','PA','SV','HN','PR','RD']::text[]
|
||||||
|
when not ('RD'=any(paises)) then array_append(paises,'RD')
|
||||||
|
else paises
|
||||||
|
end,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
-- 3) Alias BambooHR activos: habilitar RD en la función compartida.
|
||||||
|
create or replace function public.cruce_cuentas_bamboo_aliases_activos(p_pais text)
|
||||||
|
returns jsonb
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path=public
|
||||||
|
as $$
|
||||||
|
select jsonb_build_object(
|
||||||
|
'ok',true,
|
||||||
|
'pais',upper(trim(coalesce(p_pais,''))),
|
||||||
|
'aliases',coalesce(
|
||||||
|
jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'id',a.id,
|
||||||
|
'pais',a.pais,
|
||||||
|
'nombre_origen',a.nombre_origen,
|
||||||
|
'nombre_origen_normalizado',a.nombre_origen_normalizado,
|
||||||
|
'cuenta_bancaria',a.cuenta_bancaria,
|
||||||
|
'bamboo_employee_number',a.bamboo_employee_number,
|
||||||
|
'bamboo_employee_id',a.bamboo_employee_id,
|
||||||
|
'bamboo_nombre',a.bamboo_nombre,
|
||||||
|
'motivo',a.motivo,
|
||||||
|
'activo',a.activo
|
||||||
|
) order by a.updated_at desc
|
||||||
|
) filter (where a.id is not null),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
)
|
||||||
|
from public.cruce_cuentas_bamboo_aliases a
|
||||||
|
where a.activo=true
|
||||||
|
and a.pais=upper(trim(coalesce(p_pais,'')))
|
||||||
|
and upper(trim(coalesce(p_pais,''))) in ('GT','TT','PA','SV','HN','PR','RD');
|
||||||
|
$$;
|
||||||
|
revoke all on function public.cruce_cuentas_bamboo_aliases_activos(text) from public,anon,authenticated;
|
||||||
|
grant execute on function public.cruce_cuentas_bamboo_aliases_activos(text) to service_role;
|
||||||
|
|
||||||
|
-- 4) Históricos paginados multi-país, incluyendo RD.
|
||||||
|
create index if not exists idx_cruces_cuentas_reportes_country_created
|
||||||
|
on public.cruces_cuentas_gt_reportes(country,created_at desc);
|
||||||
|
create index if not exists idx_cruces_cuentas_reportes_country_status_created
|
||||||
|
on public.cruces_cuentas_gt_reportes(country,estado,created_at desc);
|
||||||
|
|
||||||
|
create or replace function public.cruce_cuentas_get_historicos(
|
||||||
|
p_country text,
|
||||||
|
p_page integer default 1,
|
||||||
|
p_page_size integer default 10,
|
||||||
|
p_search text default null,
|
||||||
|
p_status text default null
|
||||||
|
)
|
||||||
|
returns jsonb
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path=public
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_country text := upper(trim(coalesce(p_country,'')));
|
||||||
|
v_page integer := greatest(coalesce(p_page,1),1);
|
||||||
|
v_page_size integer := least(greatest(coalesce(p_page_size,10),1),100);
|
||||||
|
v_offset integer;
|
||||||
|
v_search text := nullif(trim(coalesce(p_search,'')),'');
|
||||||
|
v_status text := nullif(trim(coalesce(p_status,'')),'');
|
||||||
|
v_total bigint;
|
||||||
|
v_filtered_total bigint;
|
||||||
|
v_reports jsonb;
|
||||||
|
begin
|
||||||
|
if v_country not in ('GT','TT','PA','SV','HN','PR','RD') then
|
||||||
|
raise exception 'País no permitido: %',v_country;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
v_offset := (v_page-1)*v_page_size;
|
||||||
|
|
||||||
|
select count(*) into v_total
|
||||||
|
from public.cruces_cuentas_gt_reportes r
|
||||||
|
where upper(coalesce(r.country,'GT'))=v_country;
|
||||||
|
|
||||||
|
select count(*) into v_filtered_total
|
||||||
|
from public.cruces_cuentas_gt_reportes r
|
||||||
|
where upper(coalesce(r.country,'GT'))=v_country
|
||||||
|
and (v_status is null or r.estado=v_status)
|
||||||
|
and (
|
||||||
|
v_search is null
|
||||||
|
or coalesce(r.period_label,'') ilike '%'||v_search||'%'
|
||||||
|
or coalesce(r.payroll_file_name,'') ilike '%'||v_search||'%'
|
||||||
|
or coalesce(r.ejecutado_por_nombre,'') ilike '%'||v_search||'%'
|
||||||
|
or coalesce(r.resuelto_por_nombre,'') ilike '%'||v_search||'%'
|
||||||
|
or r.id::text ilike '%'||v_search||'%'
|
||||||
|
);
|
||||||
|
|
||||||
|
select coalesce(
|
||||||
|
jsonb_agg((to_jsonb(q)-'detalle_banco_sin_bamboo') order by q.created_at desc),
|
||||||
|
'[]'::jsonb
|
||||||
|
) into v_reports
|
||||||
|
from (
|
||||||
|
select r.*
|
||||||
|
from public.cruces_cuentas_gt_reportes r
|
||||||
|
where upper(coalesce(r.country,'GT'))=v_country
|
||||||
|
and (v_status is null or r.estado=v_status)
|
||||||
|
and (
|
||||||
|
v_search is null
|
||||||
|
or coalesce(r.period_label,'') ilike '%'||v_search||'%'
|
||||||
|
or coalesce(r.payroll_file_name,'') ilike '%'||v_search||'%'
|
||||||
|
or coalesce(r.ejecutado_por_nombre,'') ilike '%'||v_search||'%'
|
||||||
|
or coalesce(r.resuelto_por_nombre,'') ilike '%'||v_search||'%'
|
||||||
|
or r.id::text ilike '%'||v_search||'%'
|
||||||
|
)
|
||||||
|
order by r.created_at desc
|
||||||
|
limit v_page_size offset v_offset
|
||||||
|
) q;
|
||||||
|
|
||||||
|
return jsonb_build_object(
|
||||||
|
'ok',true,
|
||||||
|
'country',v_country,
|
||||||
|
'page',v_page,
|
||||||
|
'page_size',v_page_size,
|
||||||
|
'total',v_total,
|
||||||
|
'filtered_total',v_filtered_total,
|
||||||
|
'total_pages',greatest(ceil(v_filtered_total::numeric/v_page_size)::integer,1),
|
||||||
|
'reports',v_reports
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
revoke all on function public.cruce_cuentas_get_historicos(text,integer,integer,text,text) from public,anon;
|
||||||
|
grant execute on function public.cruce_cuentas_get_historicos(text,integer,integer,text,text) to authenticated;
|
||||||
|
|
||||||
|
-- 5) Detalle Banco sin BambooHR de históricos, incluyendo RD.
|
||||||
|
create or replace function public.cruce_cuentas_get_banco_sin_bamboo_reporte(
|
||||||
|
p_report_id text,
|
||||||
|
p_country text
|
||||||
|
)
|
||||||
|
returns jsonb
|
||||||
|
language plpgsql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path=public
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_country text := upper(trim(coalesce(p_country,'')));
|
||||||
|
v_report public.cruces_cuentas_gt_reportes%rowtype;
|
||||||
|
v_corrections jsonb := '[]'::jsonb;
|
||||||
|
begin
|
||||||
|
if auth.uid() is null then raise exception 'No autenticado.'; end if;
|
||||||
|
if v_country not in ('GT','TT','PA','SV','HN','PR','RD') then raise exception 'País no permitido: %',v_country; end if;
|
||||||
|
|
||||||
|
select r.* into v_report
|
||||||
|
from public.cruces_cuentas_gt_reportes r
|
||||||
|
where r.id::text=trim(coalesce(p_report_id,''))
|
||||||
|
and upper(coalesce(r.country,'GT'))=v_country
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
if not found then
|
||||||
|
return jsonb_build_object('ok',false,'message','No se encontró el reporte solicitado.');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select coalesce(
|
||||||
|
jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'id',c.id,
|
||||||
|
'pais',c.pais,
|
||||||
|
'nombre_origen_principal',c.nombre_origen_principal,
|
||||||
|
'nombres_origen',c.nombres_origen,
|
||||||
|
'cuenta_bancaria',c.cuenta_bancaria,
|
||||||
|
'bamboo_employee_number',c.bamboo_employee_number,
|
||||||
|
'bamboo_nombre',c.bamboo_nombre,
|
||||||
|
'estado',c.estado,
|
||||||
|
'correo_enviado',c.correo_enviado,
|
||||||
|
'correo_enviado_at',c.correo_enviado_at,
|
||||||
|
'created_at',c.created_at
|
||||||
|
) order by c.created_at desc
|
||||||
|
),
|
||||||
|
'[]'::jsonb
|
||||||
|
) into v_corrections
|
||||||
|
from public.cruce_cuentas_bamboo_correcciones c
|
||||||
|
where c.pais=v_country
|
||||||
|
and (
|
||||||
|
c.execution_id=v_report.id::text
|
||||||
|
or (
|
||||||
|
nullif(trim(coalesce(c.execution_id,'')),'') is null
|
||||||
|
and nullif(trim(coalesce(c.report_url,'')),'') is not null
|
||||||
|
and c.report_url=v_report.report_url
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return jsonb_build_object(
|
||||||
|
'ok',true,
|
||||||
|
'report',jsonb_build_object(
|
||||||
|
'id',v_report.id,
|
||||||
|
'country',v_report.country,
|
||||||
|
'period_label',v_report.period_label,
|
||||||
|
'period_start',v_report.period_start,
|
||||||
|
'period_end',v_report.period_end,
|
||||||
|
'report_url',v_report.report_url,
|
||||||
|
'banco_sin_bamboo',v_report.banco_sin_bamboo
|
||||||
|
),
|
||||||
|
'cases',coalesce(v_report.detalle_banco_sin_bamboo,'[]'::jsonb),
|
||||||
|
'corrections',v_corrections
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
revoke all on function public.cruce_cuentas_get_banco_sin_bamboo_reporte(text,text) from public,anon;
|
||||||
|
grant execute on function public.cruce_cuentas_get_banco_sin_bamboo_reporte(text,text) to authenticated;
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- VALIDACIÓN (solo lectura)
|
||||||
|
-- ============================================================
|
||||||
|
select email,nombre,paises
|
||||||
|
from public.cruce_cuentas_usuarios_autorizados
|
||||||
|
order by email;
|
||||||
|
|
||||||
|
select conname, pg_get_constraintdef(oid) as definicion
|
||||||
|
from pg_constraint
|
||||||
|
where conrelid in (
|
||||||
|
'public.cruce_cuentas_bamboo_aliases'::regclass,
|
||||||
|
'public.cruce_cuentas_bamboo_correcciones'::regclass,
|
||||||
|
'public.cruces_cuentas_gt_reportes'::regclass,
|
||||||
|
'public.cruce_cuentas_usuarios_autorizados'::regclass
|
||||||
|
)
|
||||||
|
and contype='c'
|
||||||
|
and (pg_get_constraintdef(oid) ilike '%pais%' or pg_get_constraintdef(oid) ilike '%country%')
|
||||||
|
order by conname;
|
||||||
|
|
||||||
|
select public.cruce_cuentas_get_historicos('RD',1,10,null,null) as historicos_rd;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Puerto Rico — validación final del cruce
|
||||||
|
|
||||||
|
Corrección aplicada al workflow principal `Portal de Verificación de Nómina - Puerto Rico`:
|
||||||
|
|
||||||
|
- La lógica de conciliación, montos, cuentas y categorías no fue modificada.
|
||||||
|
- Las filas devueltas a la app ahora reutilizan la resolución de identidad contra BambooHR para completar `employeeNumber` / `employee_number` cuando la nómina no lo trae.
|
||||||
|
- Se aplica a coincidencias, discrepancias, posibles cuentas mal digitadas, nómina sin pago bancario y banco sin nómina cuando existe una identificación BambooHR confiable.
|
||||||
|
- `Banco sin BambooHR` conserva su semántica: no se inventa Employee Number cuando la identidad no pudo validarse.
|
||||||
|
- La fila visual vacía previa a los encabezados y las celdas vacías de `Resolución` permanecen vacías. El valor interno `17` observado al inspeccionar el XLSX era el índice de la cadena compartida vacía (`sharedStrings[17] = ""`), no contenido visible del reporte.
|
||||||
|
|
||||||
|
Validaciones estáticas realizadas:
|
||||||
|
|
||||||
|
- JSON válido.
|
||||||
|
- 37 nodos y 30 grupos de conexiones conservados.
|
||||||
|
- Sin referencias a nodos inexistentes.
|
||||||
|
- 13 Code Nodes analizados sintácticamente sin errores.
|
||||||
|
- Endpoint principal conservado: `nominapr-bamboo-test`.
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"generated_at": "2026-09-12",
|
||||||
|
"country": "RD",
|
||||||
|
"country_name": "República Dominicana",
|
||||||
|
"currency": "DOP",
|
||||||
|
"training_references": {
|
||||||
|
"Junio 15": {
|
||||||
|
"matches": 257,
|
||||||
|
"discrepancies": 1,
|
||||||
|
"bank_without_payroll": 0,
|
||||||
|
"bank_without_bamboo": 7,
|
||||||
|
"payroll_without_account": 0,
|
||||||
|
"name_differences": 2,
|
||||||
|
"wrong_accounts": 0,
|
||||||
|
"pending": 3,
|
||||||
|
"payroll_total": 4665382.94,
|
||||||
|
"bank_total": 4644692.08,
|
||||||
|
"difference": 20690.86
|
||||||
|
},
|
||||||
|
"Junio 30": {
|
||||||
|
"matches": 213,
|
||||||
|
"discrepancies": 1,
|
||||||
|
"bank_without_payroll": 0,
|
||||||
|
"bank_without_bamboo": 8,
|
||||||
|
"payroll_without_account": 0,
|
||||||
|
"name_differences": 2,
|
||||||
|
"wrong_accounts": 0,
|
||||||
|
"pending": 3,
|
||||||
|
"payroll_total": 4148894.42,
|
||||||
|
"bank_total": 4134786.57,
|
||||||
|
"difference": 14107.85
|
||||||
|
},
|
||||||
|
"Julio 15": {
|
||||||
|
"matches": 213,
|
||||||
|
"discrepancies": 0,
|
||||||
|
"bank_without_payroll": 0,
|
||||||
|
"bank_without_bamboo": 8,
|
||||||
|
"payroll_without_account": 0,
|
||||||
|
"name_differences": 2,
|
||||||
|
"wrong_accounts": 0,
|
||||||
|
"pending": 2,
|
||||||
|
"payroll_total": 4008539.03,
|
||||||
|
"bank_total": 4008539.04,
|
||||||
|
"difference": -0.01
|
||||||
|
},
|
||||||
|
"Julio 30": {
|
||||||
|
"matches": 262,
|
||||||
|
"discrepancies": 0,
|
||||||
|
"bank_without_payroll": 0,
|
||||||
|
"bank_without_bamboo": 8,
|
||||||
|
"payroll_without_account": 0,
|
||||||
|
"name_differences": 2,
|
||||||
|
"wrong_accounts": 0,
|
||||||
|
"pending": 2,
|
||||||
|
"payroll_total": 4431266.35,
|
||||||
|
"bank_total": 4431266.42,
|
||||||
|
"difference": -0.07
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"offline_regression": [
|
||||||
|
{
|
||||||
|
"period": "Junio 15",
|
||||||
|
"payroll_rows": 210,
|
||||||
|
"supplement_rows": 48,
|
||||||
|
"all_bank_rows": 258,
|
||||||
|
"processed_bank_rows": 257,
|
||||||
|
"rejected_bank_rows": 1,
|
||||||
|
"matches": 257,
|
||||||
|
"coincidencias": 257,
|
||||||
|
"discrepancias": 1,
|
||||||
|
"banco_sin_nomina": 0,
|
||||||
|
"nomina_sin_cuenta": 0,
|
||||||
|
"possible_wrong_accounts": 0,
|
||||||
|
"name_differences": 2,
|
||||||
|
"total_nomina": 4665382.94,
|
||||||
|
"total_banco": 4644692.08,
|
||||||
|
"diferencia": 20690.86,
|
||||||
|
"discrepancy_people": [
|
||||||
|
"Angel Leonardo Baez"
|
||||||
|
],
|
||||||
|
"unused_bank": [],
|
||||||
|
"wrong": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period": "Junio 30",
|
||||||
|
"payroll_rows": 214,
|
||||||
|
"supplement_rows": 0,
|
||||||
|
"all_bank_rows": 213,
|
||||||
|
"processed_bank_rows": 213,
|
||||||
|
"rejected_bank_rows": 0,
|
||||||
|
"matches": 213,
|
||||||
|
"coincidencias": 213,
|
||||||
|
"discrepancias": 1,
|
||||||
|
"banco_sin_nomina": 0,
|
||||||
|
"nomina_sin_cuenta": 0,
|
||||||
|
"possible_wrong_accounts": 0,
|
||||||
|
"name_differences": 2,
|
||||||
|
"total_nomina": 4148894.42,
|
||||||
|
"total_banco": 4134786.57,
|
||||||
|
"diferencia": 14107.85,
|
||||||
|
"discrepancy_people": [
|
||||||
|
"Edgar Jimenez De Los Santos"
|
||||||
|
],
|
||||||
|
"unused_bank": [],
|
||||||
|
"wrong": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period": "Julio 15",
|
||||||
|
"payroll_rows": 213,
|
||||||
|
"supplement_rows": 0,
|
||||||
|
"all_bank_rows": 213,
|
||||||
|
"processed_bank_rows": 213,
|
||||||
|
"rejected_bank_rows": 0,
|
||||||
|
"matches": 213,
|
||||||
|
"coincidencias": 213,
|
||||||
|
"discrepancias": 0,
|
||||||
|
"banco_sin_nomina": 0,
|
||||||
|
"nomina_sin_cuenta": 0,
|
||||||
|
"possible_wrong_accounts": 0,
|
||||||
|
"name_differences": 2,
|
||||||
|
"total_nomina": 4008539.04,
|
||||||
|
"total_banco": 4008539.04,
|
||||||
|
"diferencia": 0.0,
|
||||||
|
"discrepancy_people": [],
|
||||||
|
"unused_bank": [],
|
||||||
|
"wrong": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period": "Julio 30",
|
||||||
|
"payroll_rows": 262,
|
||||||
|
"supplement_rows": 0,
|
||||||
|
"all_bank_rows": 262,
|
||||||
|
"processed_bank_rows": 262,
|
||||||
|
"rejected_bank_rows": 0,
|
||||||
|
"matches": 262,
|
||||||
|
"coincidencias": 262,
|
||||||
|
"discrepancias": 0,
|
||||||
|
"banco_sin_nomina": 0,
|
||||||
|
"nomina_sin_cuenta": 0,
|
||||||
|
"possible_wrong_accounts": 0,
|
||||||
|
"name_differences": 2,
|
||||||
|
"total_nomina": 4431266.42,
|
||||||
|
"total_banco": 4431266.42,
|
||||||
|
"diferencia": 0.0,
|
||||||
|
"discrepancy_people": [],
|
||||||
|
"unused_bank": [],
|
||||||
|
"wrong": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"acceptance": {
|
||||||
|
"core_row_classification": "PASS for all 4 trained periods",
|
||||||
|
"name_difference_count": "PASS: 2 for all 4 trained periods",
|
||||||
|
"bank_without_payroll": "PASS: 0 for all 4 trained periods",
|
||||||
|
"wrong_account_false_positives": "PASS: 0 for all 4 trained periods",
|
||||||
|
"june_totals": "PASS exact",
|
||||||
|
"july_aggregate_rounding": "PASS functional: zero row discrepancies; workflow uses deterministic currency rounding and therefore normalizes the historical -0.01/-0.07 aggregate artifacts to 0.00",
|
||||||
|
"bamboo_live_counts": "NOT EXECUTED OFFLINE; requires n8n live BambooHR credential. Workflow uses onlyCurrent=false and RD country/location filter."
|
||||||
|
},
|
||||||
|
"payroll_filename_detection": {
|
||||||
|
"tested_files": 16,
|
||||||
|
"failures": 0,
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Abril 15 2026_20260401.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 4,
|
||||||
|
"period_type": "quincena_15",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Abril 30 2026_20260418.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 4,
|
||||||
|
"period_type": "quincena_30",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Agosto 15 2026_20260730.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 8,
|
||||||
|
"period_type": "quincena_15",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Agosto 30 2026_20260818.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 8,
|
||||||
|
"period_type": "quincena_30",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Enero 15 2026_20260108.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 1,
|
||||||
|
"period_type": "quincena_15",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Enero 30 2026_20260119.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 1,
|
||||||
|
"period_type": "quincena_30",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Febrero 15 2026_20260202.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 2,
|
||||||
|
"period_type": "quincena_15",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Febrero 28 2026_202602018.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 2,
|
||||||
|
"period_type": "quincena_30",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Julio 15 2026_20260706.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 7,
|
||||||
|
"period_type": "quincena_15",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Julio 30 2026_20260716.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 7,
|
||||||
|
"period_type": "quincena_30",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Junio 15 2026_20260602.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 6,
|
||||||
|
"period_type": "quincena_15",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Junio 30 2026_20260616.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 6,
|
||||||
|
"period_type": "quincena_30",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Marzo 15 2026_20260303.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 3,
|
||||||
|
"period_type": "quincena_15",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Marzo 30 2026_20260318.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 3,
|
||||||
|
"period_type": "quincena_30",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Mayo 15 2026_20260507.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5,
|
||||||
|
"period_type": "quincena_15",
|
||||||
|
"pass": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "GLM_Nomina Field Force Claro, Whirpool, Purina & Latam RD Mayo 30 2026_20260519.xlsx",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5,
|
||||||
|
"period_type": "quincena_30",
|
||||||
|
"pass": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"static_validation": {
|
||||||
|
"n8n_workflow_json_and_connections": "PASS",
|
||||||
|
"n8n_code_node_js_syntax": "PASS",
|
||||||
|
"app_ts_tsx_syntax": "PASS",
|
||||||
|
"full_vite_build": "NOT EXECUTED in this container because project dependencies/node_modules were not available; deployment environment must run npm ci && npm run build"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Validación final — República Dominicana
|
||||||
|
|
||||||
|
Fecha de revisión: 12-09-2026.
|
||||||
|
|
||||||
|
## Material analizado
|
||||||
|
|
||||||
|
Se analizaron de forma conjunta:
|
||||||
|
|
||||||
|
- las **16 nóminas RD** disponibles de enero a agosto de 2026 (Q15 y cierre de mes);
|
||||||
|
- los **62 archivos de pagos** disponibles para las cuatro quincenas de entrenamiento de junio/julio 2026;
|
||||||
|
- los **4 Excel oficiales de referencia** generados durante el entrenamiento;
|
||||||
|
- la arquitectura actual de la app multi-país;
|
||||||
|
- los workflows de Puerto Rico y los workflows compartidos de Gemini/corrección BambooHR utilizados como base estructural.
|
||||||
|
|
||||||
|
## Reportes patrón
|
||||||
|
|
||||||
|
| Período | Coincidencias | Discrepancias | Banco sin nómina | Banco sin Bamboo | Dif. nombre | Cuenta mal digitada | Pendientes principales | Total nómina ref. | Total banco ref. | Diferencia ref. |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| 15-jun-2026 | 257 | 1 | 0 | 7 | 2 | 0 | 3 | RD$4,665,382.94 | RD$4,644,692.08 | RD$20,690.86 |
|
||||||
|
| 30-jun-2026 | 213 | 1 | 0 | 8 | 2 | 0 | 3 | RD$4,148,894.42 | RD$4,134,786.57 | RD$14,107.85 |
|
||||||
|
| 15-jul-2026 | 213 | 0 | 0 | 8 | 2 | 0 | 2 | RD$4,008,539.03 | RD$4,008,539.04 | -RD$0.01 |
|
||||||
|
| 30-jul-2026 | 262 | 0 | 0 | 8 | 2 | 0 | 2 | RD$4,431,266.35 | RD$4,431,266.42 | -RD$0.07 |
|
||||||
|
|
||||||
|
## Regresión independiente sobre archivos originales
|
||||||
|
|
||||||
|
La lógica RD fue re-ejecutada offline contra los archivos de nómina/pago disponibles, sin utilizar como entrada los totales de los reportes de referencia.
|
||||||
|
|
||||||
|
### 15-jun-2026
|
||||||
|
|
||||||
|
Resultado reconstruido:
|
||||||
|
|
||||||
|
- 210 filas válidas de nómina.
|
||||||
|
- 48 suplementos `Bono BICE`.
|
||||||
|
- 258 movimientos bancarios totales, de los cuales 257 fueron procesados y 1 rechazado.
|
||||||
|
- 257 coincidencias.
|
||||||
|
- 1 discrepancia.
|
||||||
|
- 0 Banco sin nómina.
|
||||||
|
- 0 Nómina sin cuenta sin resolver.
|
||||||
|
- 0 posibles cuentas mal digitadas.
|
||||||
|
- 2 diferencias de nombre bancario.
|
||||||
|
- Total nómina: RD$4,665,382.94.
|
||||||
|
- Total banco procesado: RD$4,644,692.08.
|
||||||
|
- Diferencia: RD$20,690.86.
|
||||||
|
|
||||||
|
La discrepancia reconstruida corresponde a **Angel Leonardo Baez** y reproduce el caso de pago rechazado por banco.
|
||||||
|
|
||||||
|
### 30-jun-2026
|
||||||
|
|
||||||
|
Resultado reconstruido:
|
||||||
|
|
||||||
|
- 214 filas válidas de nómina.
|
||||||
|
- 213 movimientos bancarios procesados.
|
||||||
|
- 213 coincidencias.
|
||||||
|
- 1 discrepancia.
|
||||||
|
- 0 Banco sin nómina.
|
||||||
|
- 0 Nómina sin cuenta sin resolver.
|
||||||
|
- 0 posibles cuentas mal digitadas.
|
||||||
|
- 2 diferencias de nombre bancario.
|
||||||
|
- Total nómina: RD$4,148,894.42.
|
||||||
|
- Total banco: RD$4,134,786.57.
|
||||||
|
- Diferencia: RD$14,107.85.
|
||||||
|
|
||||||
|
La discrepancia reconstruida corresponde a **Edgar Jimenez De Los Santos**. El ACH adicional de **Jose Luis Ventura** por RD$22,661.00 se incorpora correctamente al conjunto bancario.
|
||||||
|
|
||||||
|
### 15-jul-2026
|
||||||
|
|
||||||
|
Resultado reconstruido:
|
||||||
|
|
||||||
|
- 213 filas válidas.
|
||||||
|
- 213 movimientos bancarios.
|
||||||
|
- 213 coincidencias.
|
||||||
|
- 0 discrepancias.
|
||||||
|
- 0 Banco sin nómina.
|
||||||
|
- 0 Nómina sin cuenta sin resolver.
|
||||||
|
- 0 posibles cuentas mal digitadas.
|
||||||
|
- 2 diferencias de nombre.
|
||||||
|
|
||||||
|
La conciliación transacción por transacción queda completa. Con redondeo monetario determinista por fila, Nómina y Banco quedan en RD$4,008,539.04. El reporte histórico mostraba RD$4,008,539.03 / diferencia -RD$0.01. Esa diferencia histórica es un artefacto agregado de redondeo y **no debe convertirse en discrepancia**, exactamente como se decidió durante el entrenamiento.
|
||||||
|
|
||||||
|
### 30-jul-2026
|
||||||
|
|
||||||
|
Resultado reconstruido:
|
||||||
|
|
||||||
|
- 262 filas válidas.
|
||||||
|
- 262 movimientos bancarios.
|
||||||
|
- 262 coincidencias.
|
||||||
|
- 0 discrepancias.
|
||||||
|
- 0 Banco sin nómina.
|
||||||
|
- 0 Nómina sin cuenta sin resolver.
|
||||||
|
- 0 posibles cuentas mal digitadas.
|
||||||
|
- 2 diferencias de nombre.
|
||||||
|
|
||||||
|
La conciliación por transacción queda completa. El flujo redondea de forma consistente y obtiene RD$4,431,266.42 tanto en Nómina como en Banco. El reporte histórico mostraba una diferencia agregada de -RD$0.07. Nuevamente, no existe una discrepancia real por empleado y el flujo no genera un falso hallazgo.
|
||||||
|
|
||||||
|
## Diferencias de nombre bancario
|
||||||
|
|
||||||
|
La regresión devuelve exactamente los **2 casos recurrentes** que sirvieron de referencia, sin agregar el falso positivo de Waldo Antonio Gonzales/Gonzalez:
|
||||||
|
|
||||||
|
- `GOMEZ PERERA JOSE LEOPOLDO F` frente a Yelida Gómez Lee.
|
||||||
|
- `JAILYN CHAU` frente a Elizabeth Carrasco.
|
||||||
|
|
||||||
|
La lógica fuzzy se ajustó para reconocer variaciones ortográficas cercanas/reordenamiento de tokens sin convertir coincidencias reales en “Diferencia nombre banco”.
|
||||||
|
|
||||||
|
## Nóminas enero-agosto 2026
|
||||||
|
|
||||||
|
Se confirmaron 16 archivos reales, uno por cada quincena disponible. El detector local de la app identifica correctamente los 16 períodos, incluyendo **28-feb-2026 como segunda quincena/cierre de mes**.
|
||||||
|
|
||||||
|
El flujo no depende de que el calendario termine en día 30; el último día real de febrero también se interpreta como `quincena_30`.
|
||||||
|
|
||||||
|
La nómina de 30-may-2026 tiene un total bruto notablemente superior al resto. No se modificó ni se “corrigió” automáticamente porque no existen pagos de mayo en el material recibido para validar si corresponde a pagos extraordinarios. El workflow procesará el archivo por sus filas reales y requerirá los archivos bancarios de esa misma quincena para conciliarlo.
|
||||||
|
|
||||||
|
## Pagos analizados
|
||||||
|
|
||||||
|
Los 62 archivos recibidos corresponden a junio/julio 2026 e incluyen CSV regulares, lotes temporales, suplementos y siete PDFs BHD. La lógica contempla que una quincena tenga múltiples lotes y archivos con sufijo `.1.csv`.
|
||||||
|
|
||||||
|
Los ACH PDF detectados incluyen pagos de Jose Luis Ventura, Reimon Perez y Jenny Salazar. La clasificación de origen del PDF se usa como información descriptiva; el match contable se sostiene en cuenta/monto/nombre, por lo que una descripción bancaria inconsistente no invalida un pago real.
|
||||||
|
|
||||||
|
## Validaciones técnicas realizadas
|
||||||
|
|
||||||
|
- Los 5 workflows RD/compartidos son JSON válido.
|
||||||
|
- No existen conexiones n8n hacia nodos inexistentes.
|
||||||
|
- Todos los Code nodes pasan validación sintáctica JavaScript.
|
||||||
|
- Los archivos `.ts/.tsx` modificados pasan transpilación sintáctica TypeScript.
|
||||||
|
- El workflow principal RD no conserva referencias operativas a Puerto Rico, USD, `es-PR` ni `America/Puerto_Rico`.
|
||||||
|
- Moneda RD: `DOP`.
|
||||||
|
- Locale: `es-DO`.
|
||||||
|
- Zona horaria: `America/Santo_Domingo`.
|
||||||
|
- BambooHR: `onlyCurrent=false`.
|
||||||
|
- Destinataria RRHH: `nlaureano@gomezleemarketing.com`.
|
||||||
|
- Bandera: URL suministrada para República Dominicana.
|
||||||
|
- Las 6 pestañas del reporte quedan siempre definidas.
|
||||||
|
- La protección de `Resolución` incluye a Noralmy para RD.
|
||||||
|
|
||||||
|
## Límite de la validación local
|
||||||
|
|
||||||
|
No se ejecutó una prueba **live** contra las credenciales reales de n8n/BambooHR/Google Workspace/Supabase desde este entorno. Por esa razón, aunque el paquete está preparado para importar y la lógica fue validada estáticamente y contra los archivos históricos, la condición final de “producción 100%” debe cerrarse con **una ejecución real controlada en n8n**.
|
||||||
|
|
||||||
|
En particular, los conteos históricos de `Banco sin Bamboo` (7/8) dependen de la foto real de BambooHR y de los alias almacenados en Supabase al momento de ejecutar. La lógica de consulta y filtrado está implementada, pero esos conteos no pueden reproducirse offline sin consultar BambooHR.
|
||||||
|
|
||||||
|
## Gate final de producción
|
||||||
|
|
||||||
|
RD puede declararse definitivamente “ready mensual” cuando una ejecución live cumpla simultáneamente:
|
||||||
|
|
||||||
|
- webhook RD recibe el Excel + todos los CSV/PDF de la quincena;
|
||||||
|
- período detectado/seleccionado coincide con `PERÍODO` de la nómina;
|
||||||
|
- BambooHR responde correctamente con `onlyCurrent=false`;
|
||||||
|
- se generan las 6 pestañas;
|
||||||
|
- el Google Sheet se comparte correctamente;
|
||||||
|
- Noralmy recibe el correo;
|
||||||
|
- histórico `country=RD` queda guardado;
|
||||||
|
- “Marcar resuelto” actualiza ese reporte;
|
||||||
|
- un segundo procesamiento del mismo período reutiliza el reporte y preserva resoluciones aplicables;
|
||||||
|
- una de las cuatro quincenas patrón reproduce sus hallazgos funcionales;
|
||||||
|
- una quincena nueva/no entrenada no arroja falsos positivos evidentes.
|
||||||
|
|
||||||
|
El detalle de la regresión también queda en `VALIDACION_RD_REGRESION.json`.
|
||||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+270
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-270
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -9,8 +9,8 @@
|
|||||||
<title>Portal de Cruce de Cuentas GLM</title>
|
<title>Portal de Cruce de Cuentas GLM</title>
|
||||||
<link rel="icon" type="image/png" href="/cruce-cuentas/favicon.png" />
|
<link rel="icon" type="image/png" href="/cruce-cuentas/favicon.png" />
|
||||||
<link rel="apple-touch-icon" href="/cruce-cuentas/favicon.png" />
|
<link rel="apple-touch-icon" href="/cruce-cuentas/favicon.png" />
|
||||||
<script type="module" crossorigin src="/cruce-cuentas/assets/index-saVPcx_I.js"></script>
|
<script type="module" crossorigin src="/cruce-cuentas/assets/index-DGYJf0Q4.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/cruce-cuentas/assets/index-CLG5SAqX.css">
|
<link rel="stylesheet" crossorigin href="/cruce-cuentas/assets/index-DKvwoX2P.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export default function BambooCorrectionModal({
|
|||||||
if (country.code === 'SV') return 'Carlos Antonio Malta Méndez';
|
if (country.code === 'SV') return 'Carlos Antonio Malta Méndez';
|
||||||
if (country.code === 'HN') return 'Melody Chang Blandin';
|
if (country.code === 'HN') return 'Melody Chang Blandin';
|
||||||
if (country.code === 'PR') return 'Noralmy Laureano';
|
if (country.code === 'PR') return 'Noralmy Laureano';
|
||||||
|
if (country.code === 'RD') return 'Noralmy Laureano';
|
||||||
return 'haguirre@gomezleemarketing.com';
|
return 'haguirre@gomezleemarketing.com';
|
||||||
}, [country.code]);
|
}, [country.code]);
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,13 @@ function getReconciliationUrl(countryCode: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (countryCode === 'RD') {
|
||||||
|
return (
|
||||||
|
(import.meta.env.VITE_N8N_REPUBLICA_DOMINICANA_WEBHOOK_URL as string | undefined) ||
|
||||||
|
'https://agenteit.digitalcompass.agency/webhook/nominard-bamboo-test'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(import.meta.env.VITE_N8N_GUATEMALA_WEBHOOK_URL as string | undefined) ||
|
(import.meta.env.VITE_N8N_GUATEMALA_WEBHOOK_URL as string | undefined) ||
|
||||||
(import.meta.env.VITE_N8N_WEBHOOK_URL as string | undefined) ||
|
(import.meta.env.VITE_N8N_WEBHOOK_URL as string | undefined) ||
|
||||||
@@ -382,15 +389,32 @@ function normalizeWebhookResponse(payload: unknown): ReconciliationResponse {
|
|||||||
primaryRows.filter((row) => normalizeText(String(row.category ?? '')).includes('posible_cuenta_mal_digitada')).length
|
primaryRows.filter((row) => normalizeText(String(row.category ?? '')).includes('posible_cuenta_mal_digitada')).length
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalPending =
|
const responseMetadata =
|
||||||
|
(original.metadata ?? item.metadata ?? {}) as Record<string, unknown>;
|
||||||
|
const responseCountry = String(
|
||||||
|
responseMetadata.country ?? responseMetadata.country_code ?? ''
|
||||||
|
).toUpperCase();
|
||||||
|
const defaultTotalPending =
|
||||||
discrepancies +
|
discrepancies +
|
||||||
bankWithoutPayroll +
|
bankWithoutPayroll +
|
||||||
bankWithoutBamboo +
|
bankWithoutBamboo +
|
||||||
payrollWithoutAccount +
|
payrollWithoutAccount +
|
||||||
nameDifferences;
|
nameDifferences;
|
||||||
|
const rdTotalPending =
|
||||||
const responseMetadata =
|
discrepancies +
|
||||||
(original.metadata ?? item.metadata ?? {}) as Record<string, unknown>;
|
bankWithoutPayroll +
|
||||||
|
payrollWithoutAccount +
|
||||||
|
nameDifferences +
|
||||||
|
possibleWrongAccounts;
|
||||||
|
const totalPending = responseCountry === 'RD'
|
||||||
|
? toNumber(
|
||||||
|
rawSummary.totalPending ??
|
||||||
|
rawSummary.pendientes ??
|
||||||
|
rawSummary.pendientes_cruce_principal ??
|
||||||
|
rawSummary.total_pending,
|
||||||
|
rdTotalPending
|
||||||
|
)
|
||||||
|
: defaultTotalPending;
|
||||||
const responsePeriodType = String(
|
const responsePeriodType = String(
|
||||||
responseMetadata.period_type ?? responseMetadata.periodType ?? ''
|
responseMetadata.period_type ?? responseMetadata.periodType ?? ''
|
||||||
).toLowerCase();
|
).toLowerCase();
|
||||||
@@ -675,7 +699,7 @@ const MONTH_ALIASES: Record<number, string[]> = {
|
|||||||
12: ['diciembre', 'december', 'dec'],
|
12: ['diciembre', 'december', 'dec'],
|
||||||
};
|
};
|
||||||
|
|
||||||
function detectPeriodFromPayrollFilename(fileName: string): Partial<DetectedPayrollPeriod> {
|
function detectPeriodFromPayrollFilename(fileName: string, countryCode?: CountryCode): Partial<DetectedPayrollPeriod> {
|
||||||
const normalized = normalizeText(fileName)
|
const normalized = normalizeText(fileName)
|
||||||
.replace(/[_\-.()]+/g, ' ')
|
.replace(/[_\-.()]+/g, ' ')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
@@ -712,6 +736,11 @@ function detectPeriodFromPayrollFilename(fileName: string): Partial<DetectedPayr
|
|||||||
periodType = 'quincena_15';
|
periodType = 'quincena_15';
|
||||||
} else if (/\b(?:30|31)\b/.test(normalized)) {
|
} else if (/\b(?:30|31)\b/.test(normalized)) {
|
||||||
periodType = 'quincena_30';
|
periodType = 'quincena_30';
|
||||||
|
} else if (countryCode === 'RD' && year && month) {
|
||||||
|
const lastDay = new Date(year, month, 0).getDate();
|
||||||
|
if (new RegExp(`\\b${lastDay}\\b`).test(normalized)) {
|
||||||
|
periodType = 'quincena_30';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { year, month, periodType };
|
return { year, month, periodType };
|
||||||
@@ -742,7 +771,7 @@ function readStoredPeriod(countryCode: string): DetectedPayrollPeriod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function analyzePayrollFilename(fileName: string, country: CountryConfig) {
|
async function analyzePayrollFilename(fileName: string, country: CountryConfig) {
|
||||||
const local = detectPeriodFromPayrollFilename(fileName);
|
const local = detectPeriodFromPayrollFilename(fileName, country.code);
|
||||||
|
|
||||||
if (!PAYROLL_FILENAME_ANALYSIS_URL) {
|
if (!PAYROLL_FILENAME_ANALYSIS_URL) {
|
||||||
return {
|
return {
|
||||||
@@ -826,9 +855,11 @@ export default function Dashboard({
|
|||||||
const isElSalvador = country.code === 'SV';
|
const isElSalvador = country.code === 'SV';
|
||||||
const isHonduras = country.code === 'HN';
|
const isHonduras = country.code === 'HN';
|
||||||
const isPuertoRico = country.code === 'PR';
|
const isPuertoRico = country.code === 'PR';
|
||||||
|
const isRepublicaDominicana = country.code === 'RD';
|
||||||
const isMultiPayrollCountry = isPanama || isElSalvador;
|
const isMultiPayrollCountry = isPanama || isElSalvador;
|
||||||
const bankFileLabel = isPuertoRico ? 'PDF' : 'CSV';
|
const bankFileLabel = isRepublicaDominicana ? 'CSV / PDF' : isPuertoRico ? 'PDF' : 'CSV';
|
||||||
const bankFileExtensionLabel = isPuertoRico ? '.pdf' : '.csv';
|
const bankFileExtensionLabel = isRepublicaDominicana ? '.csv, .pdf' : isPuertoRico ? '.pdf' : '.csv';
|
||||||
|
const isValidBankFile = (file: File) => isRepublicaDominicana ? (isCsvFile(file) || isPdfFile(file)) : isPuertoRico ? isPdfFile(file) : isCsvFile(file);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [processingStep, setProcessingStep] = useState('');
|
const [processingStep, setProcessingStep] = useState('');
|
||||||
const [filterMode, setFilterMode] = useState<
|
const [filterMode, setFilterMode] = useState<
|
||||||
@@ -1098,7 +1129,7 @@ export default function Dashboard({
|
|||||||
const handleBankDrop = (e: React.DragEvent) => {
|
const handleBankDrop = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const files = Array.from(e.dataTransfer.files) as File[];
|
const files = Array.from(e.dataTransfer.files) as File[];
|
||||||
const validBankFiles = files.filter(isPuertoRico ? isPdfFile : isCsvFile);
|
const validBankFiles = files.filter(isValidBankFile);
|
||||||
|
|
||||||
if (validBankFiles.length === 0) {
|
if (validBankFiles.length === 0) {
|
||||||
onShowToast(
|
onShowToast(
|
||||||
@@ -1143,7 +1174,7 @@ export default function Dashboard({
|
|||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (!files || files.length === 0) return;
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
const validBankFiles = (Array.from(files) as File[]).filter(isPuertoRico ? isPdfFile : isCsvFile);
|
const validBankFiles = (Array.from(files) as File[]).filter(isValidBankFile);
|
||||||
|
|
||||||
if (validBankFiles.length === 0) {
|
if (validBankFiles.length === 0) {
|
||||||
onShowToast(
|
onShowToast(
|
||||||
@@ -1283,7 +1314,7 @@ export default function Dashboard({
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const duplicatedShipments = isPuertoRico
|
const duplicatedShipments = (isPuertoRico || isRepublicaDominicana)
|
||||||
? []
|
? []
|
||||||
: await findDuplicateBankShipments(uploadedBankFiles);
|
: await findDuplicateBankShipments(uploadedBankFiles);
|
||||||
if (duplicatedShipments.length > 0) {
|
if (duplicatedShipments.length > 0) {
|
||||||
@@ -1689,7 +1720,7 @@ export default function Dashboard({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{(isMultiPayrollCountry || isHonduras || isPuertoRico) &&
|
{(isMultiPayrollCountry || isHonduras || isPuertoRico || isRepublicaDominicana) &&
|
||||||
(uploadedPayroll !== null || uploadedPayrollFiles.length > 0 || uploadedBankFiles.length > 0) && (
|
(uploadedPayroll !== null || uploadedPayrollFiles.length > 0 || uploadedBankFiles.length > 0) && (
|
||||||
<div className="flex justify-end" style={{ marginBottom: '-20px' }}>
|
<div className="flex justify-end" style={{ marginBottom: '-20px' }}>
|
||||||
<button
|
<button
|
||||||
@@ -1845,7 +1876,7 @@ export default function Dashboard({
|
|||||||
type="file"
|
type="file"
|
||||||
ref={bankInputRef}
|
ref={bankInputRef}
|
||||||
onChange={handleBankSelect}
|
onChange={handleBankSelect}
|
||||||
accept={isPuertoRico ? ".pdf" : ".csv"}
|
accept={isRepublicaDominicana ? ".csv,.pdf" : isPuertoRico ? ".pdf" : ".csv"}
|
||||||
multiple
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
@@ -2323,40 +2354,52 @@ export default function Dashboard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-hidden">
|
||||||
<table className="w-full text-left border-collapse min-w-[960px] xl:min-w-full">
|
<table className="w-full table-fixed text-left border-collapse">
|
||||||
|
<colgroup>
|
||||||
|
<col style={{ width: '10%' }} />
|
||||||
|
<col style={{ width: '8%' }} />
|
||||||
|
<col style={{ width: '10%' }} />
|
||||||
|
<col style={{ width: '9%' }} />
|
||||||
|
<col style={{ width: '9%' }} />
|
||||||
|
<col style={{ width: '9%' }} />
|
||||||
|
<col style={{ width: '8%' }} />
|
||||||
|
<col style={{ width: '9%' }} />
|
||||||
|
<col style={{ width: '17%' }} />
|
||||||
|
<col style={{ width: '11%' }} />
|
||||||
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-[#4F758B] text-white text-xs font-bold uppercase tracking-wider">
|
<tr className="bg-[#4F758B] text-white text-xs font-bold uppercase tracking-wider">
|
||||||
<th className="py-3 px-3 xl:px-4">Empleado</th>
|
<th className="py-3 px-2 xl:px-3">Empleado</th>
|
||||||
<th className="py-3 px-3 xl:px-4">Employee Number</th>
|
<th className="py-3 px-2 xl:px-3">Employee Number</th>
|
||||||
<th className="py-3 px-3 xl:px-4">Cuenta</th>
|
<th className="py-3 px-2 xl:px-3">Cuenta</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-right">{isBonus14Result ? 'Monto Bono 14' : 'Monto Nómina'}</th>
|
<th className="py-3 px-2 xl:px-3 text-right">{isBonus14Result ? 'Monto Bono 14' : 'Monto Nómina'}</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-right">Monto Banco</th>
|
<th className="py-3 px-2 xl:px-3 text-right">Monto Banco</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-right">Diferencia</th>
|
<th className="py-3 px-2 xl:px-3 text-right">Diferencia</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-center">Estado</th>
|
<th className="py-3 px-2 xl:px-3 text-center">Estado</th>
|
||||||
<th className="py-3 px-3 xl:px-4">Categoría</th>
|
<th className="py-3 px-2 xl:px-3">Categoría</th>
|
||||||
<th className="py-3 px-3 xl:px-4">Observación</th>
|
<th className="py-3 px-2 xl:px-3">Observación</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-center">Acciones</th>
|
<th className="py-3 px-2 xl:px-3 text-center">Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-neutral-200 text-xs">
|
<tbody className="divide-y divide-neutral-200 text-xs">
|
||||||
{paginatedRows.length > 0 ? (
|
{paginatedRows.length > 0 ? (
|
||||||
paginatedRows.map((row) => (
|
paginatedRows.map((row) => (
|
||||||
<tr key={row.id} className="hover:bg-neutral-50 transition-colors">
|
<tr key={row.id} className="hover:bg-neutral-50 transition-colors">
|
||||||
<td className="py-3 px-3 xl:px-4 font-bold text-neutral-800">{row.employee}</td>
|
<td className="py-3 px-2 xl:px-3 font-bold text-neutral-800 break-words">{row.employee}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 font-mono text-neutral-600">{row.employeeNumber || '—'}</td>
|
<td className="py-3 px-2 xl:px-3 font-mono text-neutral-600 break-all">{row.employeeNumber || '—'}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 font-mono text-neutral-500">{row.account ?? '—'}</td>
|
<td className="py-3 px-2 xl:px-3 font-mono text-neutral-500 break-all">{row.account ?? '—'}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-right font-mono text-neutral-700">{formatCurrency(row.amountPayroll, country, row.currency)}</td>
|
<td className="py-3 px-2 xl:px-3 text-right font-mono text-neutral-700 whitespace-nowrap">{formatCurrency(row.amountPayroll, country, row.currency)}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-right font-mono text-neutral-700">{formatCurrency(row.amountBank, country, row.currency)}</td>
|
<td className="py-3 px-2 xl:px-3 text-right font-mono text-neutral-700 whitespace-nowrap">{formatCurrency(row.amountBank, country, row.currency)}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-right font-mono text-neutral-700">{formatCurrency(row.difference, country, row.currency)}</td>
|
<td className="py-3 px-2 xl:px-3 text-right font-mono text-neutral-700 whitespace-nowrap">{formatCurrency(row.difference, country, row.currency)}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-center">
|
<td className="py-3 px-2 xl:px-3 text-center">
|
||||||
<span className={`inline-block px-2.5 py-1 text-[9px] font-black uppercase tracking-wider border ${statusClass(row.status)}`}>
|
<span className={`inline-block max-w-full px-2 py-1 text-[9px] font-black uppercase tracking-wider border leading-tight ${statusClass(row.status)}`}>
|
||||||
{row.status}
|
{row.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-neutral-600 max-w-[180px] capitalize">{formatCategory(row.category ?? row.source, response?.reportKind ?? 'nomina')}</td>
|
<td className="py-3 px-2 xl:px-3 text-neutral-600 capitalize break-words">{formatCategory(row.category ?? row.source, response?.reportKind ?? 'nomina')}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-neutral-600 max-w-[300px]">{row.observation}</td>
|
<td className="py-3 px-2 xl:px-3 text-neutral-600 break-words">{row.observation}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-center">
|
<td className="py-3 px-2 xl:px-3 text-center">
|
||||||
{isSourceRow(row, 'banco_sin_bamboo') ? (
|
{isSourceRow(row, 'banco_sin_bamboo') ? (
|
||||||
reportedBambooRows.has(row.id) ? (
|
reportedBambooRows.has(row.id) ? (
|
||||||
<span className="inline-flex items-center px-2.5 py-1 border border-emerald-200 bg-emerald-50 text-emerald-800 text-[9px] font-black uppercase tracking-wider">
|
<span className="inline-flex items-center px-2.5 py-1 border border-emerald-200 bg-emerald-50 text-emerald-800 text-[9px] font-black uppercase tracking-wider">
|
||||||
@@ -2366,7 +2409,7 @@ export default function Dashboard({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setBambooCorrectionRow(row)}
|
onClick={() => setBambooCorrectionRow(row)}
|
||||||
className="inline-flex items-center justify-center px-3 py-2 border border-[#4F758B] text-[#4F758B] hover:bg-[#4F758B] hover:text-white text-[9px] font-black uppercase tracking-wider transition-colors whitespace-nowrap"
|
className="inline-flex max-w-full items-center justify-center px-2.5 py-2 border border-[#4F758B] text-[#4F758B] hover:bg-[#4F758B] hover:text-white text-[9px] font-black uppercase tracking-wider transition-colors whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Reportar a RRHH
|
Reportar a RRHH
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+81
-29
@@ -61,6 +61,13 @@ function getHistoryUrl(countryCode: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (countryCode === 'RD') {
|
||||||
|
return (
|
||||||
|
(import.meta.env.VITE_HISTORICOS_RD_URL as string | undefined) ||
|
||||||
|
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-rd-historicos'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(import.meta.env.VITE_HISTORICOS_GT_URL as string | undefined) ||
|
(import.meta.env.VITE_HISTORICOS_GT_URL as string | undefined) ||
|
||||||
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-historicos'
|
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-historicos'
|
||||||
@@ -100,6 +107,13 @@ function getMarkResolvedUrl(countryCode: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (countryCode === 'RD') {
|
||||||
|
return (
|
||||||
|
(import.meta.env.VITE_MARCAR_RESUELTO_RD_URL as string | undefined) ||
|
||||||
|
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-rd-marcar-resuelto'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(import.meta.env.VITE_MARCAR_RESUELTO_GT_URL as string | undefined) ||
|
(import.meta.env.VITE_MARCAR_RESUELTO_GT_URL as string | undefined) ||
|
||||||
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-marcar-resuelto'
|
'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-marcar-resuelto'
|
||||||
@@ -164,18 +178,45 @@ function normalizeReports(input: unknown, country: CountryConfig): HistoricalRep
|
|||||||
const payrollWithoutAccount = toNumber(
|
const payrollWithoutAccount = toNumber(
|
||||||
report.nominaSinCuenta ?? report.nomina_sin_cuenta ?? report.payrollWithoutAccount
|
report.nominaSinCuenta ?? report.nomina_sin_cuenta ?? report.payrollWithoutAccount
|
||||||
);
|
);
|
||||||
|
const reportMetadata =
|
||||||
|
report.metadata && typeof report.metadata === 'object' && !Array.isArray(report.metadata)
|
||||||
|
? report.metadata as Record<string, unknown>
|
||||||
|
: {};
|
||||||
|
const reportSummary =
|
||||||
|
report.summary && typeof report.summary === 'object' && !Array.isArray(report.summary)
|
||||||
|
? report.summary as Record<string, unknown>
|
||||||
|
: {};
|
||||||
const nameDifferences = toNumber(
|
const nameDifferences = toNumber(
|
||||||
report.diferenciasNombreBanco ??
|
report.diferenciasNombreBanco ??
|
||||||
report.diferencias_nombre_banco ??
|
report.diferencias_nombre_banco ??
|
||||||
report.nameDifferences
|
report.nameDifferences ??
|
||||||
|
reportMetadata.diferencias_nombre_banco ??
|
||||||
|
reportSummary.diferenciasNombreBanco
|
||||||
);
|
);
|
||||||
const totalPending =
|
const possibleWrongAccounts = toNumber(
|
||||||
|
report.posiblesCuentasMalDigitadas ??
|
||||||
|
report.posibles_cuentas_mal_digitadas ??
|
||||||
|
report.possibleWrongAccounts ??
|
||||||
|
reportMetadata.posibles_cuentas_mal_digitadas ??
|
||||||
|
reportSummary.posiblesCuentasMalDigitadas
|
||||||
|
);
|
||||||
|
const calculatedTotalPending =
|
||||||
discrepancies +
|
discrepancies +
|
||||||
bankWithoutPayroll +
|
bankWithoutPayroll +
|
||||||
bankWithoutBamboo +
|
bankWithoutBamboo +
|
||||||
payrollWithoutAccount +
|
payrollWithoutAccount +
|
||||||
nameDifferences;
|
nameDifferences;
|
||||||
const rawCountry = String(report.country ?? report.pais ?? report.country_code ?? country.code).toUpperCase();
|
const rawCountry = String(report.country ?? report.pais ?? report.country_code ?? country.code).toUpperCase();
|
||||||
|
const totalPending = rawCountry === 'RD'
|
||||||
|
? toNumber(
|
||||||
|
report.pendientes ??
|
||||||
|
report.total_pending ??
|
||||||
|
report.totalPending ??
|
||||||
|
reportMetadata.pendientes_cruce_principal ??
|
||||||
|
reportSummary.pendientes,
|
||||||
|
discrepancies + bankWithoutPayroll + payrollWithoutAccount + nameDifferences + possibleWrongAccounts
|
||||||
|
)
|
||||||
|
: calculatedTotalPending;
|
||||||
const bankFiles = Array.isArray(report.archivosBanco)
|
const bankFiles = Array.isArray(report.archivosBanco)
|
||||||
? report.archivosBanco.map(String)
|
? report.archivosBanco.map(String)
|
||||||
: Array.isArray(report.bank_file_names)
|
: Array.isArray(report.bank_file_names)
|
||||||
@@ -185,7 +226,7 @@ function normalizeReports(input: unknown, country: CountryConfig): HistoricalRep
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(report.id ?? `reporte-${index + 1}`),
|
id: String(report.id ?? `reporte-${index + 1}`),
|
||||||
country: (rawCountry === 'TT' ? 'TT' : rawCountry === 'PA' ? 'PA' : rawCountry === 'SV' ? 'SV' : rawCountry === 'HN' ? 'HN' : rawCountry === 'PR' ? 'PR' : 'GT') as HistoricalReport['country'],
|
country: (rawCountry === 'TT' ? 'TT' : rawCountry === 'PA' ? 'PA' : rawCountry === 'SV' ? 'SV' : rawCountry === 'HN' ? 'HN' : rawCountry === 'PR' ? 'PR' : rawCountry === 'RD' ? 'RD' : 'GT') as HistoricalReport['country'],
|
||||||
date: formatDate(report.fecha ?? report.period_end ?? rawExecutionDate, country.locale),
|
date: formatDate(report.fecha ?? report.period_end ?? rawExecutionDate, country.locale),
|
||||||
executionDate: formatDateTime(rawExecutionDate, country.locale),
|
executionDate: formatDateTime(rawExecutionDate, country.locale),
|
||||||
period: String(report.periodo ?? report.period_label ?? report.period ?? 'Período no especificado'),
|
period: String(report.periodo ?? report.period_label ?? report.period ?? 'Período no especificado'),
|
||||||
@@ -499,7 +540,7 @@ export default function History({
|
|||||||
if (!isCurrentRequest()) return;
|
if (!isCurrentRequest()) return;
|
||||||
|
|
||||||
// Respaldo temporal: webhook histórico existente. También se envía el país
|
// Respaldo temporal: webhook histórico existente. También se envía el país
|
||||||
// para impedir que se mezclen reportes de Guatemala, Trinidad y Tobago, Panamá, El Salvador, Honduras y Puerto Rico.
|
// para impedir que se mezclen reportes de Guatemala, Trinidad y Tobago, Panamá, El Salvador, Honduras, Puerto Rico y República Dominicana.
|
||||||
const historyUrl = getHistoryUrl(country.code);
|
const historyUrl = getHistoryUrl(country.code);
|
||||||
if (!historyUrl) {
|
if (!historyUrl) {
|
||||||
throw new Error(`No está configurado el origen histórico de ${country.name}.`);
|
throw new Error(`No está configurado el origen histórico de ${country.name}.`);
|
||||||
@@ -834,19 +875,30 @@ export default function History({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="bg-white border border-[#D0D0D0] shadow-sm overflow-hidden">
|
<div className="bg-white border border-[#D0D0D0] shadow-sm overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto lg:overflow-x-hidden">
|
||||||
<table className="w-full text-left border-collapse min-w-[960px] xl:min-w-full">
|
<table className="w-full text-left border-collapse min-w-[960px] lg:min-w-0 lg:table-fixed">
|
||||||
|
<colgroup>
|
||||||
|
<col className="lg:w-[9%]" />
|
||||||
|
<col className="lg:w-[8%]" />
|
||||||
|
<col className="lg:w-[21%]" />
|
||||||
|
<col className="lg:w-[9%]" />
|
||||||
|
<col className="lg:w-[9%]" />
|
||||||
|
<col className="lg:w-[9%]" />
|
||||||
|
<col className="lg:w-[9%]" />
|
||||||
|
<col className="lg:w-[10%]" />
|
||||||
|
<col className="lg:w-[16%]" />
|
||||||
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-[#4F758B] text-white text-xs font-bold uppercase tracking-wider">
|
<tr className="bg-[#4F758B] text-white text-xs font-bold uppercase tracking-wider">
|
||||||
<th className="py-3 px-3 xl:px-4">Fecha</th>
|
<th className="py-3 px-2 xl:px-3">Fecha</th>
|
||||||
<th className="py-3 px-3 xl:px-4">Período</th>
|
<th className="py-3 px-2 xl:px-3">Período</th>
|
||||||
<th className="py-3 px-3 xl:px-4">Archivo de Nómina / Bono 14</th>
|
<th className="py-3 px-2 xl:px-3">Archivo de Nómina / Bono 14</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-center">Coincidencias</th>
|
<th className="py-3 px-2 xl:px-3 text-center">Coincidencias</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-center">Banco sin Bamboo</th>
|
<th className="py-3 px-2 xl:px-3 text-center">Banco sin Bamboo</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-center">Pendientes</th>
|
<th className="py-3 px-2 xl:px-3 text-center">Pendientes</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-center">Estado</th>
|
<th className="py-3 px-2 xl:px-3 text-center">Estado</th>
|
||||||
<th className="py-3 px-3 xl:px-4">Resuelto por</th>
|
<th className="py-3 px-2 xl:px-3">Resuelto por</th>
|
||||||
<th className="py-3 px-3 xl:px-4 text-center">Acciones</th>
|
<th className="py-3 px-2 xl:px-3 text-center">Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-neutral-200 text-xs">
|
<tbody className="divide-y divide-neutral-200 text-xs">
|
||||||
@@ -867,7 +919,7 @@ export default function History({
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
className="hover:bg-neutral-50/70 transition-colors"
|
className="hover:bg-neutral-50/70 transition-colors"
|
||||||
>
|
>
|
||||||
<td className="py-3 px-3 xl:px-4 font-medium text-neutral-700">
|
<td className="py-3 px-2 xl:px-3 font-medium text-neutral-700">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="w-4 h-4 text-neutral-400" />
|
<Calendar className="w-4 h-4 text-neutral-400" />
|
||||||
<div>
|
<div>
|
||||||
@@ -876,15 +928,15 @@ export default function History({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-3 xl:px-4 font-bold text-[#4F758B]">{report.period}</td>
|
<td className="py-3 px-2 xl:px-3 font-bold text-[#4F758B]">{report.period}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 font-mono text-neutral-500 truncate max-w-[260px]" title={report.payrollFile}>
|
<td className="py-3 px-2 xl:px-3 font-mono text-neutral-500 min-w-0" title={report.payrollFile}>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<FileSpreadsheet className="w-4 h-4 text-neutral-400 flex-shrink-0" />
|
<FileSpreadsheet className="w-4 h-4 text-neutral-400 flex-shrink-0" />
|
||||||
<span className="truncate">{report.payrollFile || 'No especificado'}</span>
|
<span className="truncate min-w-0">{report.payrollFile || 'No especificado'}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-center font-black text-emerald-800">{report.matches}</td>
|
<td className="py-3 px-2 xl:px-3 text-center font-black text-emerald-800">{report.matches}</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-center">
|
<td className="py-3 px-2 xl:px-3 text-center">
|
||||||
{report.bankWithoutBamboo > 0 ? (
|
{report.bankWithoutBamboo > 0 ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -901,7 +953,7 @@ export default function History({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-center">
|
<td className="py-3 px-2 xl:px-3 text-center">
|
||||||
<span
|
<span
|
||||||
className={`inline-block px-2.5 py-1 text-[10px] font-bold border ${
|
className={`inline-block px-2.5 py-1 text-[10px] font-bold border ${
|
||||||
report.totalPending > 0
|
report.totalPending > 0
|
||||||
@@ -914,12 +966,12 @@ export default function History({
|
|||||||
: `${report.totalPending} pendiente${report.totalPending > 1 ? 's' : ''}`}
|
: `${report.totalPending} pendiente${report.totalPending > 1 ? 's' : ''}`}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-center">
|
<td className="py-3 px-2 xl:px-3 text-center">
|
||||||
<span className={`inline-block px-2.5 py-1 text-[9px] uppercase font-black tracking-wider border ${statusClass(report.status)}`}>
|
<span className={`inline-block px-2.5 py-1 text-[9px] uppercase font-black tracking-wider border ${statusClass(report.status)}`}>
|
||||||
{statusText(report.status)}
|
{statusText(report.status)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-neutral-600 max-w-[220px]">
|
<td className="py-3 px-2 xl:px-3 text-neutral-600 max-w-[220px]">
|
||||||
{report.resolvedBy ? (
|
{report.resolvedBy ? (
|
||||||
<div title={report.resolutionComment || ''}>
|
<div title={report.resolutionComment || ''}>
|
||||||
<p className="font-bold text-neutral-700 truncate">{report.resolvedBy}</p>
|
<p className="font-bold text-neutral-700 truncate">{report.resolvedBy}</p>
|
||||||
@@ -929,11 +981,11 @@ export default function History({
|
|||||||
<span className="text-neutral-400">Pendiente</span>
|
<span className="text-neutral-400">Pendiente</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-3 xl:px-4 text-center">
|
<td className="py-3 px-2 xl:px-3 text-center">
|
||||||
<div className="flex justify-center gap-2">
|
<div className="flex justify-center items-stretch gap-1.5 min-w-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleViewReport(report)}
|
onClick={() => handleViewReport(report)}
|
||||||
className="inline-flex items-center gap-1.5 text-[#4F758B] hover:text-[#3d6378] font-bold text-xs border border-[#4F758B]/20 hover:border-[#4F758B]/50 px-3 py-1.5 bg-white hover:bg-neutral-50 transition-colors"
|
className="inline-flex items-center gap-1.5 text-[#4F758B] hover:text-[#3d6378] font-bold text-xs border border-[#4F758B]/20 hover:border-[#4F758B]/50 px-2.5 py-1.5 bg-white hover:bg-neutral-50 transition-colors"
|
||||||
>
|
>
|
||||||
<Eye className="w-3.5 h-3.5" />
|
<Eye className="w-3.5 h-3.5" />
|
||||||
<span>Ver</span>
|
<span>Ver</span>
|
||||||
@@ -941,10 +993,10 @@ export default function History({
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleResolveReport(report)}
|
onClick={() => handleResolveReport(report)}
|
||||||
disabled={report.status === 'Resuelto'}
|
disabled={report.status === 'Resuelto'}
|
||||||
className="inline-flex items-center gap-1.5 text-emerald-700 hover:text-emerald-800 font-bold text-xs border border-emerald-200 hover:border-emerald-400 px-3 py-1.5 bg-emerald-50 hover:bg-emerald-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="inline-flex items-center gap-1.5 text-emerald-700 hover:text-emerald-800 font-bold text-xs border border-emerald-200 hover:border-emerald-400 px-2.5 py-1.5 bg-emerald-50 hover:bg-emerald-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||||
<span>{report.status === 'Resuelto' ? 'Resuelto' : 'Marcar resuelto'}</span>
|
<span className="leading-tight">{report.status === 'Resuelto' ? 'Resuelto' : 'Marcar resuelto'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ export const COUNTRIES: CountryConfig[] = [
|
|||||||
locale: 'es-PR',
|
locale: 'es-PR',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
code: 'RD',
|
||||||
|
name: 'República Dominicana',
|
||||||
|
flag: 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/Flag_of_the_Dominican_Republic.svg.webp',
|
||||||
|
currency: 'DOP',
|
||||||
|
locale: 'es-DO',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_COUNTRY_CODE: CountryCode = 'GT';
|
export const DEFAULT_COUNTRY_CODE: CountryCode = 'GT';
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ export type ViewType = 'cruce' | 'historial';
|
|||||||
|
|
||||||
export type ToastType = 'success' | 'error' | 'info';
|
export type ToastType = 'success' | 'error' | 'info';
|
||||||
|
|
||||||
export type CountryCode = 'GT' | 'TT' | 'PA' | 'SV' | 'HN' | 'PR';
|
export type CountryCode = 'GT' | 'TT' | 'PA' | 'SV' | 'HN' | 'PR' | 'RD';
|
||||||
|
|
||||||
export interface CountryConfig {
|
export interface CountryConfig {
|
||||||
code: CountryCode;
|
code: CountryCode;
|
||||||
|
|||||||
Vendored
+3
@@ -8,6 +8,7 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_N8N_EL_SALVADOR_WEBHOOK_URL?: string;
|
readonly VITE_N8N_EL_SALVADOR_WEBHOOK_URL?: string;
|
||||||
readonly VITE_N8N_HONDURAS_WEBHOOK_URL?: string;
|
readonly VITE_N8N_HONDURAS_WEBHOOK_URL?: string;
|
||||||
readonly VITE_N8N_PUERTO_RICO_WEBHOOK_URL?: string;
|
readonly VITE_N8N_PUERTO_RICO_WEBHOOK_URL?: string;
|
||||||
|
readonly VITE_N8N_REPUBLICA_DOMINICANA_WEBHOOK_URL?: string;
|
||||||
readonly VITE_HISTORICOS_GT_URL?: string;
|
readonly VITE_HISTORICOS_GT_URL?: string;
|
||||||
readonly VITE_MARCAR_RESUELTO_GT_URL?: string;
|
readonly VITE_MARCAR_RESUELTO_GT_URL?: string;
|
||||||
readonly VITE_HISTORICOS_TT_URL?: string;
|
readonly VITE_HISTORICOS_TT_URL?: string;
|
||||||
@@ -20,6 +21,8 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_MARCAR_RESUELTO_HN_URL?: string;
|
readonly VITE_MARCAR_RESUELTO_HN_URL?: string;
|
||||||
readonly VITE_HISTORICOS_PR_URL?: string;
|
readonly VITE_HISTORICOS_PR_URL?: string;
|
||||||
readonly VITE_MARCAR_RESUELTO_PR_URL?: string;
|
readonly VITE_MARCAR_RESUELTO_PR_URL?: string;
|
||||||
|
readonly VITE_HISTORICOS_RD_URL?: string;
|
||||||
|
readonly VITE_MARCAR_RESUELTO_RD_URL?: string;
|
||||||
readonly VITE_ANALIZAR_NOMINA_URL?: string;
|
readonly VITE_ANALIZAR_NOMINA_URL?: string;
|
||||||
readonly VITE_REPORTAR_BAMBOO_URL?: string;
|
readonly VITE_REPORTAR_BAMBOO_URL?: string;
|
||||||
readonly VITE_REPORTAR_APP_URL?: string;
|
readonly VITE_REPORTAR_APP_URL?: string;
|
||||||
|
|||||||
+280
File diff suppressed because one or more lines are too long
+280
File diff suppressed because one or more lines are too long
+279
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+348
File diff suppressed because one or more lines are too long
+164
@@ -0,0 +1,164 @@
|
|||||||
|
{
|
||||||
|
"name": "Portal de Verificación de Nómina República Dominicana - Históricos",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"path": "cruce-cuentas-rd-historicos",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [
|
||||||
|
304,
|
||||||
|
272
|
||||||
|
],
|
||||||
|
"id": "7550a9f8-2006-4330-8b6d-f0a99b53ba69",
|
||||||
|
"name": "Webhook Históricos RD",
|
||||||
|
"webhookId": "31b88d0c-ab11-4ff9-87c9-895b4fb6b097"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const items = $input.all();\n\nfunction formatDate(value) {\n if (!value) return '';\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return String(value).slice(0, 10);\n return date.toISOString().slice(0, 10);\n}\n\nfunction toNumber(value) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction normalizeStatus(value, discrepancias) {\n const status = String(value || '').toLowerCase();\n\n if (status === 'resuelto') return 'resuelto';\n if (status === 'procesado' && toNumber(discrepancias) === 0) return 'procesado';\n\n return 'pendiente_revision';\n}\n\nfunction statusLabel(status) {\n if (status === 'resuelto') return 'RESUELTO';\n if (status === 'procesado') return 'PROCESADO';\n return 'PENDIENTE REVISIÓN';\n}\n\nconst reports = items.map((item) => {\n const row = item.json || {};\n\n const discrepancias = toNumber(row.discrepancias);\n const bancoSinNomina = toNumber(row.banco_sin_nomina);\n const nominaSinCuenta = toNumber(row.nomina_sin_cuenta);\n const bancoSinBamboo = toNumber(row.banco_sin_bamboo);\n const nominaSinBamboo = toNumber(row.nomina_sin_bamboo);\n const bambooSinNomina = toNumber(row.bamboo_sin_nomina);\n\n const diferenciasNombreBanco = toNumber(row.metadata?.diferencias_nombre_banco ?? row.summary?.diferenciasNombreBanco ?? row.diferencias_nombre_banco);\n const posiblesCuentasMalDigitadas = toNumber(row.metadata?.posibles_cuentas_mal_digitadas ?? row.summary?.posiblesCuentasMalDigitadas ?? row.posibles_cuentas_mal_digitadas);\n const fallbackPendientes = discrepancias + bancoSinNomina + nominaSinCuenta + diferenciasNombreBanco + posiblesCuentasMalDigitadas;\n const savedPendientes = Number(row.metadata?.pendientes_cruce_principal ?? row.summary?.pendientes ?? row.pendientes_cruce_principal);\n const totalPendientes = Number.isFinite(savedPendientes) ? savedPendientes : fallbackPendientes;\n\n const estado = normalizeStatus(row.estado, totalPendientes);\n\n return {\n id: row.id,\n\n fecha: formatDate(row.period_end || row.ejecutado_en || row.created_at),\n fechaEjecucion: row.ejecutado_en || row.created_at,\n\n periodo: row.period_label || '',\n year: row.year,\n month: row.month,\n periodType: row.period_type,\n periodStart: row.period_start,\n periodEnd: row.period_end,\n\n archivoNomina: row.payroll_file_name || '',\n archivosBanco: row.bank_file_names || [],\n\n coincidencias: toNumber(row.coincidencias),\n discrepancias,\n bancoSinNomina,\n nominaSinCuenta,\n bancoSinBamboo,\n nameDifferences: diferenciasNombreBanco,\n diferenciasNombreBanco,\n possibleWrongAccounts: posiblesCuentasMalDigitadas,\n posiblesCuentasMalDigitadas,\n pendientes: totalPendientes,\n totalPending: totalPendientes,\n nominaSinBamboo,\n bambooSinNomina,\n totalPendientes,\n\n totalNomina: toNumber(row.total_nomina),\n totalBanco: toNumber(row.total_banco),\n diferenciaTotal: toNumber(row.diferencia_total),\n\n estado,\n estadoLabel: statusLabel(estado),\n\n reportUrl: row.report_url || '',\n spreadsheetId: row.spreadsheet_id || '',\n\n ejecutadoPorNombre: row.ejecutado_por_nombre || '',\n ejecutadoPorEmail: row.ejecutado_por_email || '',\n\n resueltoPorNombre: row.resuelto_por_nombre || '',\n resueltoPorEmail: row.resuelto_por_email || '',\n resueltoEn: row.resuelto_en || null,\n comentarioResolucion: row.comentario_resolucion || '',\n };\n});\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'historicos_rd_listos',\n count: reports.length,\n reports,\n },\n },\n];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
720,
|
||||||
|
272
|
||||||
|
],
|
||||||
|
"id": "bfa3bf09-7076-4322-9ea5-abde67baa069",
|
||||||
|
"name": "Normalizar históricos para app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"url": "https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes?select=*&country=eq.RD&order=ejecutado_en.desc&limit=100",
|
||||||
|
"sendHeaders": true,
|
||||||
|
"headerParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "apikey",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [
|
||||||
|
512,
|
||||||
|
272
|
||||||
|
],
|
||||||
|
"id": "6ca12435-bafc-44ab-ba07-08d6a1d72cf4",
|
||||||
|
"name": "Consultar históricos Supabase"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {
|
||||||
|
"responseHeaders": {
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"name": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Access-Control-Allow-Origin",
|
||||||
|
"value": "*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Access-Control-Allow-Methods",
|
||||||
|
"value": "GET,POST,OPTIONS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Access-Control-Allow-Headers",
|
||||||
|
"value": "Content-Type, Authorization"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.5,
|
||||||
|
"position": [
|
||||||
|
944,
|
||||||
|
272
|
||||||
|
],
|
||||||
|
"id": "c735659d-0fb1-452d-bb88-34d4076dbba9",
|
||||||
|
"name": "Responder históricos app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"content": "# 📚 HISTÓRICOS DE VERIFICACIÓN — REPÚBLICA DOMINICANA\n\nEntrega a la aplicación el historial de reportes procesados para República Dominicana.\n\n- Consulta únicamente registros con `country = RD`.\n- Supabase continúa usando la tabla histórica compartida `cruces_cuentas_gt_reportes`; el nombre de la tabla es legado y funciona como tabla multi-país.\n- Devuelve la misma estructura de históricos utilizada por los demás países.\n- Este webhook sirve como respaldo; la app intenta primero la RPC paginada de Supabase.",
|
||||||
|
"height": 496,
|
||||||
|
"width": 1360,
|
||||||
|
"color": 6
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.stickyNote",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"id": "b7b1d804-34f1-4da1-9cde-ac6aee727f02",
|
||||||
|
"name": "Sticky Note"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Consultar históricos Supabase": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Normalizar históricos para app",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Normalizar históricos para app": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Responder históricos app",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Webhook Históricos RD": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Consultar históricos Supabase",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"binaryMode": "separate"
|
||||||
|
},
|
||||||
|
"versionId": "47a1ec31-b680-45cf-98d5-9e9099779ccd",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
|
||||||
|
},
|
||||||
|
"id": "",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user