diff --git a/.env.example b/.env.example index 058403a..cf6d272 100644 --- a/.env.example +++ b/.env.example @@ -9,20 +9,24 @@ VITE_BASE_PATH="/cruce-cuentas/guatemala/" VITE_N8N_GUATEMALA_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominagt-bamboo-test" # Compatibilidad con instalaciones anteriores. VITE_N8N_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominagt-bamboo-test" +VITE_N8N_GUATEMALA_BONO14_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/bono14gt-bamboo-test" # Guatemala: históricos y resolución. VITE_HISTORICOS_GT_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-historicos" VITE_MARCAR_RESUELTO_GT_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-marcar-resuelto" # Trinidad y Tobago: completar cuando los workflows estén listos. -VITE_N8N_TRINIDAD_WEBHOOK_URL="" -VITE_HISTORICOS_TT_URL="" -VITE_MARCAR_RESUELTO_TT_URL="" +VITE_N8N_TRINIDAD_WEBHOOK_URL="https://agenteit.digitalcompass.agency/webhook/nominatt-bamboo-test" +VITE_HISTORICOS_TT_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-tt-historicos" +VITE_MARCAR_RESUELTO_TT_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-tt-marcar-resuelto" # Endpoint opcional de n8n + Gemini para analizar el nombre del archivo de nómina. # La app ya incluye detección local como respaldo. VITE_ANALIZAR_NOMINA_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-analizar-nomina" +# Reporte manual de falsos positivos Banco sin BambooHR -> RRHH. +VITE_REPORTAR_BAMBOO_URL="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-bamboo-correccion" + # Login Google vía Supabase. VITE_ENABLE_SUPABASE_AUTH="true" VITE_SUPABASE_URL="https://dbit.digitalcompass.agency" diff --git a/Flujo de n8n: Portal de Verificación de Nómina - Analizar Nómina con Gemini GT y TT.json b/Flujo de n8n: Portal de Verificación de Nómina - Analizar Nómina con Gemini GT y TT.json deleted file mode 100644 index 6491010..0000000 --- a/Flujo de n8n: Portal de Verificación de Nómina - Analizar Nómina con Gemini GT y TT.json +++ /dev/null @@ -1,280 +0,0 @@ -{ - "name": "Portal de Verificación de Nómina - Analizar Nómina con Gemini GT y TT", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "cruce-cuentas-analizar-nomina", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2.1, - "position": [ - -560, - 400 - ], - "id": "21d31196-75af-48c2-b9a2-6d79a53390a8", - "name": "Webhook - Analizar Nómina", - "webhookId": "bbf70625-109d-415b-ae60-755c317978ce" - }, - { - "parameters": { - "jsCode": "const input = $input.first().json || {};\n\nlet body = input.body ?? input;\n\nif (typeof body === 'string') {\n try {\n body = JSON.parse(body);\n } catch (error) {\n body = {};\n }\n}\n\nif (!body || typeof body !== 'object' || Array.isArray(body)) {\n body = {};\n}\n\nfunction clean(value) {\n return String(value ?? '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nconst fileName = clean(\n body.fileName ??\n body.filename ??\n body.file_name ??\n ''\n);\n\nconst country = clean(\n body.country ?? 'GT'\n).toUpperCase();\n\nconst supportedCountries = {\n GT: 'Guatemala',\n TT: 'Trinidad y Tobago',\n};\n\nconst countryName = clean(\n body.countryName ??\n body.country_name ??\n supportedCountries[country] ??\n ''\n);\n\nconst errors = [];\n\nif (!fileName) {\n errors.push(\n 'No se recibió el nombre del archivo de nómina.'\n );\n}\n\nif (!Object.prototype.hasOwnProperty.call(\n supportedCountries,\n country\n)) {\n errors.push(\n 'El analizador solo está habilitado para Guatemala y Trinidad y Tobago.'\n );\n}\n\nconst supportedExtensions = [\n '.xlsx',\n '.xls',\n '.xlsm',\n '.csv',\n];\n\nconst lowerFileName = fileName.toLowerCase();\n\nconst hasSupportedExtension =\n supportedExtensions.some((extension) =>\n lowerFileName.endsWith(extension)\n );\n\nif (fileName && !hasSupportedExtension) {\n errors.push(\n 'El archivo recibido no tiene una extensión de nómina compatible.'\n );\n}\n\nreturn [\n {\n json: {\n ok: errors.length === 0,\n errors,\n\n fileName,\n country,\n countryName:\n countryName ||\n supportedCountries[country] ||\n '',\n\n receivedAt: new Date().toISOString(),\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - -352, - 400 - ], - "id": "688ff563-63e0-47f5-a756-0c648d3af355", - "name": "Validar y preparar solicitud" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "bd9fd675-d72b-4dba-ac03-a44f1123ecb0", - "leftValue": "={{ $json.ok }}", - "rightValue": "", - "operator": { - "type": "boolean", - "operation": "true", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - -144, - 400 - ], - "id": "6f74ebb8-a476-4b69-bbca-dd298936bf53", - "name": "¿Solicitud válida?" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{\n{\n ok: false,\n message:\n 'No fue posible analizar el nombre del archivo.',\n errors: Array.isArray($json.errors)\n ? $json.errors\n : ['La solicitud recibida no es válida.'],\n detected: null\n}\n}}", - "options": { - "responseCode": 400, - "responseHeaders": { - "entries": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - } - } - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 64, - 496 - ], - "id": "c42f5bbb-858f-4bb6-a1d7-267679f4909b", - "name": "Responder error" - }, - { - "parameters": { - "text": "={{\n`Analiza este nombre de archivo de nómina:\n\nPaís seleccionado: ${$json.countryName}\nCódigo del país: ${$json.country}\nNombre del archivo: ${$json.fileName}`\n}}", - "schemaType": "manual", - "inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"country\": {\n \"type\": \"string\",\n \"enum\": [\n \"GT\",\n \"TT\"\n ]\n },\n \"year\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 2100\n },\n \"month\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 12\n },\n \"period_type\": {\n \"type\": \"string\",\n \"enum\": [\n \"quincena_15\",\n \"quincena_30\",\n \"mensual\",\n \"desconocido\"\n ]\n },\n \"confidence\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"explanation\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"country\",\n \"year\",\n \"month\",\n \"period_type\",\n \"confidence\",\n \"explanation\"\n ],\n \"additionalProperties\": false\n}", - "options": { - "systemPromptTemplate": "Eres un clasificador determinista de nombres de archivos de nómina.\n\nTu tarea es identificar el país seleccionado, el año, el mes y el período de pago utilizando únicamente la información presente en el nombre del archivo y el país recibido.\n\nNo inventes datos que no estén contenidos o razonablemente codificados en el nombre.\n\nREGLAS GENERALES:\n\n1. Devuelve exactamente el código de país recibido:\n - GT para Guatemala.\n - TT para Trinidad y Tobago.\n\n2. Detecta el año de cuatro dígitos. También puede aparecer dentro de una fecha compacta como YYYYMMDD; por ejemplo, 20260617 representa el año 2026.\n\n3. Convierte el mes a un número aceptando nombres en español o inglés:\n Enero / January = 1\n Febrero / February = 2\n Marzo / March = 3\n Abril / April = 4\n Mayo / May = 5\n Junio / June = 6\n Julio / July = 7\n Agosto / August = 8\n Septiembre / September = 9\n Octubre / October = 10\n Noviembre / November = 11\n Diciembre / December = 12\n\n4. Usa period_type = quincena_15 cuando aparezca cualquiera de estas señales:\n - 1Q\n - Q1\n - Primera quincena\n - 1ra quincena\n - First fortnight\n - First half\n - Mid month\n - Día 15\n - 15th\n\n5. Usa period_type = quincena_30 cuando aparezca cualquiera de estas señales:\n - 2Q\n - Q2\n - Segunda quincena\n - 2da quincena\n - Second fortnight\n - Second half\n - Fin de mes\n - End of month\n - Month end\n - Día 30\n - Día 31\n - 30th\n - 31st\n - El último día real del mes, incluyendo 28 o 29 de febrero\n\n6. Aunque el último día real del mes no sea 30, utiliza quincena_30 para representar la segunda quincena o cierre de mes.\n\n7. Si solamente aparecen el mes y el año, sin ninguna señal de quincena ni día de cierre, utiliza mensual.\n\n8. Para nombres como \"GLM_TT_Payroll June 30TH_20260617.xlsx\":\n - country = TT\n - year = 2026\n - month = 6\n - period_type = quincena_30\n\n9. Si no puedes identificar de manera confiable el año o el mes, utiliza:\n year = 0\n month = 0\n period_type = desconocido\n\n10. confidence debe estar entre 0 y 1.\n\n11. explanation debe ser breve y mencionar las señales encontradas en el nombre.\n\nNo agregues campos diferentes a los definidos por el esquema." - } - }, - "type": "@n8n/n8n-nodes-langchain.informationExtractor", - "typeVersion": 1.2, - "position": [ - 64, - 128 - ], - "id": "efe8a811-b868-4efb-8348-4bd0b378df4b", - "name": "Extraer período con Gemini", - "retryOnFail": true, - "waitBetweenTries": 2000, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "modelName": "models/gemini-2.5-pro", - "options": {} - }, - "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", - "typeVersion": 1.1, - "position": [ - 64, - 304 - ], - "id": "38ff566c-2b41-40aa-873a-65ff7eee4668", - "name": "Gemini - Analizar título nómina", - "credentials": { - "googlePalmApi": { - "id": "jvsXYwL6IOoY2DBU", - "name": "Isaac - Gemini Api Pago" - } - } - }, - { - "parameters": { - "jsCode": "const request =\n $('Validar y preparar solicitud').first().json || {};\n\nconst incoming = $input.first().json || {};\n\nfunction cleanText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeText(value) {\n return cleanText(value)\n .toUpperCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[_\\-.,()[\\]{}]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction parseObject(value) {\n if (\n value &&\n typeof value === 'object' &&\n !Array.isArray(value)\n ) {\n return value;\n }\n\n if (typeof value !== 'string') {\n return {};\n }\n\n const cleaned = value\n .replace(/```json/gi, '')\n .replace(/```/g, '')\n .trim();\n\n try {\n return JSON.parse(cleaned);\n } catch (error) {\n return {};\n }\n}\n\nfunction clamp(value, minimum, maximum) {\n const number = Number(value);\n\n if (!Number.isFinite(number)) {\n return minimum;\n }\n\n return Math.min(\n maximum,\n Math.max(minimum, number)\n );\n}\n\nfunction pad(number) {\n return String(number).padStart(2, '0');\n}\n\nfunction getLastDay(year, month) {\n return new Date(\n Date.UTC(year, month, 0)\n ).getUTCDate();\n}\n\nfunction buildPeriodDates(\n year,\n month,\n periodType\n) {\n if (\n !Number.isInteger(year) ||\n year < 2000 ||\n !Number.isInteger(month) ||\n month < 1 ||\n month > 12\n ) {\n return {\n periodStart: null,\n periodEnd: null,\n };\n }\n\n const lastDay = getLastDay(year, month);\n const yearMonth = `${year}-${pad(month)}`;\n\n if (periodType === 'quincena_15') {\n return {\n periodStart: `${yearMonth}-01`,\n periodEnd: `${yearMonth}-15`,\n };\n }\n\n if (periodType === 'quincena_30') {\n return {\n periodStart: `${yearMonth}-16`,\n periodEnd:\n `${yearMonth}-${pad(lastDay)}`,\n };\n }\n\n if (periodType === 'mensual') {\n return {\n periodStart: `${yearMonth}-01`,\n periodEnd:\n `${yearMonth}-${pad(lastDay)}`,\n };\n }\n\n return {\n periodStart: null,\n periodEnd: null,\n };\n}\n\nconst countryNames = {\n GT: 'Guatemala',\n TT: 'Trinidad y Tobago',\n};\n\nconst country = ['GT', 'TT'].includes(\n String(request.country || '').toUpperCase()\n)\n ? String(request.country).toUpperCase()\n : 'GT';\n\nconst countryName =\n request.countryName ||\n countryNames[country];\n\nconst monthNames = {\n 1: 'Enero',\n 2: 'Febrero',\n 3: 'Marzo',\n 4: 'Abril',\n 5: 'Mayo',\n 6: 'Junio',\n 7: 'Julio',\n 8: 'Agosto',\n 9: 'Septiembre',\n 10: 'Octubre',\n 11: 'Noviembre',\n 12: 'Diciembre',\n};\n\nconst monthPatterns = [\n ['ENERO', 1],\n ['JANUARY', 1],\n ['JAN', 1],\n\n ['FEBRERO', 2],\n ['FEBRUARY', 2],\n ['FEB', 2],\n\n ['MARZO', 3],\n ['MARCH', 3],\n ['MAR', 3],\n\n ['ABRIL', 4],\n ['APRIL', 4],\n ['APR', 4],\n\n ['MAYO', 5],\n ['MAY', 5],\n\n ['JUNIO', 6],\n ['JUNE', 6],\n ['JUN', 6],\n\n ['JULIO', 7],\n ['JULY', 7],\n ['JUL', 7],\n\n ['AGOSTO', 8],\n ['AUGUST', 8],\n ['AUG', 8],\n\n ['SEPTIEMBRE', 9],\n ['SETIEMBRE', 9],\n ['SEPTEMBER', 9],\n ['SEPT', 9],\n ['SEP', 9],\n\n ['OCTUBRE', 10],\n ['OCTOBER', 10],\n ['OCT', 10],\n\n ['NOVIEMBRE', 11],\n ['NOVEMBER', 11],\n ['NOV', 11],\n\n ['DICIEMBRE', 12],\n ['DECEMBER', 12],\n ['DEC', 12],\n];\n\nconst monthPatternText =\n monthPatterns\n .map(([name]) => name)\n .sort((a, b) => b.length - a.length)\n .join('|');\n\nconst validPeriodTypes = new Set([\n 'quincena_15',\n 'quincena_30',\n 'mensual',\n 'desconocido',\n]);\n\nconst fileName = cleanText(request.fileName);\nconst normalizedFileName =\n normalizeText(fileName);\n\nconst compactDateMatch =\n normalizedFileName.match(\n /\\b(20\\d{2})(0[1-9]|1[0-2])([0-2]\\d|3[01])\\b/\n );\n\nconst standaloneYearMatch =\n normalizedFileName.match(/\\b(20\\d{2})\\b/);\n\nconst fallbackYear = standaloneYearMatch\n ? Number(standaloneYearMatch[1])\n : compactDateMatch\n ? Number(compactDateMatch[1])\n : 0;\n\nlet fallbackMonth = 0;\n\nfor (const [monthName, monthNumber] of monthPatterns) {\n const monthRegex = new RegExp(\n `\\\\b${monthName}\\\\b`\n );\n\n if (monthRegex.test(normalizedFileName)) {\n fallbackMonth = monthNumber;\n break;\n }\n}\n\nif (!fallbackMonth && compactDateMatch) {\n fallbackMonth = Number(compactDateMatch[2]);\n}\n\nconst monthThenDayRegex = new RegExp(\n `\\\\b(?:${monthPatternText})\\\\s+([0-3]?\\\\d)(?:ST|ND|RD|TH)?\\\\b`\n);\n\nconst dayThenMonthRegex = new RegExp(\n `\\\\b([0-3]?\\\\d)(?:ST|ND|RD|TH)?\\\\s+(?:DE\\\\s+)?(?:${monthPatternText})\\\\b`\n);\n\nconst explicitDayRegex =\n /\\b(?:DIA|DAY)\\s+([0-3]?\\d)(?:ST|ND|RD|TH)?\\b/;\n\nconst monthThenDayMatch =\n normalizedFileName.match(monthThenDayRegex);\n\nconst dayThenMonthMatch =\n normalizedFileName.match(dayThenMonthRegex);\n\nconst explicitDayMatch =\n normalizedFileName.match(explicitDayRegex);\n\nconst detectedDay = monthThenDayMatch\n ? Number(monthThenDayMatch[1])\n : dayThenMonthMatch\n ? Number(dayThenMonthMatch[1])\n : explicitDayMatch\n ? Number(explicitDayMatch[1])\n : 0;\n\nconst firstFortnight =\n /\\b(1Q|Q1)\\b/.test(normalizedFileName) ||\n normalizedFileName.includes(\n 'PRIMERA QUINCENA'\n ) ||\n normalizedFileName.includes(\n '1RA QUINCENA'\n ) ||\n normalizedFileName.includes(\n 'FIRST FORTNIGHT'\n ) ||\n normalizedFileName.includes(\n 'FIRST HALF'\n ) ||\n normalizedFileName.includes(\n 'MID MONTH'\n ) ||\n normalizedFileName.includes(\n 'MIDMONTH'\n );\n\nconst secondFortnight =\n /\\b(2Q|Q2)\\b/.test(normalizedFileName) ||\n normalizedFileName.includes(\n 'SEGUNDA QUINCENA'\n ) ||\n normalizedFileName.includes(\n '2DA QUINCENA'\n ) ||\n normalizedFileName.includes(\n 'SECOND FORTNIGHT'\n ) ||\n normalizedFileName.includes(\n 'SECOND HALF'\n ) ||\n normalizedFileName.includes(\n 'FIN DE MES'\n ) ||\n normalizedFileName.includes(\n 'END OF MONTH'\n ) ||\n normalizedFileName.includes(\n 'MONTH END'\n ) ||\n normalizedFileName.includes(\n 'MONTHEND'\n );\n\nlet fallbackPeriodType = 'desconocido';\nlet fallbackConfidence = 0.2;\nlet strongPeriodEvidence = false;\n\nif (firstFortnight) {\n fallbackPeriodType = 'quincena_15';\n fallbackConfidence = 0.98;\n strongPeriodEvidence = true;\n} else if (secondFortnight) {\n fallbackPeriodType = 'quincena_30';\n fallbackConfidence = 0.98;\n strongPeriodEvidence = true;\n} else if (detectedDay === 15) {\n fallbackPeriodType = 'quincena_15';\n fallbackConfidence = 0.95;\n strongPeriodEvidence = true;\n} else if (\n fallbackYear &&\n fallbackMonth &&\n detectedDay === getLastDay(\n fallbackYear,\n fallbackMonth\n )\n) {\n fallbackPeriodType = 'quincena_30';\n fallbackConfidence = 0.95;\n strongPeriodEvidence = true;\n} else if (\n detectedDay === 30 ||\n detectedDay === 31\n) {\n fallbackPeriodType = 'quincena_30';\n fallbackConfidence = 0.94;\n strongPeriodEvidence = true;\n} else if (\n fallbackYear &&\n fallbackMonth\n) {\n fallbackPeriodType = 'mensual';\n fallbackConfidence = 0.75;\n}\n\nlet rawAI =\n incoming.output ??\n incoming.result ??\n incoming.text ??\n incoming;\n\nrawAI = parseObject(rawAI);\n\nconst aiCountry =\n String(rawAI.country || '').toUpperCase();\n\nconst aiYear = Number(rawAI.year);\nconst aiMonth = Number(rawAI.month);\n\nconst aiPeriodType =\n validPeriodTypes.has(rawAI.period_type)\n ? rawAI.period_type\n : 'desconocido';\n\nconst aiConfidence = clamp(\n rawAI.confidence,\n 0,\n 1\n);\n\nconst aiIsUsable =\n Number.isInteger(aiYear) &&\n aiYear >= 2000 &&\n aiYear <= 2100 &&\n Number.isInteger(aiMonth) &&\n aiMonth >= 1 &&\n aiMonth <= 12 &&\n aiPeriodType !== 'desconocido';\n\nlet year =\n fallbackYear ||\n (aiIsUsable ? aiYear : 0);\n\nlet month =\n fallbackMonth ||\n (aiIsUsable ? aiMonth : 0);\n\nlet periodType =\n aiIsUsable\n ? aiPeriodType\n : fallbackPeriodType;\n\nlet source =\n aiIsUsable\n ? 'gemini'\n : 'local_fallback';\n\nlet confidence =\n aiIsUsable\n ? aiConfidence\n : fallbackConfidence;\n\nlet conflictResolved = false;\n\nif (\n aiCountry &&\n aiCountry !== country\n) {\n source = 'gemini_validated_with_rules';\n conflictResolved = true;\n}\n\nif (\n strongPeriodEvidence &&\n periodType !== fallbackPeriodType\n) {\n periodType = fallbackPeriodType;\n source = 'gemini_validated_with_rules';\n confidence = Math.max(\n fallbackConfidence,\n Math.min(aiConfidence, 0.9)\n );\n conflictResolved = true;\n}\n\nif (\n fallbackYear &&\n aiIsUsable &&\n aiYear !== fallbackYear\n) {\n year = fallbackYear;\n source = 'gemini_validated_with_rules';\n conflictResolved = true;\n}\n\nif (\n fallbackMonth &&\n aiIsUsable &&\n aiMonth !== fallbackMonth\n) {\n month = fallbackMonth;\n source = 'gemini_validated_with_rules';\n conflictResolved = true;\n}\n\nif (\n !aiIsUsable &&\n fallbackYear &&\n fallbackMonth\n) {\n year = fallbackYear;\n month = fallbackMonth;\n periodType = fallbackPeriodType;\n confidence = fallbackConfidence;\n source = 'local_fallback';\n}\n\nconst complete =\n year >= 2000 &&\n month >= 1 &&\n month <= 12 &&\n periodType !== 'desconocido';\n\nconst dates = buildPeriodDates(\n year,\n month,\n periodType\n);\n\nlet periodDescription = 'Período desconocido';\n\nif (periodType === 'quincena_15') {\n periodDescription = 'Quincena 15';\n}\n\nif (periodType === 'quincena_30') {\n periodDescription =\n 'Quincena 30 / fin de mes';\n}\n\nif (periodType === 'mensual') {\n periodDescription = 'Mensual';\n}\n\nconst monthLabel =\n monthNames[month] || 'Mes desconocido';\n\nconst periodLabel = complete\n ? `${monthLabel} ${year} · ${periodDescription}`\n : 'Período pendiente de confirmar';\n\nconst requiresManualReview =\n !complete ||\n confidence < 0.75;\n\nconst explanation =\n cleanText(rawAI.explanation) ||\n (\n source === 'local_fallback'\n ? 'El período fue identificado mediante las reglas locales de respaldo.'\n : 'El período fue identificado mediante Gemini y validado con las reglas del portal.'\n );\n\nreturn [\n {\n json: {\n ok: true,\n\n message: requiresManualReview\n ? 'El archivo fue analizado, pero el período debe confirmarse manualmente.'\n : 'El período de la nómina fue detectado correctamente.',\n\n detected: {\n country,\n countryName,\n\n fileName,\n\n year,\n month,\n\n periodType,\n period_type: periodType,\n\n periodLabel,\n period_label: periodLabel,\n\n periodStart: dates.periodStart,\n period_start: dates.periodStart,\n\n periodEnd: dates.periodEnd,\n period_end: dates.periodEnd,\n\n confidence:\n Math.round(confidence * 100) / 100,\n\n complete,\n requiresManualReview,\n requires_manual_review:\n requiresManualReview,\n\n source,\n conflictResolved,\n explanation,\n },\n\n debug: {\n fallback: {\n country,\n year: fallbackYear,\n month: fallbackMonth,\n periodType: fallbackPeriodType,\n detectedDay,\n confidence: fallbackConfidence,\n strongPeriodEvidence,\n compactDate:\n compactDateMatch?.[0] || null,\n },\n\n gemini: {\n country: aiCountry || null,\n year: aiYear || 0,\n month: aiMonth || 0,\n periodType: aiPeriodType,\n confidence: aiConfidence,\n explanation:\n cleanText(rawAI.explanation),\n },\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 400, - 144 - ], - "id": "3d608f18-2ed8-47a3-914c-c2494dfea8f7", - "name": "Normalizar análisis IA" - }, - { - "parameters": { - "options": { - "responseCode": 200, - "responseHeaders": { - "entries": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - } - } - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 608, - 144 - ], - "id": "b0843a40-9e7d-400f-b265-d563b789786f", - "name": "Responder análisis" - }, - { - "parameters": { - "content": "# 🧠 ANÁLISIS AUTOMÁTICO DE NÓMINA — GT Y TT\n\nAnaliza el nombre o título del archivo de nómina para identificar automáticamente el período correspondiente antes de ejecutar la verificación.\n\n## Recepción y validación\n\nEl Webhook recibe desde el Portal de Verificación de Nóminas la información del archivo seleccionado.\n\nLa solicitud puede incluir:\n\n- Nombre del archivo.\n- Título o descripción disponible.\n- País o portal de origen.\n- Identificador de la solicitud.\n- Información complementaria enviada por la aplicación.\n\nAntes de utilizar inteligencia artificial, el flujo valida que exista suficiente información para analizar el archivo.\n\n## Extracción con Gemini\n\nCuando la solicitud es válida, Gemini interpreta el nombre o título de la nómina e intenta determinar:\n\n- Año.\n- Mes.\n- Tipo de período.\n- Quincena 15.\n- Quincena 30 o fin de mes.\n- Nómina mensual.\n- País, cuando puede inferirse de forma confiable.\n- Nivel de confianza del análisis.\n\nGemini se utiliza únicamente para interpretar el texto recibido; no ejecuta el cruce de nómina ni modifica información.\n\n## Normalización del resultado\n\nLa respuesta de Gemini se transforma al formato requerido por la aplicación.\n\nEste bloque:\n\n- Limpia texto adicional generado por la IA.\n- Normaliza el año a formato numérico.\n- Convierte el mes al valor esperado por el portal.\n- Estandariza el tipo de período.\n- Corrige variaciones como “segunda quincena”, “fin de mes” o “30”.\n- Conserva los valores originales cuando la IA no puede determinar un dato.\n- Prepara una respuesta consistente para Guatemala y Trinidad y Tobago.\n\n## Respuesta al portal\n\nCuando el análisis termina, el workflow devuelve a la aplicación:\n\n- Indicador de éxito.\n- Año detectado.\n- Mes detectado.\n- Tipo de período detectado.\n- País identificado, cuando corresponda.\n- Nivel de confianza.\n- Datos que requieren revisión manual.\n\nLos valores detectados sirven para completar automáticamente el formulario, pero el usuario puede corregirlos antes de ejecutar el proceso.\n\n## Ruta de error\n\nCuando la solicitud es inválida:\n\n- No se llama a Gemini.\n- No se intenta interpretar información incompleta.\n- Se devuelve una respuesta de error al portal.\n- Se indica que falta el nombre del archivo u otro dato obligatorio.\n\n## Reglas\n\n- No inventar un período cuando el título no contiene información suficiente.\n- No ejecutar el cruce de nómina desde este workflow.\n- No tratar una predicción de baja confianza como un dato definitivo.\n- Mantener la opción de corrección manual en la aplicación.\n- Responder siempre al Webhook, tanto en la ruta válida como en la ruta de error.\n- Utilizar una estructura de respuesta compatible con los portales de Guatemala y Trinidad y Tobago.", - "height": 1488, - "width": 2256, - "color": 2 - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - -1424, - -416 - ], - "id": "27e40ac1-a1eb-4ce5-ad0f-919b81b39dfb", - "name": "Sticky Note" - } - ], - "pinData": {}, - "connections": { - "Webhook - Analizar Nómina": { - "main": [ - [ - { - "node": "Validar y preparar solicitud", - "type": "main", - "index": 0 - } - ] - ] - }, - "Validar y preparar solicitud": { - "main": [ - [ - { - "node": "¿Solicitud válida?", - "type": "main", - "index": 0 - } - ] - ] - }, - "¿Solicitud válida?": { - "main": [ - [ - { - "node": "Extraer período con Gemini", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Responder error", - "type": "main", - "index": 0 - } - ] - ] - }, - "Gemini - Analizar título nómina": { - "ai_languageModel": [ - [ - { - "node": "Extraer período con Gemini", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - }, - "Extraer período con Gemini": { - "main": [ - [ - { - "node": "Normalizar análisis IA", - "type": "main", - "index": 0 - } - ] - ] - }, - "Normalizar análisis IA": { - "main": [ - [ - { - "node": "Responder análisis", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1", - "binaryMode": "separate", - "availableInMCP": true, - "timeSavedMode": "fixed", - "errorWorkflow": "puF4LUczoSz3hcek", - "timezone": "America/Santo_Domingo", - "callerPolicy": "workflowsFromSameOwner" - }, - "versionId": "1aba63da-c451-45a3-ab57-a88ac4bd4ea6", - "meta": { - "templateCredsSetupCompleted": true, - "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" - }, - "id": "E9cNPlUjeGnhFqW2", - "tags": [] -} \ No newline at end of file diff --git a/Flujo de n8n: Portal de Verificación de Nómina - GT.json b/Flujo de n8n: Portal de Verificación de Nómina - GT.json deleted file mode 100644 index 5752368..0000000 --- a/Flujo de n8n: Portal de Verificación de Nómina - GT.json +++ /dev/null @@ -1,1750 +0,0 @@ -{ - "name": "Portal de Verificación de Nómina - GT", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "nominagt-bamboo-test", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2.1, - "position": [ - 14768, - 25968 - ], - "id": "bbb096bb-6f2c-48df-907c-06c9857943ac", - "name": "Webhook", - "webhookId": "4061b0e1-0d8e-4fb4-bea8-790c718447ee" - }, - { - "parameters": { - "jsCode": "const item = $input.first();\n\nconst body = item.json.body || {};\nconst binary = item.binary || {};\n\nlet metadata = {};\n\ntry {\n metadata = typeof body.metadata === 'string'\n ? JSON.parse(body.metadata)\n : body.metadata || {};\n} catch (error) {\n metadata = {};\n}\n\nconst binaryKeys = Object.keys(binary);\n\nconst payrollKey = binaryKeys.find((key) => key === 'payroll_file');\nconst bankKeys = binaryKeys.filter((key) => key.startsWith('bank_files'));\n\nconst payrollFile = payrollKey\n ? {\n binary_key: payrollKey,\n file_name: binary[payrollKey].fileName,\n file_extension: binary[payrollKey].fileExtension,\n mime_type: binary[payrollKey].mimeType,\n file_size: binary[payrollKey].fileSize,\n }\n : null;\n\nconst bankFiles = bankKeys.map((key) => ({\n binary_key: key,\n file_name: binary[key].fileName,\n file_extension: binary[key].fileExtension,\n mime_type: binary[key].mimeType,\n file_size: binary[key].fileSize,\n}));\n\nconst errors = [];\n\nif (!metadata.country || metadata.country !== 'GT') {\n errors.push('El país recibido no es Guatemala.');\n}\n\nif (!metadata.year) {\n errors.push('No se recibió el año del cruce.');\n}\n\nif (!metadata.month) {\n errors.push('No se recibió el mes del cruce.');\n}\n\nif (!metadata.period_type) {\n errors.push('No se recibió el tipo de quincena.');\n}\n\nif (!metadata.period_start || !metadata.period_end) {\n errors.push('No se recibió el período calculado.');\n}\n\nif (!payrollFile) {\n errors.push('No se recibió el archivo de nómina.');\n}\n\nif (bankFiles.length === 0) {\n errors.push('No se recibió ningún archivo CSV del banco.');\n}\n\nreturn [\n {\n json: {\n ok: errors.length === 0,\n stage: 'entrada_recibida',\n errors,\n metadata,\n payroll_file: payrollFile,\n bank_files: bankFiles,\n summary: {\n payroll_files_count: payrollFile ? 1 : 0,\n bank_files_count: bankFiles.length,\n },\n },\n binary,\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 14976, - 25968 - ], - "id": "756188b6-dc61-4e3a-965a-47885e847e69", - "name": "Preparar entrada app" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{\n(() => {\n const data = $json || {};\n\n const original =\n data.originalResponse ||\n data.original_response ||\n data.response ||\n data.cruceResponse ||\n data.cruce_response ||\n data;\n\n const summary = original.summary || data.summary || {};\n const rows = original.rows || data.rows || [];\n const bankWithoutBamboo =\n original.bankWithoutBamboo ||\n data.bankWithoutBamboo ||\n [];\n const bambooSummary =\n original.bambooSummary ||\n data.bambooSummary ||\n {};\n\n const reportUrl =\n data.reportUrl ||\n data.report_url ||\n data.googleSheetUrl ||\n data.google_sheet_url ||\n data.spreadsheetUrl ||\n data.spreadsheet_url ||\n original.reportUrl ||\n original.report_url ||\n null;\n\n return {\n ok: original.ok ?? data.ok ?? true,\n message: reportUrl\n ? 'Cruce procesado correctamente. Google Sheet generado.'\n : 'Cruce procesado correctamente.',\n stage: reportUrl ? 'cruce_completado_con_reporte' : 'cruce_completado',\n errors: original.errors || data.errors || [],\n metadata: original.metadata || data.metadata || {},\n summary,\n rows,\n bankWithoutBamboo,\n bambooSummary,\n reportUrl,\n debug: {\n source_stage: data.stage || null,\n rows_returned:\n Array.isArray(rows) ? rows.length : 0,\n banco_sin_bamboo_rows:\n Array.isArray(bankWithoutBamboo)\n ? bankWithoutBamboo.length\n : 0,\n report_url_found: Boolean(reportUrl),\n },\n };\n})()\n}}", - "options": { - "responseCode": 200, - "responseHeaders": { - "entries": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - } - } - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 25056, - 26528 - ], - "id": "cfa673b1-08ba-41c0-b06c-15189672e1f2", - "name": "Respond to Webhook" - }, - { - "parameters": { - "jsCode": "const input = $input.first();\nconst json = input.json || {};\nconst binary = input.binary || {};\n\nfunction parseCsvLine(line) {\n const result = [];\n let current = '';\n let insideQuotes = false;\n\n for (let i = 0; i < line.length; i++) {\n const char = line[i];\n const nextChar = line[i + 1];\n\n if (char === '\"' && insideQuotes && nextChar === '\"') {\n current += '\"';\n i += 1;\n continue;\n }\n\n if (char === '\"') {\n insideQuotes = !insideQuotes;\n continue;\n }\n\n if (char === ',' && !insideQuotes) {\n result.push(current.trim());\n current = '';\n continue;\n }\n\n current += char;\n }\n\n result.push(current.trim());\n return result;\n}\n\nfunction normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeForCompare(value) {\n return normalizeText(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeAccount(value) {\n return String(value ?? '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim();\n}\n\nfunction isValidAccount(value) {\n const account = normalizeAccount(value);\n return account.length >= 7 && !/^0+$/.test(account);\n}\n\nfunction parseMoney(value) {\n const raw = String(value ?? '');\n const cleaned = raw\n .replace(/USD/gi, '')\n .replace(/GTQ/gi, '')\n .replace(/QTZ/gi, '')\n .replace(/Q/gi, '')\n .replace(/,/g, '')\n .replace(/\\s+/g, '')\n .trim();\n\n const parsed = Number.parseFloat(cleaned);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction detectCurrency(value, concept) {\n const combined = `${value ?? ''} ${concept ?? ''}`.toUpperCase();\n return combined.includes('USD') ? 'USD' : 'QTZ';\n}\n\nfunction roundMoney(value) {\n return Math.round((Number(value) || 0) * 100) / 100;\n}\n\nfunction getColumnIndex(headers, expectedNames) {\n const normalizedHeaders = headers.map((header) => normalizeForCompare(header));\n\n for (const expected of expectedNames) {\n const normalizedExpected = normalizeForCompare(expected);\n const exact = normalizedHeaders.findIndex((header) => header === normalizedExpected);\n if (exact >= 0) return exact;\n }\n\n for (const expected of expectedNames) {\n const normalizedExpected = normalizeForCompare(expected);\n const partial = normalizedHeaders.findIndex((header) => header.includes(normalizedExpected));\n if (partial >= 0) return partial;\n }\n\n return -1;\n}\n\nfunction extractShipmentNumber(lines) {\n for (const line of lines.slice(0, 30)) {\n const normalized = normalizeForCompare(line);\n const match = normalized.match(/(?:detalle del envio|numero de envio)[^0-9]{0,30}(\\d{1,20})/);\n if (match?.[1]) return match[1];\n }\n\n return '';\n}\n\nfunction extractPlanNumber(lines) {\n for (const line of lines.slice(0, 30)) {\n const normalized = normalizeForCompare(line);\n const match = normalized.match(/(?:numero de plan|plan)[^a-z0-9]{0,20}([a-z0-9-]{2,30})/);\n if (match?.[1]) return match[1].toUpperCase();\n }\n\n return '';\n}\n\nfunction getNameWords(value) {\n const ignored = new Set(['de', 'del', 'la', 'las', 'los', 'y', 'e', 'el']);\n return normalizeForCompare(value)\n .split(' ')\n .filter((word) => word.length > 1 && !ignored.has(word));\n}\n\nfunction editDistance(a, b) {\n if (a === b) return 0;\n if (!a) return b.length;\n if (!b) return a.length;\n\n const previous = Array.from({ length: b.length + 1 }, (_, index) => index);\n\n for (let i = 1; i <= a.length; i++) {\n const current = [i];\n\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n current[j] = Math.min(\n current[j - 1] + 1,\n previous[j] + 1,\n previous[j - 1] + cost\n );\n }\n\n for (let j = 0; j < current.length; j++) previous[j] = current[j];\n }\n\n return previous[b.length];\n}\n\nfunction tokenMatches(a, b) {\n if (a === b) return true;\n const minLength = Math.min(a.length, b.length);\n if (minLength >= 8 && editDistance(a, b) <= 2) return true;\n if (minLength >= 5 && editDistance(a, b) <= 1) return true;\n return false;\n}\n\nfunction samePersonName(a, b) {\n const normalizedA = normalizeForCompare(a);\n const normalizedB = normalizeForCompare(b);\n\n if (!normalizedA || !normalizedB) return false;\n if (normalizedA === normalizedB) return true;\n\n const wordsA = getNameWords(a);\n const wordsB = getNameWords(b);\n if (!wordsA.length || !wordsB.length) return false;\n\n const usedB = new Set();\n let matches = 0;\n\n for (const wordA of wordsA) {\n const matchIndex = wordsB.findIndex((wordB, index) => {\n return !usedB.has(index) && tokenMatches(wordA, wordB);\n });\n\n if (matchIndex >= 0) {\n usedB.add(matchIndex);\n matches += 1;\n }\n }\n\n const smallerLength = Math.min(wordsA.length, wordsB.length);\n const ratio = matches / smallerLength;\n\n if (smallerLength <= 2) return matches === smallerLength && matches >= 2;\n return matches >= 2 && ratio >= 0.6;\n}\n\nconst bankKeys = Object.keys(binary).filter((key) => key.startsWith('bank_files'));\nconst allBankRows = [];\nconst fileSummaries = [];\nconst nameDifferences = [];\n\nfor (const key of bankKeys) {\n const file = binary[key];\n const buffer = await this.helpers.getBinaryDataBuffer(0, key);\n const text = buffer.toString('latin1');\n\n const lines = text\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n\n const shipmentNumber = extractShipmentNumber(lines);\n const planNumber = extractPlanNumber(lines);\n\n const markerIndex = lines.findIndex((line) =>\n normalizeForCompare(line).includes('transacciones del envio')\n );\n\n if (markerIndex === -1) {\n fileSummaries.push({\n file_name: file.fileName,\n shipment_number: shipmentNumber,\n plan_number: planNumber,\n ok: false,\n rows_count: 0,\n total_amount: 0,\n error: 'No se encontró el bloque \"Transacciones del envío\".',\n });\n continue;\n }\n\n const headerIndex = lines.findIndex((line, index) => {\n if (index <= markerIndex) return false;\n const normalized = normalizeForCompare(line);\n return normalized.includes('cuenta destino') && normalized.includes('monto');\n });\n\n if (headerIndex === -1) {\n fileSummaries.push({\n file_name: file.fileName,\n shipment_number: shipmentNumber,\n plan_number: planNumber,\n ok: false,\n rows_count: 0,\n total_amount: 0,\n error: 'No se encontró el encabezado de transacciones.',\n });\n continue;\n }\n\n const headers = parseCsvLine(lines[headerIndex]).map(normalizeText);\n\n const idxCuentaDestino = getColumnIndex(headers, ['Cuenta Destino']);\n const idxNombreArchivo = getColumnIndex(headers, ['Nombre en Archivo']);\n const idxNombreCuentahabiente = getColumnIndex(headers, ['Nombre del Cuentahabiente']);\n const idxMonto = getColumnIndex(headers, ['Monto']);\n const idxConcepto = getColumnIndex(headers, ['Concepto']);\n const idxEstado = getColumnIndex(headers, ['Estado']);\n const idxReferencia = getColumnIndex(headers, ['Referencia']);\n const idxNumeroEnvio = getColumnIndex(headers, ['Número de envío', 'Numero de envio']);\n const idxNumeroPlan = getColumnIndex(headers, ['Número de plan', 'Numero de plan']);\n\n const rowsFromFile = [];\n\n for (let i = headerIndex + 1; i < lines.length; i++) {\n const values = parseCsvLine(lines[i]);\n\n const rawAccount = idxCuentaDestino >= 0 ? values[idxCuentaDestino] : '';\n const reference = normalizeText(idxReferencia >= 0 ? values[idxReferencia] : '');\n const referenceDigits = normalizeAccount(reference);\n const account = normalizeAccount(rawAccount);\n const amountRaw = idxMonto >= 0 ? values[idxMonto] : '';\n const amount = roundMoney(parseMoney(amountRaw));\n const concept = normalizeText(idxConcepto >= 0 ? values[idxConcepto] : '');\n\n if (amount <= 0) continue;\n\n const bankNameFile = normalizeText(idxNombreArchivo >= 0 ? values[idxNombreArchivo] : '');\n const bankAccountHolder = normalizeText(\n idxNombreCuentahabiente >= 0 ? values[idxNombreCuentahabiente] : ''\n );\n\n const validAccount = isValidAccount(account);\n const fallbackReference = isValidAccount(referenceDigits) ? referenceDigits : '';\n const displayAccount = validAccount ? account : fallbackReference;\n const currency = detectCurrency(amountRaw, concept);\n\n const rowShipment = normalizeText(idxNumeroEnvio >= 0 ? values[idxNumeroEnvio] : '') || shipmentNumber;\n const rowPlan = normalizeText(idxNumeroPlan >= 0 ? values[idxNumeroPlan] : '') || planNumber;\n\n const groupKey = validAccount\n ? `ACCOUNT:${account}:${currency}`\n : fallbackReference\n ? `REFERENCE:${fallbackReference}:${currency}`\n : `ROW:${file.fileName}:${i + 1}:${currency}`;\n\n const row = {\n source_file: file.fileName,\n row_number: i + 1,\n group_key: groupKey,\n account: displayAccount,\n raw_account: account,\n account_is_valid: validAccount,\n reference,\n shipment_number: rowShipment,\n plan_number: rowPlan,\n bank_name_file: bankNameFile,\n bank_account_holder: bankAccountHolder,\n amount,\n currency,\n concept,\n status: normalizeText(idxEstado >= 0 ? values[idxEstado] : ''),\n };\n\n rowsFromFile.push(row);\n allBankRows.push(row);\n\n if (\n bankNameFile &&\n bankAccountHolder &&\n !samePersonName(bankNameFile, bankAccountHolder)\n ) {\n nameDifferences.push({\n id: `bank_name_difference_${file.fileName}_${i + 1}`,\n source_file: file.fileName,\n row_number: i + 1,\n shipment_number: rowShipment,\n plan_number: rowPlan,\n account: displayAccount,\n reference,\n bank_name_file: bankNameFile,\n bank_account_holder: bankAccountHolder,\n amount,\n currency,\n status: 'Pendiente revisión',\n category: 'diferencia_nombre_banco',\n observation: `El Nombre en Archivo (${bankNameFile}) no coincide con el Nombre del Cuentahabiente (${bankAccountHolder}).`,\n });\n }\n }\n\n const fileTotal = roundMoney(rowsFromFile.reduce((sum, row) => sum + row.amount, 0));\n\n fileSummaries.push({\n file_name: file.fileName,\n shipment_number: shipmentNumber,\n plan_number: planNumber,\n ok: true,\n rows_count: rowsFromFile.length,\n total_amount: fileTotal,\n name_differences_count: nameDifferences.filter((row) => row.source_file === file.fileName).length,\n error: null,\n });\n}\n\nconst groupedMap = new Map();\n\nfor (const row of allBankRows) {\n const current = groupedMap.get(row.group_key) || {\n group_key: row.group_key,\n account: row.account,\n raw_account: row.raw_account,\n account_is_valid: row.account_is_valid,\n amount: 0,\n currency: row.currency,\n transactions_count: 0,\n bank_name_files: new Set(),\n bank_account_holders: new Set(),\n source_files: new Set(),\n shipment_numbers: new Set(),\n plan_numbers: new Set(),\n source_rows: [],\n };\n\n current.amount = roundMoney(current.amount + row.amount);\n current.transactions_count += 1;\n if (row.bank_name_file) current.bank_name_files.add(row.bank_name_file);\n if (row.bank_account_holder) current.bank_account_holders.add(row.bank_account_holder);\n if (row.source_file) current.source_files.add(row.source_file);\n if (row.shipment_number) current.shipment_numbers.add(row.shipment_number);\n if (row.plan_number) current.plan_numbers.add(row.plan_number);\n current.source_rows.push(row);\n\n groupedMap.set(row.group_key, current);\n}\n\nconst groupedByAccount = Array.from(groupedMap.values()).map((row) => {\n const bankNameFiles = Array.from(row.bank_name_files);\n const bankAccountHolders = Array.from(row.bank_account_holders);\n\n return {\n ...row,\n bank_name_file: bankNameFiles[0] || '',\n bank_account_holder: bankAccountHolders[0] || '',\n bank_name_files: bankNameFiles,\n bank_account_holders: bankAccountHolders,\n source_files: Array.from(row.source_files),\n shipment_numbers: Array.from(row.shipment_numbers),\n plan_numbers: Array.from(row.plan_numbers),\n };\n});\n\nconst totalsByCurrency = {};\nfor (const row of allBankRows) {\n totalsByCurrency[row.currency] = roundMoney((totalsByCurrency[row.currency] || 0) + row.amount);\n}\n\nconst bankTotal = roundMoney(allBankRows.reduce((sum, row) => sum + row.amount, 0));\n\nreturn [\n {\n json: {\n ...json,\n stage: 'banco_parseado',\n bank: {\n files_count: bankKeys.length,\n valid_files_count: fileSummaries.filter((file) => file.ok).length,\n rows_count: allBankRows.length,\n grouped_accounts_count: groupedByAccount.length,\n total_amount: bankTotal,\n totals_by_currency: totalsByCurrency,\n name_differences_count: nameDifferences.length,\n name_differences: nameDifferences,\n file_summaries: fileSummaries,\n rows: allBankRows,\n grouped_by_account: groupedByAccount,\n },\n },\n binary,\n },\n];\n" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 15648, - 25152 - ], - "id": "65e7f1a9-ac7a-46b5-8f9d-6729c5c80a31", - "name": "Parsear CSV banco GT" - }, - { - "parameters": { - "mode": "combine", - "combineBy": "combineByPosition", - "options": {} - }, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 21264, - 26512 - ], - "id": "c47dde0f-72aa-49b6-b58b-f7051df5df62", - "name": "Merge" - }, - { - "parameters": { - "jsCode": "const data = $input.first().json || {};\n\nfunction roundMoney(value) {\n return Math.round((Number(value) || 0) * 100) / 100;\n}\n\nfunction moneyDiff(a, b) {\n return roundMoney((Number(a) || 0) - (Number(b) || 0));\n}\n\nfunction moneyEquals(a, b, tolerance = 0.02) {\n return Math.abs(roundMoney(a) - roundMoney(b)) <= tolerance;\n}\n\nfunction normalizeAccount(value) {\n return String(value ?? '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim();\n}\n\nfunction normalizeName(value) {\n return String(value ?? '')\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction nameWords(value) {\n const ignored = new Set(['de', 'del', 'la', 'las', 'los', 'y', 'e', 'el']);\n return normalizeName(value)\n .split(' ')\n .filter((word) => word.length > 1 && !ignored.has(word));\n}\n\nfunction editDistance(a, b) {\n if (a === b) return 0;\n if (!a) return b.length;\n if (!b) return a.length;\n\n const previous = Array.from({ length: b.length + 1 }, (_, index) => index);\n\n for (let i = 1; i <= a.length; i++) {\n const current = [i];\n\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n\n current[j] = Math.min(\n current[j - 1] + 1,\n previous[j] + 1,\n previous[j - 1] + cost\n );\n }\n\n for (let j = 0; j < current.length; j++) {\n previous[j] = current[j];\n }\n }\n\n return previous[b.length];\n}\n\nfunction tokenMatches(a, b) {\n if (a === b) return true;\n\n const minLength = Math.min(a.length, b.length);\n\n if (minLength >= 8 && editDistance(a, b) <= 2) return true;\n if (minLength >= 5 && editDistance(a, b) <= 1) return true;\n\n return false;\n}\n\nfunction samePersonName(a, b) {\n const normalizedA = normalizeName(a);\n const normalizedB = normalizeName(b);\n\n if (!normalizedA || !normalizedB) return false;\n if (normalizedA === normalizedB) return true;\n\n const wordsA = nameWords(a);\n const wordsB = nameWords(b);\n\n if (!wordsA.length || !wordsB.length) return false;\n\n const usedB = new Set();\n let matches = 0;\n\n for (const wordA of wordsA) {\n const matchIndex = wordsB.findIndex((wordB, index) => {\n return !usedB.has(index) && tokenMatches(wordA, wordB);\n });\n\n if (matchIndex >= 0) {\n usedB.add(matchIndex);\n matches += 1;\n }\n }\n\n const smallerLength = Math.min(wordsA.length, wordsB.length);\n const ratio = matches / smallerLength;\n\n if (smallerLength <= 2) {\n return matches === smallerLength && matches >= 2;\n }\n\n return matches >= 2 && ratio >= 0.6;\n}\n\nfunction accountDistance(a, b) {\n return editDistance(normalizeAccount(a), normalizeAccount(b));\n}\n\nfunction accountRelationship(payrollAccount, bankAccount) {\n const payroll = normalizeAccount(payrollAccount);\n const bank = normalizeAccount(bankAccount);\n\n if (!payroll || !bank) {\n return { matches: false, type: 'none' };\n }\n\n if (payroll === bank) {\n return { matches: true, type: 'exact' };\n }\n\n const bankHasPayrollSuffix =\n bank.endsWith(payroll) &&\n bank.length > payroll.length &&\n bank.length - payroll.length <= 6;\n\n const payrollHasBankSuffix =\n payroll.endsWith(bank) &&\n payroll.length > bank.length &&\n payroll.length - bank.length <= 6;\n\n if (bankHasPayrollSuffix || payrollHasBankSuffix) {\n return { matches: true, type: 'reference_prefix' };\n }\n\n return { matches: false, type: 'none' };\n}\n\nfunction formatMoney(value) {\n return Math.abs(roundMoney(value)).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n}\n\nfunction bankNames(bank) {\n return Array.from(new Set([\n ...(Array.isArray(bank.bank_name_files) ? bank.bank_name_files : []),\n ...(Array.isArray(bank.bank_account_holders) ? bank.bank_account_holders : []),\n bank.bank_name_file || '',\n bank.bank_account_holder || '',\n ].filter(Boolean)));\n}\n\nfunction bankMatchesName(bank, payrollName) {\n return bankNames(bank).some((name) => samePersonName(payrollName, name));\n}\n\nfunction bestBankDisplayName(bank) {\n return (\n bank.bank_name_file ||\n bank.bank_account_holder ||\n bankNames(bank)[0] ||\n ''\n );\n}\n\n\nfunction bambooAliases(employee) {\n return Array.from(new Set([\n ...(Array.isArray(employee.aliases) ? employee.aliases : []),\n employee.full_name || '',\n [\n employee.first_name,\n employee.middle_name,\n employee.last_name,\n ].filter(Boolean).join(' '),\n [\n employee.preferred_name,\n employee.last_name,\n ].filter(Boolean).join(' '),\n [\n employee.first_name,\n employee.last_name,\n ].filter(Boolean).join(' '),\n ].map((value) =>\n String(value || '').trim()\n ).filter(Boolean)));\n}\n\nfunction bambooEmployeeNumber(employee) {\n return normalizeAccount(\n employee.employee_number ||\n employee.employeeNumber ||\n ''\n );\n}\n\nfunction bankRowKey(row) {\n return [\n row.source_file || '',\n row.row_number || '',\n ].join('|');\n}\n\nfunction bankRowNames(row) {\n const rowKey = bankRowKey(row);\n\n const linkedPayrollNames =\n typeof linkedPayrollNamesByBankRow !== 'undefined'\n ? linkedPayrollNamesByBankRow.get(rowKey) || []\n : [];\n\n return Array.from(new Set([\n row.bank_name_file || '',\n row.bank_account_holder || '',\n row.participant_name || '',\n ...linkedPayrollNames,\n ].map((value) =>\n String(value || '').trim()\n ).filter(Boolean)));\n}\n\nfunction bankRowEmployeeNumbers(row) {\n const rowKey = bankRowKey(row);\n\n const linkedNumbers =\n typeof linkedPayrollNumbersByBankRow !== 'undefined'\n ? linkedPayrollNumbersByBankRow.get(rowKey) || []\n : [];\n\n return Array.from(new Set(\n linkedNumbers\n .map(normalizeAccount)\n .filter((value) => value.length >= 6)\n ));\n}\n\nfunction extractDigitSequences(value) {\n return Array.from(new Set(\n String(value || '')\n .match(/\\d{6,20}/g) || []\n )).map(normalizeAccount).filter(\n (value) => value.length >= 6\n );\n}\n\nfunction bankRowReferenceNumbers(row) {\n return Array.from(new Set([\n ...extractDigitSequences(row.reference),\n ...extractDigitSequences(row.concept),\n ...extractDigitSequences(row.addenda),\n ...extractDigitSequences(row.participant_id),\n ]));\n}\n\nfunction isClearlyNonEmployeePayment(row) {\n const normalized = normalizeName([\n row.concept || '',\n row.bank_name_file || '',\n row.bank_account_holder || '',\n ].join(' '));\n\n return [\n 'pension alimenticia',\n 'embargo judicial',\n 'retencion judicial',\n ].some((token) =>\n normalized.includes(normalizeName(token))\n );\n}\n\nfunction buildBambooSearchIndex(employees) {\n const records = [];\n const exactAliasSets = new Map();\n const tokenIndexSets = new Map();\n const employeeNumberSets = new Map();\n\n for (\n let employeeIndex = 0;\n employeeIndex < employees.length;\n employeeIndex++\n ) {\n const employee = employees[employeeIndex];\n const aliases = [];\n const seenAliases = new Set();\n\n for (const rawAlias of bambooAliases(employee)) {\n const normalized = normalizeName(rawAlias);\n\n if (\n !normalized ||\n seenAliases.has(normalized)\n ) {\n continue;\n }\n\n seenAliases.add(normalized);\n\n const words = Array.from(new Set(\n nameWords(normalized)\n ));\n\n if (!words.length) continue;\n\n const wordSet = new Set(words);\n\n aliases.push({\n raw: rawAlias,\n normalized,\n words,\n wordSet,\n });\n\n let exactSet =\n exactAliasSets.get(normalized);\n\n if (!exactSet) {\n exactSet = new Set();\n exactAliasSets.set(\n normalized,\n exactSet\n );\n }\n\n exactSet.add(employeeIndex);\n\n for (const token of words) {\n if (token.length < 3) continue;\n\n let tokenSet =\n tokenIndexSets.get(token);\n\n if (!tokenSet) {\n tokenSet = new Set();\n tokenIndexSets.set(\n token,\n tokenSet\n );\n }\n\n tokenSet.add(employeeIndex);\n }\n }\n\n const employeeNumber =\n bambooEmployeeNumber(employee);\n\n if (employeeNumber.length >= 6) {\n let numberSet =\n employeeNumberSets.get(\n employeeNumber\n );\n\n if (!numberSet) {\n numberSet = new Set();\n employeeNumberSets.set(\n employeeNumber,\n numberSet\n );\n }\n\n numberSet.add(employeeIndex);\n }\n\n records.push({\n employee,\n aliases,\n employeeNumber,\n });\n }\n\n const exactAliasMap = new Map();\n const tokenIndex = new Map();\n const employeeNumberMap = new Map();\n\n for (const [key, value] of exactAliasSets) {\n exactAliasMap.set(\n key,\n Array.from(value)\n );\n }\n\n for (const [key, value] of tokenIndexSets) {\n tokenIndex.set(\n key,\n Array.from(value)\n );\n }\n\n for (\n const [key, value] of\n employeeNumberSets\n ) {\n employeeNumberMap.set(\n key,\n Array.from(value)\n );\n }\n\n return {\n records,\n exactAliasMap,\n tokenIndex,\n employeeNumberMap,\n };\n}\n\nfunction aliasMatchDetails(\n queryName,\n alias\n) {\n const queryNormalized =\n normalizeName(queryName);\n\n if (!queryNormalized) return null;\n\n if (\n queryNormalized === alias.normalized\n ) {\n return {\n score: 1,\n exact: true,\n containment: true,\n matchedTokens:\n alias.words.length,\n exactMatches:\n alias.words.length,\n fuzzyMatches: 0,\n queryCoverage: 1,\n aliasCoverage: 1,\n };\n }\n\n const queryWords = Array.from(\n new Set(nameWords(queryNormalized))\n );\n\n if (\n queryWords.length < 2 ||\n alias.words.length < 2\n ) {\n return null;\n }\n\n const querySet = new Set(queryWords);\n\n let exactMatches = 0;\n\n for (const queryWord of queryWords) {\n if (alias.wordSet.has(queryWord)) {\n exactMatches += 1;\n }\n }\n\n const queryInsideAlias =\n exactMatches === queryWords.length;\n\n let aliasWordsInsideQuery = 0;\n\n for (const aliasWord of alias.words) {\n if (querySet.has(aliasWord)) {\n aliasWordsInsideQuery += 1;\n }\n }\n\n const aliasInsideQuery =\n aliasWordsInsideQuery ===\n alias.words.length;\n\n if (\n queryInsideAlias ||\n aliasInsideQuery\n ) {\n const shorterLength = Math.min(\n queryWords.length,\n alias.words.length\n );\n const longerLength = Math.max(\n queryWords.length,\n alias.words.length\n );\n\n return {\n score:\n 0.90 +\n (\n shorterLength /\n Math.max(1, longerLength)\n ) * 0.09,\n exact: false,\n containment: true,\n matchedTokens: exactMatches,\n exactMatches,\n fuzzyMatches: 0,\n queryCoverage:\n exactMatches / queryWords.length,\n aliasCoverage:\n aliasWordsInsideQuery /\n alias.words.length,\n };\n }\n\n if (exactMatches < 2) {\n return null;\n }\n\n const usedAliasWords = new Set();\n let fuzzyMatches = 0;\n\n for (let queryIndex = 0;\n queryIndex < queryWords.length;\n queryIndex++\n ) {\n const queryWord =\n queryWords[queryIndex];\n\n if (alias.wordSet.has(queryWord)) {\n continue;\n }\n\n const aliasIndex =\n alias.words.findIndex(\n (aliasWord, currentIndex) =>\n !usedAliasWords.has(\n currentIndex\n ) &&\n !querySet.has(aliasWord) &&\n tokenMatches(\n queryWord,\n aliasWord\n )\n );\n\n if (aliasIndex >= 0) {\n usedAliasWords.add(aliasIndex);\n fuzzyMatches += 1;\n }\n }\n\n const matchedTokens =\n exactMatches + fuzzyMatches;\n\n if (\n fuzzyMatches > 1 ||\n matchedTokens < 3\n ) {\n return null;\n }\n\n const queryCoverage =\n matchedTokens / queryWords.length;\n\n const aliasCoverage =\n matchedTokens / alias.words.length;\n\n if (\n queryCoverage < 0.67 ||\n aliasCoverage < 0.60\n ) {\n return null;\n }\n\n return {\n score:\n queryCoverage * 0.48 +\n aliasCoverage * 0.32 +\n (\n exactMatches /\n matchedTokens\n ) * 0.20,\n exact: false,\n containment: false,\n matchedTokens,\n exactMatches,\n fuzzyMatches,\n queryCoverage,\n aliasCoverage,\n };\n}\n\nfunction candidateIndexesForName(\n normalizedName\n) {\n const tokens = Array.from(\n new Set(nameWords(normalizedName))\n ).filter((token) =>\n token.length >= 3\n );\n\n const votes = new Map();\n\n for (const token of tokens) {\n const indexes =\n bambooSearch.tokenIndex.get(token) || [];\n\n /*\n * Los tokens muy comunes no aportan suficiente identidad.\n * Ignorarlos evita cientos de candidatos y mantiene el nodo rápido.\n */\n if (indexes.length > 240) continue;\n\n for (const index of indexes) {\n votes.set(\n index,\n (votes.get(index) || 0) + 1\n );\n }\n }\n\n return Array.from(votes.entries())\n .filter(([, voteCount]) =>\n voteCount >= 2 ||\n (\n tokens.length === 2 &&\n voteCount === 2\n )\n )\n .sort((left, right) =>\n right[1] - left[1]\n )\n .slice(0, 90)\n .map(([index]) => index);\n}\n\nconst bambooMatchCache = new Map();\n\nfunction findBambooMatch(bankRow) {\n const names = bankRowNames(bankRow)\n .map((raw) => ({\n raw,\n normalized:\n normalizeName(raw),\n tokenCount:\n nameWords(raw).length,\n }))\n .filter((entry) =>\n entry.normalized &&\n entry.tokenCount >= 2\n )\n .sort((left, right) =>\n right.tokenCount -\n left.tokenCount\n );\n\n const directEmployeeNumbers =\n bankRowEmployeeNumbers(bankRow);\n\n const referenceNumbers =\n bankRowReferenceNumbers(bankRow);\n\n const cacheKey = [\n ...directEmployeeNumbers\n .slice()\n .sort(),\n ...referenceNumbers\n .slice()\n .sort(),\n ...names\n .map((entry) =>\n entry.normalized\n )\n .sort(),\n ].join('|');\n\n if (bambooMatchCache.has(cacheKey)) {\n return bambooMatchCache.get(\n cacheKey\n );\n }\n\n const numberCandidates = new Set();\n\n for (const employeeNumber of [\n ...directEmployeeNumbers,\n ...referenceNumbers,\n ]) {\n for (\n const index of\n bambooSearch.employeeNumberMap\n .get(employeeNumber) || []\n ) {\n numberCandidates.add(index);\n }\n }\n\n if (numberCandidates.size === 1) {\n const index =\n numberCandidates.values()\n .next().value;\n\n const result = {\n found: true,\n matched_by:\n directEmployeeNumbers.length\n ? 'employee_number_payroll'\n : 'employee_number_reference',\n confidence: 1,\n employee:\n bambooSearch.records[index]\n .employee,\n };\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n }\n\n\n /*\n * Consulta primero la resolución calculada una sola vez en el\n * normalizador. Esto evita repetir búsquedas aproximadas por cada fila\n * bancaria y mantiene el task runner estable incluso con miles de\n * empleados en BambooHR.\n */\n const precomputedNameMatches =\n data.bamboo?.resolved_name_matches ||\n {};\n\n const precomputedNameEntries =\n names.map((entry) => {\n const raw =\n typeof entry === 'string'\n ? entry\n : entry?.raw || '';\n\n return {\n raw,\n normalized:\n typeof entry === 'string'\n ? normalizeName(entry)\n : (\n entry?.normalized ||\n normalizeName(raw)\n ),\n token_count:\n typeof entry === 'string'\n ? nameWords(entry).length\n : (\n entry?.tokenCount ||\n nameWords(raw).length\n ),\n };\n }).filter((entry) =>\n entry.normalized\n );\n\n const precomputedFoundByEmployee =\n new Map();\n\n function resolutionEmployeeKey(\n employee\n ) {\n return (\n String(\n employee?.bamboo_id ||\n ''\n ).trim() ||\n normalizeAccount(\n employee?.employee_number ||\n employee?.employeeNumber ||\n ''\n ) ||\n normalizeName(\n employee?.full_name ||\n employee?.displayName ||\n ''\n )\n );\n }\n\n for (\n const nameEntry of\n precomputedNameEntries\n ) {\n const decision =\n precomputedNameMatches[\n nameEntry.normalized\n ];\n\n if (\n !decision ||\n decision.found !== true\n ) {\n continue;\n }\n\n let employee =\n Number.isInteger(\n decision.employee_index\n )\n ? bambooEmployees[\n decision.employee_index\n ]\n : null;\n\n const expectedKey =\n String(\n decision.employee_key ||\n ''\n ).trim();\n\n if (\n !employee ||\n (\n expectedKey &&\n resolutionEmployeeKey(\n employee\n ) !== expectedKey\n )\n ) {\n employee =\n bambooEmployees.find(\n (candidate) =>\n resolutionEmployeeKey(\n candidate\n ) === expectedKey\n ) || null;\n }\n\n if (!employee) continue;\n\n const employeeKey =\n resolutionEmployeeKey(employee);\n\n const candidate = {\n employee,\n employee_key:\n employeeKey,\n confidence:\n Number(\n decision.confidence || 0\n ),\n matched_by:\n decision.matched_by ||\n 'precomputed_name',\n bank_name:\n nameEntry.raw,\n bamboo_alias:\n decision.bamboo_alias ||\n employee.full_name ||\n '',\n informativeness:\n nameEntry.token_count,\n };\n\n const existing =\n precomputedFoundByEmployee\n .get(employeeKey);\n\n if (\n !existing ||\n candidate.confidence >\n existing.confidence ||\n (\n candidate.confidence ===\n existing.confidence &&\n candidate.informativeness >\n existing.informativeness\n )\n ) {\n precomputedFoundByEmployee.set(\n employeeKey,\n candidate\n );\n }\n }\n\n const precomputedRanked =\n Array.from(\n precomputedFoundByEmployee\n .values()\n ).sort((left, right) => {\n if (\n right.confidence !==\n left.confidence\n ) {\n return (\n right.confidence -\n left.confidence\n );\n }\n\n return (\n right.informativeness -\n left.informativeness\n );\n });\n\n if (precomputedRanked.length === 1) {\n const best =\n precomputedRanked[0];\n\n const result = {\n found: true,\n matched_by:\n best.matched_by,\n confidence:\n best.confidence,\n employee:\n best.employee,\n bank_name:\n best.bank_name,\n bamboo_alias:\n best.bamboo_alias,\n };\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n }\n\n if (\n precomputedRanked.length > 1\n ) {\n const best =\n precomputedRanked[0];\n\n const second =\n precomputedRanked[1];\n\n if (\n best.confidence -\n second.confidence >= 0.08\n ) {\n const result = {\n found: true,\n matched_by:\n best.matched_by,\n confidence:\n best.confidence,\n employee:\n best.employee,\n bank_name:\n best.bank_name,\n bamboo_alias:\n best.bamboo_alias,\n };\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n }\n\n const result = {\n found: false,\n matched_by: null,\n confidence:\n best.confidence,\n employee: null,\n ambiguous: true,\n reason:\n 'conflicting_precomputed_name_matches',\n best_candidate: {\n employee:\n best.employee,\n score:\n best.confidence,\n bank_name:\n best.bank_name,\n bamboo_alias:\n best.bamboo_alias,\n },\n };\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n }\n\n\n /*\n * Primero se intenta una coincidencia textual exacta.\n * Se acepta solamente cuando todos los alias exactos apuntan\n * al mismo empleado.\n */\n const exactIndexes = new Set();\n\n for (const name of names) {\n for (\n const index of\n bambooSearch.exactAliasMap\n .get(name.normalized) || []\n ) {\n exactIndexes.add(index);\n }\n }\n\n if (exactIndexes.size === 1) {\n const index =\n exactIndexes.values()\n .next().value;\n\n const result = {\n found: true,\n matched_by: 'exact_name',\n confidence: 1,\n employee:\n bambooSearch.records[index]\n .employee,\n bank_name:\n names[0]?.raw || '',\n bamboo_alias:\n bambooSearch.records[index]\n .aliases[0]?.raw || '',\n };\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n }\n\n const candidatesByEmployee =\n new Map();\n\n /*\n * La búsqueda ya no recorre todos los empleados.\n * Cada nombre consulta el índice invertido y solo compara\n * un máximo de 90 candidatos que comparten al menos dos palabras.\n */\n for (const name of names) {\n const candidateIndexes =\n candidateIndexesForName(\n name.normalized\n );\n\n for (const index of candidateIndexes) {\n const record =\n bambooSearch.records[index];\n\n let bestAliasMatch = null;\n let bestAlias = '';\n\n for (const alias of record.aliases) {\n const details =\n aliasMatchDetails(\n name.raw,\n alias\n );\n\n if (\n details &&\n (\n !bestAliasMatch ||\n details.score >\n bestAliasMatch.score\n )\n ) {\n bestAliasMatch = details;\n bestAlias = alias.raw;\n }\n }\n\n if (!bestAliasMatch) continue;\n\n const existing =\n candidatesByEmployee.get(index);\n\n const candidate = {\n index,\n employee: record.employee,\n score: bestAliasMatch.score,\n details: bestAliasMatch,\n bank_name: name.raw,\n bamboo_alias: bestAlias,\n informativeness:\n name.tokenCount,\n };\n\n if (\n !existing ||\n candidate.score >\n existing.score ||\n (\n candidate.score ===\n existing.score &&\n candidate.informativeness >\n existing.informativeness\n )\n ) {\n candidatesByEmployee.set(\n index,\n candidate\n );\n }\n }\n }\n\n const rankedCandidates =\n Array.from(\n candidatesByEmployee.values()\n ).sort((left, right) => {\n if (right.score !== left.score) {\n return right.score - left.score;\n }\n\n if (\n right.details.exactMatches !==\n left.details.exactMatches\n ) {\n return (\n right.details.exactMatches -\n left.details.exactMatches\n );\n }\n\n return (\n right.informativeness -\n left.informativeness\n );\n });\n\n const best =\n rankedCandidates[0] || null;\n\n const second =\n rankedCandidates[1] || null;\n\n const margin =\n best\n ? best.score -\n (second?.score || 0)\n : 0;\n\n const strongContainment =\n Boolean(\n best?.details?.containment &&\n best.details.exactMatches >= 2 &&\n (\n !second ||\n margin >= 0.035 ||\n best.details.exactMatches >\n second.details.exactMatches\n )\n );\n\n const strongPartial =\n Boolean(\n best &&\n !best.details.containment &&\n best.score >= 0.82 &&\n best.details.exactMatches >= 2 &&\n (\n !second ||\n margin >= 0.07\n )\n );\n\n let result;\n\n if (\n best &&\n (\n strongContainment ||\n strongPartial\n )\n ) {\n result = {\n found: true,\n matched_by:\n strongContainment\n ? 'unique_token_containment'\n : 'strong_indexed_name',\n confidence:\n Math.min(1, best.score),\n employee: best.employee,\n bank_name: best.bank_name,\n bamboo_alias:\n best.bamboo_alias,\n };\n } else {\n result = {\n found: false,\n matched_by: null,\n confidence:\n best?.score || 0,\n employee: null,\n ambiguous: Boolean(\n best &&\n second &&\n best.score >= 0.75 &&\n margin < 0.07\n ),\n best_candidate:\n best\n ? {\n employee:\n best.employee,\n score:\n best.score,\n bank_name:\n best.bank_name,\n bamboo_alias:\n best.bamboo_alias,\n }\n : null,\n };\n }\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n}\n\nfunction supplementKey(supplement) {\n return [\n supplement.source_sheet || '',\n supplement.row_number || '',\n supplement.supplement_id || '',\n supplement.account || '',\n supplement.payroll_amount || 0,\n ].join('|');\n}\n\nconst payrollAccounts = (data.payroll?.grouped_by_account || [])\n .map((row) => ({\n ...row,\n group_key:\n row.group_key ||\n `${normalizeAccount(row.account)}:${row.currency || 'QTZ'}`,\n account: normalizeAccount(row.account),\n employee_name: row.employee_name || row.employee || '',\n employee_number: row.employee_number || row.employeeNumber || '',\n currency: row.currency || 'QTZ',\n payroll_amount: roundMoney(row.payroll_amount || row.payrollAmount),\n source_rows: Array.isArray(row.source_rows) ? [...row.source_rows] : [],\n source_sheets: Array.isArray(row.source_sheets)\n ? [...row.source_sheets]\n : [],\n }))\n .filter((row) => row.account && row.payroll_amount > 0);\n\nconst payrollNoAccountRows = (data.payroll?.no_account_rows || [])\n .map((row) => ({\n ...row,\n account: '',\n employee_name: row.employee_name || row.employee || '',\n employee_number: row.employee_number || row.employeeNumber || '',\n currency: row.currency || 'QTZ',\n payroll_amount: roundMoney(row.payroll_amount || row.payrollAmount),\n }))\n .filter((row) => row.payroll_amount > 0);\n\nconst bankAccounts = (data.bank?.grouped_by_account || [])\n .map((row) => ({\n ...row,\n group_key:\n row.group_key ||\n `ACCOUNT:${normalizeAccount(row.account)}:${row.currency || 'QTZ'}`,\n account: normalizeAccount(row.account),\n account_is_valid: Boolean(row.account_is_valid),\n currency: row.currency || 'QTZ',\n amount: roundMoney(row.amount || row.bank_amount || row.bankAmount),\n source_rows: Array.isArray(row.source_rows) ? [...row.source_rows] : [],\n }))\n .filter((row) => row.amount > 0);\n\nconst bambooEmployees = Array.isArray(data.bamboo?.employees)\n ? data.bamboo.employees\n : [];\n\nconst bambooValidationAvailable =\n data.bamboo?.fetch_complete === true &&\n data.bamboo?.validation_available === true &&\n bambooEmployees.length > 0;\n\nconst bambooValidationWarning =\n bambooValidationAvailable\n ? null\n : (\n data.errors?.find((error) =>\n String(error || '').toLowerCase().includes('bamboohr')\n ) ||\n 'La validación Banco sin Bamboo no estuvo disponible porque la descarga de empleados de BambooHR quedó incompleta.'\n );\n\nconst bambooSearch = buildBambooSearchIndex(\n bambooEmployees\n);\n\nconst bankDetailRows = Array.isArray(data.bank?.rows)\n ? data.bank.rows\n : [];\n\nconst potentialSupplements = (\n data.payroll?.potential_supplements ||\n data.debug_payroll?.potential_supplements ||\n data.debug_payroll?.attached_supplements ||\n []\n)\n .map((row) => ({\n ...row,\n account: normalizeAccount(row.account),\n currency: row.currency || 'QTZ',\n payroll_amount: roundMoney(row.payroll_amount || row.payrollAmount),\n }))\n .filter((row) => {\n const id = normalizeName(row.supplement_id || '');\n\n return (\n row.account &&\n row.payroll_amount >= 10 &&\n !id.includes('back up')\n );\n });\n\nconst supplementsByAccountCurrency = new Map();\n\nfor (const supplement of potentialSupplements) {\n const key = `${supplement.account}:${supplement.currency}`;\n const current = supplementsByAccountCurrency.get(key) || [];\n\n current.push(supplement);\n supplementsByAccountCurrency.set(key, current);\n}\n\nfunction chooseConditionalSupplements(payroll, bank) {\n const baseAmount = roundMoney(payroll.payroll_amount);\n const bankAmount = roundMoney(bank.amount);\n const candidates =\n supplementsByAccountCurrency.get(\n `${payroll.account}:${payroll.currency}`\n ) || [];\n\n if (\n !candidates.length ||\n bankAmount <= baseAmount + 0.02\n ) {\n return {\n selected: [],\n effectiveAmount: baseAmount,\n baseAmount,\n improvement: 0,\n };\n }\n\n const baseDifference = Math.abs(baseAmount - bankAmount);\n let bestSelected = [];\n let bestAmount = baseAmount;\n let bestDifference = baseDifference;\n\n if (candidates.length <= 12) {\n const combinations = 1 << candidates.length;\n\n for (let mask = 1; mask < combinations; mask++) {\n const selected = [];\n let selectedTotal = 0;\n\n for (let index = 0; index < candidates.length; index++) {\n if ((mask & (1 << index)) !== 0) {\n selected.push(candidates[index]);\n selectedTotal = roundMoney(\n selectedTotal + candidates[index].payroll_amount\n );\n }\n }\n\n const candidateAmount = roundMoney(baseAmount + selectedTotal);\n const candidateDifference = Math.abs(\n candidateAmount - bankAmount\n );\n\n if (candidateDifference < bestDifference) {\n bestSelected = selected;\n bestAmount = candidateAmount;\n bestDifference = candidateDifference;\n }\n }\n } else {\n const sorted = [...candidates].sort(\n (a, b) => b.payroll_amount - a.payroll_amount\n );\n\n let runningAmount = baseAmount;\n const selected = [];\n\n for (const candidate of sorted) {\n const nextAmount = roundMoney(\n runningAmount + candidate.payroll_amount\n );\n\n if (\n Math.abs(nextAmount - bankAmount) <\n Math.abs(runningAmount - bankAmount)\n ) {\n selected.push(candidate);\n runningAmount = nextAmount;\n }\n }\n\n bestSelected = selected;\n bestAmount = runningAmount;\n bestDifference = Math.abs(bestAmount - bankAmount);\n }\n\n const improvement = roundMoney(\n baseDifference - bestDifference\n );\n\n // Evita sumar valores accidentales o inmateriales, como un \"Asignado\" de Q1.\n if (!bestSelected.length || improvement < 5) {\n return {\n selected: [],\n effectiveAmount: baseAmount,\n baseAmount,\n improvement: 0,\n };\n }\n\n return {\n selected: bestSelected,\n effectiveAmount: roundMoney(bestAmount),\n baseAmount,\n improvement,\n };\n}\n\nfunction getDirectCandidates(payroll, matchedBankKeys) {\n return bankAccounts\n .filter((bank) => {\n if (matchedBankKeys.has(bank.group_key)) return false;\n if (bank.currency !== payroll.currency) return false;\n\n const relationship = accountRelationship(\n payroll.account,\n bank.account\n );\n\n if (!relationship.matches) return false;\n\n // Un sufijo de referencia solamente es válido cuando el nombre también\n // corresponde a la misma persona.\n if (\n relationship.type === 'reference_prefix' &&\n !bankMatchesName(bank, payroll.employee_name)\n ) {\n return false;\n }\n\n return true;\n })\n .map((bank) => {\n const relationship = accountRelationship(\n payroll.account,\n bank.account\n );\n const supplementDecision =\n chooseConditionalSupplements(payroll, bank);\n\n return {\n bank,\n relationship,\n supplementDecision,\n nameMatches: bankMatchesName(bank, payroll.employee_name),\n };\n })\n .sort((a, b) => {\n const exactDifference =\n Number(b.relationship.type === 'exact') -\n Number(a.relationship.type === 'exact');\n\n if (exactDifference !== 0) return exactDifference;\n\n const nameDifference =\n Number(b.nameMatches) - Number(a.nameMatches);\n\n if (nameDifference !== 0) return nameDifference;\n\n return (\n Math.abs(\n a.supplementDecision.effectiveAmount - a.bank.amount\n ) -\n Math.abs(\n b.supplementDecision.effectiveAmount - b.bank.amount\n )\n );\n });\n}\n\nfunction buildSources(payroll, selectedSupplements) {\n const supplementRows = selectedSupplements.map((row) => ({\n source_sheet: row.source_sheet,\n row_number: row.row_number,\n amount: row.payroll_amount,\n supplement_original_name:\n row.supplement_original_name || row.employee_name || '',\n supplement_id: row.supplement_id || '',\n applied_conditionally: true,\n }));\n\n const sourceRows = [\n ...(payroll.source_rows || []),\n ...supplementRows,\n ];\n\n const sourceSheets = Array.from(new Set([\n ...(payroll.source_sheets || []),\n ...selectedSupplements\n .map((row) => row.source_sheet)\n .filter(Boolean),\n ]));\n\n return { sourceRows, sourceSheets };\n}\n\nconst matchedPayrollKeys = new Set();\nconst matchedBankKeys = new Set();\nconst matchedNoAccountIndexes = new Set();\nconst appliedSupplementKeys = new Set();\nconst appliedSupplements = [];\nconst finalExactReconciliations = [];\nconst rows = [];\n\nfunction registerSupplements(selected) {\n for (const supplement of selected || []) {\n const key = supplementKey(supplement);\n\n if (!appliedSupplementKeys.has(key)) {\n appliedSupplementKeys.add(key);\n appliedSupplements.push(supplement);\n }\n }\n}\n\n// 1) Cuenta exacta o referencia con prefijo, y monto conciliado.\nfor (const payroll of payrollAccounts) {\n const candidates = getDirectCandidates(\n payroll,\n matchedBankKeys\n ).filter((candidate) => {\n return moneyEquals(\n candidate.supplementDecision.effectiveAmount,\n candidate.bank.amount\n );\n });\n\n if (candidates.length !== 1) continue;\n\n const candidate = candidates[0];\n const bank = candidate.bank;\n const decision = candidate.supplementDecision;\n const sources = buildSources(payroll, decision.selected);\n\n matchedPayrollKeys.add(payroll.group_key);\n matchedBankKeys.add(bank.group_key);\n registerSupplements(decision.selected);\n\n rows.push({\n id: `match_${payroll.group_key}_${bank.group_key}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: payroll.currency,\n payrollAmount: decision.effectiveAmount,\n payroll_amount: decision.effectiveAmount,\n payrollBaseAmount: decision.baseAmount,\n payroll_base_amount: decision.baseAmount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: 0,\n status: 'Coincidencia',\n category: 'coincidencia',\n subcategory:\n candidate.relationship.type === 'reference_prefix'\n ? 'referencia_bancaria_con_prefijo'\n : decision.selected.length\n ? 'cuenta_monto_y_suplemento_condicional'\n : 'cuenta_y_monto_coinciden',\n observation:\n candidate.relationship.type === 'reference_prefix'\n ? 'Conciliado por nombre, monto y referencia bancaria con prefijo.'\n : decision.selected.length\n ? 'Conciliado correctamente. Se aplicó un suplemento porque el banco mostró un pago adicional.'\n : 'Conciliado correctamente.',\n applied_supplements: decision.selected,\n source_sheets: sources.sourceSheets,\n source_rows: sources.sourceRows,\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 2) Cuenta diferente, pero nombre y monto coinciden.\n// Se ejecuta antes de crear diferencias directas para resolver casos como\n// Ashly/Ashley Ramos: la cuenta de la nómina apunta a otra transacción,\n// pero existe otra cuenta bancaria con el mismo nombre y monto correcto.\nfor (const payroll of payrollAccounts) {\n if (matchedPayrollKeys.has(payroll.group_key)) continue;\n\n const candidates = bankAccounts\n .filter((bank) => {\n if (matchedBankKeys.has(bank.group_key)) return false;\n if (bank.currency !== payroll.currency) return false;\n if (!bankMatchesName(bank, payroll.employee_name)) return false;\n\n const decision = chooseConditionalSupplements(\n payroll,\n bank\n );\n\n return moneyEquals(\n decision.effectiveAmount,\n bank.amount\n );\n })\n .map((bank) => ({\n bank,\n supplementDecision: chooseConditionalSupplements(\n payroll,\n bank\n ),\n }));\n\n if (candidates.length !== 1) continue;\n\n const candidate = candidates[0];\n const bank = candidate.bank;\n const decision = candidate.supplementDecision;\n const relationship = accountRelationship(\n payroll.account,\n bank.account\n );\n\n // Las referencias con prefijo ya debieron resolverse en el paso 1.\n if (relationship.type === 'reference_prefix') continue;\n\n const sources = buildSources(payroll, decision.selected);\n\n matchedPayrollKeys.add(payroll.group_key);\n matchedBankKeys.add(bank.group_key);\n registerSupplements(decision.selected);\n\n rows.push({\n id: `possible_wrong_account_${payroll.group_key}_${bank.group_key}`,\n employee: payroll.employee_name || bestBankDisplayName(bank),\n employee_name:\n payroll.employee_name || bestBankDisplayName(bank),\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: payroll.currency,\n payrollAmount: decision.effectiveAmount,\n payroll_amount: decision.effectiveAmount,\n payrollBaseAmount: decision.baseAmount,\n payroll_base_amount: decision.baseAmount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: 0,\n status: 'Riesgo',\n category: 'posible_cuenta_mal_digitada',\n subcategory:\n 'nombre_y_monto_coinciden_cuenta_diferente',\n observation:\n `El nombre y el monto coinciden, pero la cuenta de nómina ` +\n `(${payroll.account || 'sin cuenta'}) es diferente a la cuenta ` +\n `del banco (${bank.account || 'sin cuenta válida'}).`,\n applied_supplements: decision.selected,\n source_sheets: sources.sourceSheets,\n source_rows: sources.sourceRows,\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 3) Nómina sin cuenta válida: conciliar por nombre y monto.\nfor (\n let index = 0;\n index < payrollNoAccountRows.length;\n index++\n) {\n const payroll = payrollNoAccountRows[index];\n\n const candidates = bankAccounts.filter((bank) => {\n if (matchedBankKeys.has(bank.group_key)) return false;\n if (bank.currency !== payroll.currency) return false;\n if (!moneyEquals(bank.amount, payroll.payroll_amount)) {\n return false;\n }\n\n return bankMatchesName(bank, payroll.employee_name);\n });\n\n if (candidates.length !== 1) continue;\n\n const bank = candidates[0];\n\n matchedNoAccountIndexes.add(index);\n matchedBankKeys.add(bank.group_key);\n\n rows.push({\n id: `match_no_account_${index}_${bank.group_key}`,\n employee:\n payroll.employee_name || bestBankDisplayName(bank),\n employee_name:\n payroll.employee_name || bestBankDisplayName(bank),\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: bank.account,\n payrollAccount: '',\n payroll_account: '',\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: payroll.currency,\n payrollAmount: payroll.payroll_amount,\n payroll_amount: payroll.payroll_amount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: 0,\n status: 'Coincidencia',\n category: 'coincidencia',\n subcategory:\n 'conciliado_por_nombre_y_monto_sin_cuenta_nomina',\n observation:\n 'Conciliado por nombre y monto. La nómina no tenía una cuenta bancaria válida.',\n source_sheet: payroll.source_sheet,\n row_number: payroll.row_number,\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 4) Diferencias reales en una cuenta exacta o equivalente.\nfor (const payroll of payrollAccounts) {\n if (matchedPayrollKeys.has(payroll.group_key)) continue;\n\n const candidates = getDirectCandidates(\n payroll,\n matchedBankKeys\n );\n\n if (!candidates.length) continue;\n\n const candidate = candidates[0];\n const bank = candidate.bank;\n const decision = candidate.supplementDecision;\n const sources = buildSources(payroll, decision.selected);\n const difference = moneyDiff(\n decision.effectiveAmount,\n bank.amount\n );\n\n matchedPayrollKeys.add(payroll.group_key);\n matchedBankKeys.add(bank.group_key);\n registerSupplements(decision.selected);\n\n rows.push({\n id: `difference_${payroll.group_key}_${bank.group_key}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: payroll.currency,\n payrollAmount: decision.effectiveAmount,\n payroll_amount: decision.effectiveAmount,\n payrollBaseAmount: decision.baseAmount,\n payroll_base_amount: decision.baseAmount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference,\n status: 'Riesgo',\n category: 'discrepancia',\n subcategory: 'diferencia_monto',\n observation:\n `Diferencia de ${payroll.currency} ` +\n `${formatMoney(difference)}.`,\n applied_supplements: decision.selected,\n source_sheets: sources.sourceSheets,\n source_rows: sources.sourceRows,\n bank_source_rows: bank.source_rows,\n });\n}\n\n\n// 4.5) Reconciliación final exacta de pares residuales.\n//\n// Este paso corrige casos en los que nómina y banco contienen:\n// - la misma cuenta normalizada;\n// - el mismo empleado;\n// - el mismo monto;\n// pero no fueron enlazados en los pasos anteriores por diferencias técnicas\n// de agrupación, moneda inferida o metadatos del CSV.\n//\n// Es deliberadamente conservador: exige una única contraparte bancaria.\nfor (const payroll of payrollAccounts) {\n if (matchedPayrollKeys.has(payroll.group_key)) continue;\n\n const candidates = bankAccounts\n .filter((bank) => {\n if (matchedBankKeys.has(bank.group_key)) return false;\n\n const payrollAccount = normalizeAccount(payroll.account);\n const bankAccount = normalizeAccount(bank.account);\n\n if (!payrollAccount || payrollAccount !== bankAccount) {\n return false;\n }\n\n if (!bankMatchesName(bank, payroll.employee_name)) {\n return false;\n }\n\n const decision = chooseConditionalSupplements(payroll, bank);\n\n return moneyEquals(\n decision.effectiveAmount,\n bank.amount\n );\n })\n .map((bank) => ({\n bank,\n supplementDecision: chooseConditionalSupplements(\n payroll,\n bank\n ),\n }));\n\n if (candidates.length !== 1) continue;\n\n const candidate = candidates[0];\n const bank = candidate.bank;\n const decision = candidate.supplementDecision;\n const sources = buildSources(\n payroll,\n decision.selected\n );\n\n matchedPayrollKeys.add(payroll.group_key);\n matchedBankKeys.add(bank.group_key);\n registerSupplements(decision.selected);\n\n finalExactReconciliations.push({\n employee_name: payroll.employee_name,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payroll_currency: payroll.currency,\n bank_currency: bank.currency,\n payroll_amount: decision.effectiveAmount,\n bank_amount: bank.amount,\n payroll_group_key: payroll.group_key,\n bank_group_key: bank.group_key,\n });\n\n rows.push({\n id: `final_exact_match_${payroll.group_key}_${bank.group_key}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: bank.currency || payroll.currency,\n payrollAmount: decision.effectiveAmount,\n payroll_amount: decision.effectiveAmount,\n payrollBaseAmount: decision.baseAmount,\n payroll_base_amount: decision.baseAmount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: 0,\n status: 'Coincidencia',\n category: 'coincidencia',\n subcategory: 'reconciliacion_final_cuenta_nombre_monto',\n observation:\n 'Conciliado por cuenta, nombre y monto en la validación final.',\n applied_supplements: decision.selected,\n source_sheets: sources.sourceSheets,\n source_rows: sources.sourceRows,\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 5) Nómina con cuenta sin pago bancario.\nfor (const payroll of payrollAccounts) {\n if (matchedPayrollKeys.has(payroll.group_key)) continue;\n\n rows.push({\n id: `payroll_without_bank_${payroll.group_key}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: '',\n bank_account: '',\n currency: payroll.currency,\n payrollAmount: payroll.payroll_amount,\n payroll_amount: payroll.payroll_amount,\n payrollBaseAmount: payroll.payroll_amount,\n payroll_base_amount: payroll.payroll_amount,\n bankAmount: 0,\n bank_amount: 0,\n difference: payroll.payroll_amount,\n status: 'Riesgo',\n category: 'discrepancia',\n subcategory: 'nomina_con_cuenta_sin_pago_banco',\n observation:\n 'Está en nómina, pero no aparece pagado en el banco.',\n applied_supplements: [],\n source_sheets: payroll.source_sheets,\n source_rows: payroll.source_rows,\n });\n}\n\n// 6) Banco sin nómina.\nfor (const bank of bankAccounts) {\n if (matchedBankKeys.has(bank.group_key)) continue;\n\n rows.push({\n id: `bank_without_payroll_${bank.group_key}`,\n employee:\n bestBankDisplayName(bank) || 'Pago bancario sin nómina',\n employee_name:\n bestBankDisplayName(bank) || 'Pago bancario sin nómina',\n employeeNumber: '',\n employee_number: '',\n account: bank.account,\n payrollAccount: '',\n payroll_account: '',\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: bank.currency,\n payrollAmount: 0,\n payroll_amount: 0,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: roundMoney(0 - bank.amount),\n status: 'Pendiente revisión',\n category: 'banco_sin_nomina',\n subcategory: 'pago_banco_sin_fila_nomina',\n observation:\n 'Recibió un pago en el banco, pero no aparece en la nómina cargada.',\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 7) Nómina sin cuenta que no pudo conciliarse.\nfor (\n let index = 0;\n index < payrollNoAccountRows.length;\n index++\n) {\n if (matchedNoAccountIndexes.has(index)) continue;\n\n const payroll = payrollNoAccountRows[index];\n\n rows.push({\n id:\n `payroll_without_account_` +\n `${payroll.source_sheet}_${payroll.row_number}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: '',\n payrollAccount: '',\n payroll_account: '',\n bankAccount: '',\n bank_account: '',\n currency: payroll.currency,\n payrollAmount: payroll.payroll_amount,\n payroll_amount: payroll.payroll_amount,\n bankAmount: 0,\n bank_amount: 0,\n difference: payroll.payroll_amount,\n status: 'Pendiente revisión',\n category: 'nomina_sin_cuenta',\n subcategory: 'nomina_sin_cuenta_bancaria',\n observation:\n 'Tiene monto en nómina, pero no tiene una cuenta bancaria válida para cruzar contra el banco.',\n source_sheet: payroll.source_sheet,\n row_number: payroll.row_number,\n });\n}\n\n// 8) Consolidar el mismo empleado cuando aparece con dos cuentas de nómina.\nconst originalRows = [...rows];\nconst usedRowIds = new Set();\nconst consolidatedRows = [];\n\nfor (const differenceRow of originalRows) {\n if (\n differenceRow.category !== 'discrepancia' ||\n differenceRow.subcategory !== 'diferencia_monto' ||\n usedRowIds.has(differenceRow.id)\n ) {\n continue;\n }\n\n const extraPayrollRow = originalRows.find((candidate) => {\n if (\n candidate.id === differenceRow.id ||\n usedRowIds.has(candidate.id) ||\n candidate.subcategory !==\n 'nomina_con_cuenta_sin_pago_banco' ||\n candidate.currency !== differenceRow.currency\n ) {\n return false;\n }\n\n const samePerson = samePersonName(\n differenceRow.employee_name || differenceRow.employee,\n candidate.employee_name || candidate.employee\n );\n\n const similarAccounts =\n accountDistance(\n differenceRow.account,\n candidate.account\n ) <= 2;\n\n const combinedPayroll = roundMoney(\n differenceRow.payroll_amount +\n candidate.payroll_amount\n );\n\n const totalMatches = moneyEquals(\n combinedPayroll,\n differenceRow.bank_amount\n );\n\n return samePerson && similarAccounts && totalMatches;\n });\n\n if (!extraPayrollRow) continue;\n\n usedRowIds.add(differenceRow.id);\n usedRowIds.add(extraPayrollRow.id);\n\n const totalPayroll = roundMoney(\n differenceRow.payroll_amount +\n extraPayrollRow.payroll_amount\n );\n\n const accounts = Array.from(new Set([\n differenceRow.account,\n extraPayrollRow.account,\n ].filter(Boolean)));\n\n consolidatedRows.push({\n id:\n `split_account_` +\n `${differenceRow.account}_${extraPayrollRow.account}`,\n employee: differenceRow.employee_name,\n employee_name: differenceRow.employee_name,\n employeeNumber:\n differenceRow.employee_number ||\n extraPayrollRow.employee_number ||\n '',\n employee_number:\n differenceRow.employee_number ||\n extraPayrollRow.employee_number ||\n '',\n account:\n differenceRow.bank_account ||\n differenceRow.account,\n payrollAccount: accounts.join(' / '),\n payroll_account: accounts.join(' / '),\n bankAccount: differenceRow.bank_account,\n bank_account: differenceRow.bank_account,\n currency: differenceRow.currency,\n payrollAmount: totalPayroll,\n payroll_amount: totalPayroll,\n bankAmount: differenceRow.bank_amount,\n bank_amount: differenceRow.bank_amount,\n difference: moneyDiff(\n totalPayroll,\n differenceRow.bank_amount\n ),\n status: 'Riesgo',\n category: 'posible_cuenta_mal_digitada',\n subcategory:\n 'mismo_empleado_con_cuentas_distintas_en_nomina',\n observation:\n `El total de nómina coincide con el banco, pero el empleado ` +\n `aparece con cuentas distintas en la nómina: ` +\n `${accounts.join(' y ')}. La cuenta utilizada por el banco ` +\n `fue ${differenceRow.bank_account}.`,\n applied_supplements:\n differenceRow.applied_supplements || [],\n source_sheets: Array.from(new Set([\n ...(differenceRow.source_sheets || []),\n ...(extraPayrollRow.source_sheets || []),\n ])),\n source_rows: [\n ...(differenceRow.source_rows || []),\n ...(extraPayrollRow.source_rows || []),\n ],\n bank_source_rows:\n differenceRow.bank_source_rows || [],\n });\n}\n\nconst coreRows = [\n ...originalRows.filter(\n (row) => !usedRowIds.has(row.id)\n ),\n ...consolidatedRows,\n];\n\nconst coreCoincidencias = coreRows.filter(\n (row) => row.category === 'coincidencia'\n).length;\n\nconst coreDiscrepancias = coreRows.filter((row) => {\n return (\n row.category === 'discrepancia' ||\n row.category === 'posible_cuenta_mal_digitada'\n );\n}).length;\n\nconst coreBancoSinNomina = coreRows.filter(\n (row) => row.category === 'banco_sin_nomina'\n).length;\n\nconst coreNominaSinCuenta = coreRows.filter(\n (row) => row.category === 'nomina_sin_cuenta'\n).length;\n\nconst corePosiblesCuentas = coreRows.filter(\n (row) => row.category === 'posible_cuenta_mal_digitada'\n).length;\n\nconst linkedPayrollNamesByBankRow = new Map();\nconst linkedPayrollNumbersByBankRow = new Map();\n\nfor (const reconciliationRow of coreRows) {\n const linkedName =\n reconciliationRow.employee_name ||\n reconciliationRow.employee ||\n '';\n const linkedEmployeeNumber = normalizeAccount(\n reconciliationRow.employee_number ||\n reconciliationRow.employeeNumber ||\n ''\n );\n\n for (\n const bankSourceRow of\n reconciliationRow.bank_source_rows || []\n ) {\n const rowKey = bankRowKey(bankSourceRow);\n\n const names =\n linkedPayrollNamesByBankRow.get(rowKey) || [];\n const numbers =\n linkedPayrollNumbersByBankRow.get(rowKey) || [];\n\n if (linkedName) names.push(linkedName);\n if (linkedEmployeeNumber.length >= 6) {\n numbers.push(linkedEmployeeNumber);\n }\n\n linkedPayrollNamesByBankRow.set(\n rowKey,\n Array.from(new Set(names))\n );\n linkedPayrollNumbersByBankRow.set(\n rowKey,\n Array.from(new Set(numbers))\n );\n }\n}\n\nconst bambooMatchDetails = [];\nconst bambooExcludedPayments = [];\nconst bankWithoutBambooMap = new Map();\n\nif (bambooValidationAvailable) {\nfor (const bankRow of bankDetailRows) {\n if (isClearlyNonEmployeePayment(bankRow)) {\n bambooExcludedPayments.push({\n source_file: bankRow.source_file,\n row_number: bankRow.row_number,\n reason: 'pago_no_empleado_identificado',\n bank_name_file: bankRow.bank_name_file,\n bank_account_holder:\n bankRow.bank_account_holder,\n amount: bankRow.amount,\n currency: bankRow.currency,\n });\n continue;\n }\n\n const match = findBambooMatch(bankRow);\n\n if (match.found) {\n bambooMatchDetails.push({\n source_file: bankRow.source_file,\n row_number: bankRow.row_number,\n account: bankRow.account,\n amount: bankRow.amount,\n currency: bankRow.currency,\n bank_name_file: bankRow.bank_name_file,\n bank_account_holder:\n bankRow.bank_account_holder,\n matched_by: match.matched_by,\n confidence: roundMoney(match.confidence),\n bamboo_employee_number:\n match.employee?.employee_number || '',\n bamboo_employee_name:\n match.employee?.full_name || '',\n bamboo_status:\n match.employee?.status || '',\n bamboo_overlaps_period:\n Boolean(match.employee?.overlaps_period),\n });\n continue;\n }\n\n const displayName =\n bankRow.bank_name_file ||\n bankRow.bank_account_holder ||\n 'Pago bancario sin empleado identificado';\n\n const groupingKey = [\n normalizeAccount(bankRow.account),\n normalizeName(displayName),\n bankRow.currency || 'QTZ',\n ].join('|');\n\n const current =\n bankWithoutBambooMap.get(groupingKey) || {\n id: `bank_without_bamboo_${groupingKey}`,\n employee: displayName,\n employee_name: displayName,\n bank_name_file:\n bankRow.bank_name_file || '',\n bank_account_holder:\n bankRow.bank_account_holder || '',\n account: normalizeAccount(bankRow.account),\n bankAccount: normalizeAccount(bankRow.account),\n bank_account: normalizeAccount(bankRow.account),\n currency: bankRow.currency || 'QTZ',\n bankAmount: 0,\n bank_amount: 0,\n shipment_numbers: new Set(),\n references: new Set(),\n source_files: new Set(),\n source_rows: [],\n status: 'Pendiente revisión',\n category: 'banco_sin_bamboo',\n subcategory:\n 'pago_bancario_sin_empleado_bamboohr_gt',\n observation:\n 'Se encontró un pago en el banco, pero no se encontró una coincidencia confiable con un empleado de Guatemala en BambooHR.',\n best_bamboo_candidate:\n match.best_candidate\n ? {\n employee_number:\n match.best_candidate.employee\n ?.employee_number || '',\n employee_name:\n match.best_candidate.employee\n ?.full_name || '',\n score: roundMoney(\n match.best_candidate.score\n ),\n }\n : null,\n ambiguous_bamboo_match:\n Boolean(match.ambiguous),\n };\n\n current.bankAmount = roundMoney(\n current.bankAmount +\n Number(bankRow.amount || 0)\n );\n current.bank_amount = current.bankAmount;\n\n if (bankRow.shipment_number) {\n current.shipment_numbers.add(\n bankRow.shipment_number\n );\n }\n\n if (bankRow.reference) {\n current.references.add(bankRow.reference);\n }\n\n if (bankRow.source_file) {\n current.source_files.add(\n bankRow.source_file\n );\n }\n\n current.source_rows.push(bankRow);\n bankWithoutBambooMap.set(\n groupingKey,\n current\n );\n}\n}\n\nconst bankWithoutBamboo = Array.from(\n bankWithoutBambooMap.values()\n).map((row) => ({\n ...row,\n shipment_numbers: Array.from(\n row.shipment_numbers\n ),\n references: Array.from(row.references),\n source_files: Array.from(row.source_files),\n difference: roundMoney(\n 0 - row.bank_amount\n ),\n}));\n\nconst nameDifferenceRows = (\n data.bank?.name_differences || []\n).map((row) => ({\n id: row.id,\n employee: row.bank_name_file,\n employee_name: row.bank_name_file,\n employeeNumber: '',\n employee_number: '',\n account: row.account,\n payrollAccount: '',\n payroll_account: '',\n bankAccount: row.account,\n bank_account: row.account,\n currency: row.currency || 'QTZ',\n payrollAmount: 0,\n payroll_amount: 0,\n bankAmount: row.amount,\n bank_amount: row.amount,\n difference: 0,\n status: 'Pendiente revisión',\n category: 'diferencia_nombre_banco',\n subcategory: 'nombre_archivo_vs_cuentahabiente',\n observation: row.observation,\n bank_name_file: row.bank_name_file,\n bank_account_holder: row.bank_account_holder,\n source_file: row.source_file,\n shipment_number: row.shipment_number,\n plan_number: row.plan_number,\n reference: row.reference,\n row_number: row.row_number,\n}));\n\nfunction priority(row) {\n const category = String(\n row.category || ''\n ).toLowerCase();\n\n if (category === 'posible_cuenta_mal_digitada') return 1;\n if (category === 'discrepancia') return 2;\n if (category === 'banco_sin_nomina') return 3;\n if (category === 'nomina_sin_cuenta') return 4;\n if (category === 'diferencia_nombre_banco') return 5;\n if (category === 'coincidencia') return 99;\n\n return 50;\n}\n\nconst rowsFinales = [\n ...coreRows,\n ...nameDifferenceRows,\n].sort((a, b) => {\n const priorityDifference =\n priority(a) - priority(b);\n\n if (priorityDifference !== 0) {\n return priorityDifference;\n }\n\n return String(\n a.employee_name || ''\n ).localeCompare(\n String(b.employee_name || ''),\n 'es'\n );\n});\n\nconst appliedSupplementsTotal = roundMoney(\n appliedSupplements.reduce(\n (sum, row) => sum + row.payroll_amount,\n 0\n )\n);\n\nconst totalNominaBase = roundMoney(\n data.payroll?.total_amount || 0\n);\n\nconst totalNomina = roundMoney(\n totalNominaBase + appliedSupplementsTotal\n);\n\nconst totalBanco = roundMoney(\n data.bank?.total_amount || 0\n);\n\nconst diferenciasNombreBanco =\n nameDifferenceRows.length;\n\nconst pendientes =\n coreDiscrepancias +\n coreBancoSinNomina +\n coreNominaSinCuenta +\n diferenciasNombreBanco;\n\nconst unusedPotentialSupplements =\n potentialSupplements.filter((row) => {\n return !appliedSupplementKeys.has(\n supplementKey(row)\n );\n });\n\nconst bambooSummaryCompact = {\n ...(data.bamboo || {}),\n};\n\n/*\n * El arreglo completo de empleados solo se necesita dentro de este nodo.\n * No se reenvía a Google Sheets, Supabase ni al webhook para evitar cargar\n * cerca de 1 MB innecesario en todos los nodos posteriores.\n */\ndelete bambooSummaryCompact.employees;\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'cruce_nomina_completa_banco_condicional',\n errors: [],\n metadata: data.metadata || {},\n summary: {\n coincidencias: coreCoincidencias,\n discrepancias: coreDiscrepancias,\n bancoSinNomina: coreBancoSinNomina,\n bancoSinBamboo: bankWithoutBamboo.length,\n nominaSinCuenta: coreNominaSinCuenta,\n diferenciasNombreBanco,\n posiblesCuentasMalDigitadas:\n corePosiblesCuentas,\n pendientes,\n filasNominaValidas:\n data.payroll?.valid_rows_count || 0,\n filasNominaSinCuenta:\n data.payroll?.no_account_rows_count || 0,\n suplementosPotenciales:\n potentialSupplements.length,\n suplementosNominaAplicados:\n appliedSupplements.length,\n suplementosNominaNoAplicados:\n unusedPotentialSupplements.length,\n suplementosNominaAdjuntados:\n appliedSupplements.length,\n suplementosNominaNoAdjuntados:\n data.payroll?.unattached_supplements_count || 0,\n reconciliacionesExactasFinales:\n finalExactReconciliations.length,\n cuentasNominaAgrupadas:\n payrollAccounts.length,\n transaccionesBanco:\n data.bank?.rows_count || 0,\n cuentasBancoAgrupadas:\n bankAccounts.length,\n empleadosBambooGT:\n bambooEmployees.length,\n empleadosBambooEnPeriodo:\n Number(\n data.bamboo?.active_in_period_count || 0\n ),\n bambooPaginasDescargadas:\n Number(\n data.bamboo?.pages_fetched || 0\n ),\n bambooEmpleadosEsperados:\n Number(\n data.bamboo?.expected_total || 0\n ),\n bambooDescargaCompleta:\n Boolean(\n data.bamboo?.fetch_complete\n ),\n bambooValidacionDisponible:\n bambooValidationAvailable,\n totalNominaBase,\n totalSuplementosAplicados:\n appliedSupplementsTotal,\n totalNomina,\n totalBanco,\n diferenciaTotal:\n moneyDiff(totalNomina, totalBanco),\n },\n rows: rowsFinales,\n bankWithoutBamboo,\n nameDifferences: nameDifferenceRows,\n bambooSummary: bambooSummaryCompact,\n reportUrl: null,\n debug: {\n sheet_summaries:\n data.payroll?.sheet_summaries || [],\n potential_supplements:\n potentialSupplements,\n applied_supplements:\n appliedSupplements,\n final_exact_reconciliations:\n finalExactReconciliations,\n bamboo_search:\n {\n employees_indexed:\n bambooSearch.records.length,\n exact_aliases:\n bambooSearch.exactAliasMap.size,\n indexed_tokens:\n bambooSearch.tokenIndex.size,\n cache_entries:\n bambooMatchCache.size,\n },\n bamboo_matches:\n bambooMatchDetails,\n bamboo_excluded_payments:\n bambooExcludedPayments,\n bamboo_validation_available:\n bambooValidationAvailable,\n bamboo_validation_warning:\n bambooValidationWarning,\n banco_sin_bamboo:\n bankWithoutBamboo,\n unused_potential_supplements:\n unusedPotentialSupplements,\n unattached_supplements:\n data.debug_payroll?.unattached_supplements || [],\n payroll_preview:\n payrollAccounts.slice(0, 10),\n bank_preview:\n bankAccounts.slice(0, 10),\n payroll_no_account_preview:\n payrollNoAccountRows.slice(0, 10),\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 21696, - 26512 - ], - "id": "a9aeea33-6818-40c1-9eb9-b6a3521a4f66", - "name": "Cruzar Nómina vs Banco" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "1) Nomina General" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15648, - 25344 - ], - "id": "3e169c34-36e6-40d2-9751-4d3efdcf8c08", - "name": "Extract - Nomina General", - "retryOnFail": false - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "2) Temporales" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15648, - 25584 - ], - "id": "c30318b3-98e8-44bf-8c0b-315f8a54f1e8", - "name": "Extract - Temporales", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "3) Auditorias" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15648, - 25824 - ], - "id": "b7b0d02e-7e91-428e-aecd-3a8a08e77175", - "name": "Extract - Auditorias", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "4) Bono Mariana" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15664, - 26016 - ], - "id": "cdfb15a2-d9d0-4544-80e1-e798e42e796f", - "name": "Extract - Bono Mariana", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "5) Movilidad WP" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15664, - 26224 - ], - "id": "12b2497a-642d-4f4a-b32c-d4faa535f565", - "name": "Extract - Movilidad WP", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "6)Viaticos PMI" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15664, - 26432 - ], - "id": "d33f7284-0947-44ad-8a03-47a0b8bb4122", - "name": "Extract - Viaticos PMI", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "7) Combustible Purina" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15680, - 26768 - ], - "id": "5b82536d-1e30-4fdf-9812-b2f93e8a19c9", - "name": "Extract - Combustible Purina", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "8) Combustible P&G" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15664, - 27104 - ], - "id": "fc8edacd-6084-42b1-8aa1-cbd18e96bf1b", - "name": "Extract - Combustible PG", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "9) Combustibles Liquidables" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15664, - 27392 - ], - "id": "ee4ee831-169e-4d33-88be-4d0d704ec095", - "name": "Extract - Combustibles Liquidables", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "jsCode": "function normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeForCompare(value) {\n return normalizeText(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeAccount(value) {\n if (value === null || value === undefined || value === '') return '';\n\n if (typeof value === 'number') {\n return String(Math.trunc(value)).trim();\n }\n\n return String(value)\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim();\n}\n\nfunction parseMoney(value) {\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value : 0;\n }\n\n const cleaned = String(value ?? '')\n .replace(/USD/gi, '')\n .replace(/GTQ/gi, '')\n .replace(/QTZ/gi, '')\n .replace(/Q/gi, '')\n .replace(/,/g, '')\n .replace(/\\s+/g, '')\n .trim();\n\n const parsed = Number.parseFloat(cleaned);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction roundMoney(value) {\n return Math.round((Number(value) || 0) * 100) / 100;\n}\n\nfunction getValue(row, possibleKeys) {\n for (const key of possibleKeys) {\n if (row[key] !== undefined && row[key] !== null && row[key] !== '') {\n return row[key];\n }\n }\n\n const rowKeys = Object.keys(row || {});\n\n for (const wanted of possibleKeys) {\n const wantedNormalized = normalizeForCompare(wanted);\n const found = rowKeys.find((key) => normalizeForCompare(key) === wantedNormalized);\n\n if (found && row[found] !== undefined && row[found] !== null && row[found] !== '') {\n return row[found];\n }\n }\n\n return '';\n}\n\nfunction getNodeRows(nodeName) {\n try {\n return $items(nodeName)\n .map((item) => item.json || {})\n .filter((row) => {\n const text = JSON.stringify(row || {}).toLowerCase();\n if (text.includes('spreadsheet does not contain sheet')) return false;\n if (text.includes('no sheet')) return false;\n if (row.error) return false;\n return true;\n });\n } catch (error) {\n return [];\n }\n}\n\nfunction isProbablyInvalidEmployeeName(value) {\n const name = normalizeText(value);\n const lower = normalizeForCompare(name);\n\n if (!name) return true;\n if (/^[\\d.,\\s]+$/.test(name)) return true;\n\n const invalidExact = new Set([\n 'nombre', 'nombre completo', 'empleado', 'colaborador', 'cuenta',\n 'cuenta bancaria', 'total', 'subtotal', 'gran total', 'total general',\n 'guatemala', 'coordinador', 'supervisor', 'kam', 'pais', 'país',\n 'proyecto', 'cliente', 'marca', 'canal', 'concepto', 'descripcion',\n 'descripción', 'ejecutado en tarjeta', 'ejecutado en efectivo',\n 'ajustes segun comentarios', 'ajuste segun comentarios', 'comentarios',\n 'disponible', 'ejecutado', 'pendiente'\n ]);\n\n if (invalidExact.has(lower)) return true;\n\n const invalidContains = [\n 'total ', 'total:', 'subtotal', 'resumen', 'observacion', 'observación',\n 'monto', 'cuenta', 'banco', 'nomina', 'nómina', 'bonificacion',\n 'bonificación', 'departamento', 'puesto', 'posicion', 'posición',\n 'spoc', 'gema hsm', 'gema hfs', 'hsm-dpp', 'lider de ejecucion',\n 'líder de ejecución', 'coordinador nacional', 'ejecutado en tarjeta',\n 'ejecutado en efectivo', 'ajustes segun comentarios',\n 'ajuste segun comentarios', 'segun comentarios', 'comentarios',\n 'presupuesto combustible', 'credito 30 dias', 'crédito 30 días',\n 'no se deposita', 'ejecucion mensual', 'ejecución mensual'\n ];\n\n if (invalidContains.some((token) => lower.includes(normalizeForCompare(token)))) {\n return true;\n }\n\n const words = name.split(/\\s+/).filter(Boolean);\n return words.length < 2;\n}\n\nfunction isValidCrossableAccount(account) {\n const normalized = normalizeAccount(account);\n return normalized.length >= 7 && !/^0+$/.test(normalized);\n}\n\nfunction buildRow({ sourceSheet, rowNumber, employeeName, account, email, amount, currency = 'QTZ', extra = {} }) {\n return {\n source_sheet: sourceSheet,\n row_number: rowNumber,\n employee_name: normalizeText(employeeName),\n employee_number: null,\n account: normalizeAccount(account),\n email: normalizeText(email).toLowerCase(),\n payroll_amount: roundMoney(amount),\n currency,\n ...extra,\n };\n}\n\nconst payrollRows = [];\nconst noAccountRows = [];\nconst supplementRows = [];\nconst ignoredRows = [];\nconst sheetSummaries = [];\n\nfunction addNormalizedRow(row, options = {}) {\n const amount = roundMoney(row.payroll_amount);\n\n if (amount <= 0) {\n ignoredRows.push({ ...row, reason: 'amount_zero_or_invalid' });\n return 'ignored';\n }\n\n if (amount > 150000) {\n ignoredRows.push({ ...row, reason: 'suspicious_large_amount' });\n return 'ignored';\n }\n\n if (isProbablyInvalidEmployeeName(row.employee_name)) {\n ignoredRows.push({ ...row, reason: 'invalid_employee_name' });\n return 'ignored';\n }\n\n if (options.supplementWithoutAccount) {\n supplementRows.push({ ...row, account: '' });\n return 'supplement';\n }\n\n if (!isValidCrossableAccount(row.account)) {\n noAccountRows.push({\n ...row,\n account: '',\n status: 'Pendiente revisión',\n observation: 'Tiene monto en nómina, pero no tiene cuenta bancaria válida para cruzar contra banco.',\n });\n return 'no_account';\n }\n\n payrollRows.push(row);\n return 'valid';\n}\n\nfunction addSheetSummary(sourceSheet, rawRows, validRows, noAccountCount, ignoredCount, supplementCount, totalAmount) {\n sheetSummaries.push({\n source_sheet: sourceSheet,\n raw_rows_count: rawRows,\n valid_rows_count: validRows,\n no_account_rows_count: noAccountCount,\n supplement_rows_count: supplementCount,\n ignored_rows_count: ignoredCount,\n total_amount: roundMoney(totalAmount),\n });\n}\n\nfunction processStandardSheet(config) {\n const rows = getNodeRows(config.nodeName);\n let validRows = 0;\n let noAccountCount = 0;\n let ignoredCount = 0;\n let sheetTotal = 0;\n\n rows.forEach((row, index) => {\n const employeeName = normalizeText(getValue(row, config.nameKeys));\n const account = normalizeAccount(getValue(row, config.accountKeys));\n const email = normalizeText(getValue(row, config.emailKeys || [])).toLowerCase();\n const amount = roundMoney(parseMoney(getValue(row, config.amountKeys)));\n const currencyValue = normalizeText(getValue(row, config.currencyKeys || []));\n const currency = normalizeForCompare(currencyValue).includes('dolar') || currencyValue.toUpperCase().includes('USD')\n ? 'USD'\n : 'QTZ';\n\n const normalizedRow = buildRow({\n sourceSheet: config.sourceSheet,\n rowNumber: index + 2,\n employeeName,\n account,\n email,\n amount,\n currency,\n });\n\n const result = addNormalizedRow(normalizedRow);\n if (result === 'valid') validRows += 1;\n else if (result === 'no_account') noAccountCount += 1;\n else ignoredCount += 1;\n\n if (result === 'valid' || result === 'no_account') {\n sheetTotal = roundMoney(sheetTotal + amount);\n }\n });\n\n addSheetSummary(config.sourceSheet, rows.length, validRows, noAccountCount, ignoredCount, 0, sheetTotal);\n}\n\nfunction valuesFromRow(row) {\n // Los Extract configurados con Header Row desactivado devuelven cada fila\n // dentro de una propiedad `row` como arreglo posicional.\n // Si usamos Object.values(row), obtenemos un arreglo anidado y el\n // normalizador no puede detectar cuenta, nombre, ID ni monto.\n const rawValues = Array.isArray(row?.row)\n ? row.row\n : Object.values(row || {});\n\n return rawValues.filter((value) => {\n return value !== null &&\n value !== undefined &&\n normalizeText(value) !== '';\n });\n}\n\nfunction looksLikeAccount(value) {\n const account = normalizeAccount(value);\n return account.length >= 6 && account.length <= 14;\n}\n\nfunction looksLikeMoney(value) {\n const amount = roundMoney(parseMoney(value));\n return amount > 0 && amount <= 150000;\n}\n\nfunction looksLikeName(value) {\n const text = normalizeText(value);\n if (!text || /\\d/.test(text)) return false;\n return !isProbablyInvalidEmployeeName(text);\n}\n\nfunction findNameAfter(values, startIndex) {\n for (let i = Math.max(0, startIndex); i < values.length; i++) {\n if (looksLikeName(values[i])) return { value: values[i], index: i };\n }\n return { value: '', index: -1 };\n}\n\nfunction processPositionalSheet(config) {\n const rows = getNodeRows(config.nodeName);\n let validRows = 0;\n let noAccountCount = 0;\n let ignoredCount = 0;\n let supplementCount = 0;\n let sheetTotal = 0;\n\n rows.forEach((row, index) => {\n const values = valuesFromRow(row);\n const rowText = normalizeForCompare(values.join(' '));\n\n if (config.excludeIfContains?.some((token) => rowText.includes(normalizeForCompare(token)))) {\n ignoredCount += 1;\n return;\n }\n\n let employeeName = '';\n let account = '';\n let email = '';\n let amount = 0;\n let supplementWithoutAccount = false;\n let extra = {};\n\n if (config.sourceSheet === 'Temporales WMC') {\n const accountIndex = values.findIndex(looksLikeAccount);\n account = accountIndex >= 0 ? normalizeAccount(values[accountIndex]) : '';\n const foundName = findNameAfter(values, accountIndex + 1);\n employeeName = foundName.value;\n\n const moneyCandidates = values\n .slice(foundName.index + 1)\n .map((value) => roundMoney(parseMoney(value)))\n .filter((candidate) => candidate > 0 && candidate <= 150000);\n\n amount = moneyCandidates.length ? moneyCandidates[moneyCandidates.length - 1] : 0;\n } else if (config.sourceSheet === '6)Viaticos PMI') {\n const accountIndex = values.findIndex(looksLikeAccount);\n account = accountIndex >= 0 ? normalizeAccount(values[accountIndex]) : '';\n employeeName = findNameAfter(values, accountIndex + 1).value;\n const moneyCandidates = values\n .slice(accountIndex + 1)\n .map((value) => roundMoney(parseMoney(value)))\n .filter((candidate) => candidate > 0 && candidate <= 150000);\n amount = moneyCandidates.length ? moneyCandidates[moneyCandidates.length - 1] : 0;\n } else if (config.sourceSheet === '7) Combustible Purina') {\n const accountIndex = values.findIndex(looksLikeAccount);\n account = accountIndex >= 0 ? normalizeAccount(values[accountIndex]) : '';\n\n for (let i = accountIndex - 1; i >= 0; i--) {\n if (looksLikeName(values[i])) {\n employeeName = normalizeText(values[i]);\n break;\n }\n }\n\n for (let i = accountIndex - 1; i >= 0; i--) {\n const candidate = roundMoney(parseMoney(values[i]));\n if (candidate > 0 && candidate <= 150000) {\n amount = candidate;\n break;\n }\n }\n } else if (config.sourceSheet === '8) Combustible P&G') {\n const accountIndex = values.findIndex(looksLikeAccount);\n account = accountIndex >= 0 ? normalizeAccount(values[accountIndex]) : '';\n const foundName = findNameAfter(values, accountIndex + 1);\n employeeName = foundName.value;\n\n for (let i = foundName.index + 1; i < values.length; i++) {\n const candidate = roundMoney(parseMoney(values[i]));\n if (candidate > 0 && candidate <= 150000) {\n amount = candidate;\n break;\n }\n }\n } else if (config.sourceSheet === '9) Combustibles Liquidables') {\n account = normalizeAccount(values[0]);\n employeeName = normalizeText(values[2]);\n email = normalizeText(values[6]).toLowerCase();\n const moneyCandidates = values\n .map((value) => roundMoney(parseMoney(value)))\n .filter((candidate) => candidate > 0 && candidate <= 150000);\n amount = moneyCandidates.length ? moneyCandidates[moneyCandidates.length - 1] : 0;\n } else if (\n config.sourceSheet === 'Combustibles PMI' ||\n config.sourceSheet === 'Combustible Tarjeta Motorola'\n ) {\n const idIndex = values.findIndex((value) => {\n const normalized = normalizeForCompare(value);\n return /^(spoc|xpert|moto)/.test(normalized);\n });\n\n // Opción B:\n // Solo procesar filas de la tabla principal que tengan un ID operativo\n // (SPOC / XPERT / MOTO). Esto excluye encabezados, totales y la lista\n // auxiliar/duplicada que aparece debajo de la tabla principal.\n if (idIndex < 0) {\n ignoredCount += 1;\n return;\n }\n\n const foundName = findNameAfter(values, idIndex + 1);\n employeeName = foundName.value;\n\n // Tomar únicamente el primer monto positivo después del nombre:\n // corresponde a la columna Asignado de la tabla principal.\n for (let i = foundName.index + 1; i < values.length; i++) {\n const candidate = roundMoney(parseMoney(values[i]));\n if (candidate > 0 && candidate <= 150000) {\n amount = candidate;\n break;\n }\n }\n\n supplementWithoutAccount = true;\n extra = {\n supplement_id: idIndex >= 0 ? normalizeText(values[idIndex]) : '',\n supplement_original_name: employeeName,\n };\n }\n\n const normalizedRow = buildRow({\n sourceSheet: config.sourceSheet,\n rowNumber: index + 1,\n employeeName,\n account,\n email,\n amount,\n extra,\n });\n\n const result = addNormalizedRow(normalizedRow, { supplementWithoutAccount });\n if (result === 'valid') validRows += 1;\n else if (result === 'no_account') noAccountCount += 1;\n else if (result === 'supplement') supplementCount += 1;\n else ignoredCount += 1;\n\n if (result !== 'ignored') sheetTotal = roundMoney(sheetTotal + amount);\n });\n\n addSheetSummary(\n config.sourceSheet,\n rows.length,\n validRows,\n noAccountCount,\n ignoredCount,\n supplementCount,\n sheetTotal\n );\n}\n\nconst standardSheetConfigs = [\n {\n nodeName: 'Extract - Nomina General',\n sourceSheet: '1) Nomina General',\n accountKeys: ['Cuenta', 'CUENTA', 'Cuenta Bancaria', 'CUENTA BANCARIA'],\n nameKeys: ['NOMBRE COMPLETO', 'Nombre Completo', 'NOMBRE', 'Nombre'],\n emailKeys: ['EMAIL', 'Email', 'CORREO', 'Correo'],\n amountKeys: ['NETO A PAGAR', 'Neto a Pagar', 'NETO', 'Neto'],\n currencyKeys: ['Moneda', 'MONEDA'],\n },\n {\n nodeName: 'Extract - Temporales',\n sourceSheet: '2) Temporales',\n accountKeys: ['Cuenta', 'CUENTA', 'Cuenta Bancaria', 'CUENTA BANCARIA'],\n nameKeys: ['NOMBRE', 'Nombre', 'NOMBRE COMPLETO', 'Nombre Completo'],\n emailKeys: ['EMAIL', 'Email', 'CORREO', 'Correo'],\n amountKeys: ['NETO A PAGAR', 'Neto a Pagar', 'NETO', 'Neto'],\n },\n {\n nodeName: 'Extract - Auditorias',\n sourceSheet: '3) Auditorias',\n accountKeys: ['Cuenta', 'CUENTA', 'Cuenta Bancaria', 'CUENTA BANCARIA'],\n nameKeys: ['NOMBRE COMPLETO', 'Nombre Completo', 'NOMBRE', 'Nombre'],\n emailKeys: ['EMAIL', 'Email', 'CORREO', 'Correo'],\n amountKeys: ['NETO A PAGAR', 'Neto a Pagar', 'NETO', 'Neto'],\n currencyKeys: ['Moneda', 'MONEDA'],\n },\n {\n nodeName: 'Extract - Bono Mariana',\n sourceSheet: '4) Bono Mariana',\n accountKeys: ['Cuenta', 'CUENTA', 'Cuenta Bancaria', 'CUENTA BANCARIA'],\n nameKeys: ['NOMBRE COMPLETO', 'Nombre Completo', 'NOMBRE', 'Nombre'],\n emailKeys: ['EMAIL', 'Email', 'CORREO', 'Correo'],\n amountKeys: ['NETO A PAGAR', 'Neto a Pagar', 'NETO', 'Neto', 'MONTO', 'Monto'],\n },\n {\n nodeName: 'Extract - Movilidad WP',\n sourceSheet: '5) Movilidad WP',\n accountKeys: ['Cuenta', 'CUENTA', 'Cuenta Bancaria', 'CUENTA BANCARIA'],\n nameKeys: ['NOMBRE', 'Nombre', 'NOMBRE COMPLETO', 'Nombre Completo'],\n emailKeys: ['EMAIL', 'Email', 'CORREO', 'Correo'],\n amountKeys: ['NETO A PAGAR', 'Neto a Pagar', 'NETO', 'Neto'],\n },\n {\n nodeName: 'Extract - Mot Variable Abril',\n sourceSheet: 'Mot Variable Abril',\n accountKeys: ['Cuenta', 'CUENTA', 'Cuenta Bancaria', 'CUENTA BANCARIA'],\n nameKeys: ['NOMBRE COMPLETO', 'Nombre Completo', 'NOMBRE', 'Nombre', 'Empleado', 'EMPLEADO'],\n emailKeys: ['EMAIL', 'Email', 'CORREO', 'Correo'],\n amountKeys: ['MONTO A PAGAR', 'Monto a pagar', 'NETO A PAGAR', 'Neto a Pagar', 'MONTO NETO', 'Monto Neto', 'VALOR A PAGAR', 'Valor a pagar'],\n },\n];\n\nconst positionalSheetConfigs = [\n { nodeName: 'Extract - Temporales WMC', sourceSheet: 'Temporales WMC' },\n { nodeName: 'Extract - Viaticos PMI', sourceSheet: '6)Viaticos PMI' },\n { nodeName: 'Extract - Combustible Purina', sourceSheet: '7) Combustible Purina' },\n { nodeName: 'Extract - Combustible PG', sourceSheet: '8) Combustible P&G' },\n { nodeName: 'Extract - Combustibles Liquidables', sourceSheet: '9) Combustibles Liquidables' },\n];\n\nfor (const config of standardSheetConfigs) processStandardSheet(config);\nfor (const config of positionalSheetConfigs) processPositionalSheet(config);\n\nfunction nameWords(value) {\n const ignored = new Set(['de', 'del', 'la', 'las', 'los', 'y', 'e', 'el']);\n return normalizeForCompare(value)\n .split(' ')\n .filter((word) => word.length > 1 && !ignored.has(word));\n}\n\nfunction editDistance(a, b) {\n if (a === b) return 0;\n if (!a) return b.length;\n if (!b) return a.length;\n\n const previous = Array.from({ length: b.length + 1 }, (_, index) => index);\n\n for (let i = 1; i <= a.length; i++) {\n const current = [i];\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + cost);\n }\n for (let j = 0; j < current.length; j++) previous[j] = current[j];\n }\n\n return previous[b.length];\n}\n\nfunction tokenMatches(a, b) {\n if (a === b) return true;\n const minLength = Math.min(a.length, b.length);\n if (minLength >= 8 && editDistance(a, b) <= 2) return true;\n if (minLength >= 5 && editDistance(a, b) <= 1) return true;\n return false;\n}\n\nfunction nameMatchScore(a, b) {\n const wordsA = nameWords(a);\n const wordsB = nameWords(b);\n if (!wordsA.length || !wordsB.length) return 0;\n\n const usedB = new Set();\n let matches = 0;\n\n for (const wordA of wordsA) {\n const matchIndex = wordsB.findIndex((wordB, index) => {\n return !usedB.has(index) && tokenMatches(wordA, wordB);\n });\n\n if (matchIndex >= 0) {\n usedB.add(matchIndex);\n matches += 1;\n }\n }\n\n const ratio = matches / Math.min(wordsA.length, wordsB.length);\n const firstTokenBonus = tokenMatches(wordsA[0], wordsB[0]) ? 0.15 : 0;\n return ratio + firstTokenBonus;\n}\n\nconst accountNameMap = new Map();\nfor (const row of payrollRows) {\n const current = accountNameMap.get(row.account) || {\n account: row.account,\n employee_name: row.employee_name,\n currency: row.currency,\n };\n accountNameMap.set(row.account, current);\n}\n\nconst canonicalPayrollPeople = Array.from(accountNameMap.values());\nconst attachedSupplements = [];\nconst unattachedSupplements = [];\n\nfor (const supplement of supplementRows) {\n const scored = canonicalPayrollPeople\n .map((candidate) => ({\n ...candidate,\n score: nameMatchScore(supplement.employee_name, candidate.employee_name),\n }))\n .sort((a, b) => b.score - a.score);\n\n const top = scored[0];\n const second = scored[1];\n const supplementTokens = nameWords(supplement.employee_name);\n const firstToken = supplementTokens[0] || '';\n const sameFirstTokenCandidates = canonicalPayrollPeople.filter((candidate) => {\n const candidateFirst = nameWords(candidate.employee_name)[0] || '';\n return firstToken && candidateFirst === firstToken;\n });\n\n const confidentByScore = top && top.score >= 0.75 && (!second || top.score - second.score >= 0.05 || top.score >= 1);\n const confidentShortUniqueFirstName = top && supplementTokens.length <= 2 && top.score >= 0.6 && sameFirstTokenCandidates.length === 1;\n\n if (confidentByScore || confidentShortUniqueFirstName) {\n const attached = {\n ...supplement,\n account: top.account,\n employee_name: top.employee_name,\n currency: top.currency || supplement.currency || 'QTZ',\n supplement_original_name: supplement.employee_name,\n supplement_match_score: roundMoney(top.score),\n attached_by_name: true,\n };\n\n // Opción B condicional:\n // El suplemento queda vinculado al empleado y a su cuenta, pero NO se\n // suma todavía a la nómina base. El nodo de cruce decidirá si debe\n // aplicarse según el monto realmente pagado por el banco.\n attachedSupplements.push(attached);\n } else {\n unattachedSupplements.push({\n ...supplement,\n best_candidate: top?.employee_name || '',\n best_score: roundMoney(top?.score || 0),\n });\n\n ignoredRows.push({\n ...supplement,\n reason: 'supplement_without_unique_payroll_match',\n best_candidate: top?.employee_name || '',\n best_score: roundMoney(top?.score || 0),\n });\n }\n}\n\nconst groupedMap = new Map();\n\nfor (const row of payrollRows) {\n const groupKey = `${row.account}:${row.currency || 'QTZ'}`;\n const current = groupedMap.get(groupKey) || {\n group_key: groupKey,\n account: row.account,\n employee_name: row.employee_name,\n employee_number: null,\n email: row.email,\n currency: row.currency || 'QTZ',\n payroll_amount: 0,\n rows_count: 0,\n source_sheets: new Set(),\n source_rows: [],\n };\n\n current.payroll_amount = roundMoney(current.payroll_amount + row.payroll_amount);\n current.rows_count += 1;\n if (!current.email && row.email) current.email = row.email;\n current.source_sheets.add(row.source_sheet);\n current.source_rows.push({\n source_sheet: row.source_sheet,\n row_number: row.row_number,\n amount: row.payroll_amount,\n supplement_original_name: row.supplement_original_name || '',\n attached_by_name: Boolean(row.attached_by_name),\n });\n\n groupedMap.set(groupKey, current);\n}\n\nconst groupedByAccount = Array.from(groupedMap.values()).map((row) => ({\n ...row,\n source_sheets: Array.from(row.source_sheets),\n}));\n\nconst totalsByCurrency = {};\nfor (const row of [...payrollRows, ...noAccountRows]) {\n const currency = row.currency || 'QTZ';\n totalsByCurrency[currency] = roundMoney((totalsByCurrency[currency] || 0) + row.payroll_amount);\n}\n\nconst totalPayroll = roundMoney(\n payrollRows.reduce((sum, row) => sum + row.payroll_amount, 0) +\n noAccountRows.reduce((sum, row) => sum + row.payroll_amount, 0)\n);\n\nreturn [\n {\n json: {\n payroll: {\n source: 'template_guatemala_completo',\n sheets_count: standardSheetConfigs.length + positionalSheetConfigs.length,\n sheet_summaries: sheetSummaries,\n raw_rows_count: sheetSummaries.reduce((sum, sheet) => sum + sheet.raw_rows_count, 0),\n valid_rows_count: payrollRows.length,\n no_account_rows_count: noAccountRows.length,\n ignored_rows_count: ignoredRows.length,\n grouped_accounts_count: groupedByAccount.length,\n // Los suplementos potenciales no forman parte del total base\n // hasta que el banco confirme que hubo un pago adicional.\n attached_supplements_count: 0,\n potential_supplements_count: attachedSupplements.length,\n potential_supplements: attachedSupplements,\n unattached_supplements_count: unattachedSupplements.length,\n total_amount: totalPayroll,\n totals_by_currency: totalsByCurrency,\n rows: payrollRows,\n no_account_rows: noAccountRows,\n grouped_by_account: groupedByAccount,\n },\n debug_payroll: {\n // Se conserva attached_supplements por compatibilidad con las\n // revisiones anteriores, pero ahora representa suplementos\n // potenciales vinculados, todavía no aplicados.\n attached_supplements: attachedSupplements,\n potential_supplements: attachedSupplements,\n unattached_supplements: unattachedSupplements,\n ignored_rows_preview: ignoredRows.slice(0, 100),\n no_account_rows_preview: noAccountRows.slice(0, 50),\n },\n },\n },\n];\n" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 19872, - 27120 - ], - "id": "44044451-fb64-44a9-8cd0-109b64855806", - "name": "Normalizar Nómina Completa" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 16672, - 25648 - ], - "id": "5e8d1212-8780-40c0-ac54-eb65141bd71f", - "name": "Merge Hojas 01-02" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 16864, - 25856 - ], - "id": "fabe0277-9275-4bb9-a4e4-606fc7e7a6ad", - "name": "Merge Hojas 03" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 17152, - 25984 - ], - "id": "9730a653-c2b3-49bf-890f-56f2a6ba2567", - "name": "Merge Hojas 04" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 17568, - 26112 - ], - "id": "e1672eb2-31e0-4040-9971-c3eb99972e3f", - "name": "Merge Hojas 05" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 17872, - 26256 - ], - "id": "2906d58d-35b0-4112-a709-8b3097fd2537", - "name": "Merge Hojas 06" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 18208, - 26528 - ], - "id": "2b809028-6720-49c0-9a9c-f14cffa78c14", - "name": "Merge Hojas 07" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 18464, - 26656 - ], - "id": "652101ad-34b6-4893-8e83-683ad7e78fd4", - "name": "Merge Hojas 08" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 18768, - 27040 - ], - "id": "3af433bb-32fe-4c06-bea7-d00e9fea9c95", - "name": "Merge Hojas 09" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "Mot Variable Abril" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15664, - 27648 - ], - "id": "f8d6e3d6-7448-4a54-99e6-eec7f4fdec7b", - "name": "Extract - Mot Variable Abril", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 19072, - 27152 - ], - "id": "4672d0c6-3116-42b5-b1f3-d7807b5313e1", - "name": "Merge Hojas " - }, - { - "parameters": { - "jsCode": "const data = $input.first().json || {};\n\nfunction normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction roundMoney(value) {\n return Math.round((Number(value) || 0) * 100) / 100;\n}\n\nfunction readableCategory(value) {\n const map = {\n coincidencia: 'Coincidencia',\n discrepancia: 'Discrepancia',\n posible_cuenta_mal_digitada: 'Posible cuenta mal digitada',\n banco_sin_nomina: 'Banco sin nómina',\n banco_sin_bamboo: 'Banco sin Bamboo',\n nomina_sin_cuenta: 'Nómina sin cuenta',\n diferencia_nombre_banco: 'Diferencia nombre banco',\n };\n\n return map[value] || normalizeText(value).replace(/_/g, ' ');\n}\n\nfunction firstValue(value) {\n if (Array.isArray(value)) {\n return value.filter(Boolean).join(' / ');\n }\n\n return normalizeText(value);\n}\n\nfunction formatPeriodEnd(value) {\n const raw = normalizeText(value);\n\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(raw)) {\n return raw;\n }\n\n const [year, month, day] = raw.split('-');\n const monthNames = {\n '01': 'ene',\n '02': 'feb',\n '03': 'mar',\n '04': 'abr',\n '05': 'may',\n '06': 'jun',\n '07': 'jul',\n '08': 'ago',\n '09': 'sep',\n '10': 'oct',\n '11': 'nov',\n '12': 'dic',\n };\n\n return `${day}-${monthNames[month] || month}-${year}`;\n}\n\nfunction mainReportSense(row, difference) {\n const category = normalizeText(row.category).toLowerCase();\n const subcategory = normalizeText(row.subcategory).toLowerCase();\n\n if (category === 'posible_cuenta_mal_digitada') {\n return 'Revisar cuenta';\n }\n\n if (\n subcategory === 'nomina_con_cuenta_sin_pago_banco' ||\n (Number(row.bank_amount ?? row.bankAmount ?? 0) === 0 &&\n Number(row.payroll_amount ?? row.payrollAmount ?? 0) > 0)\n ) {\n return 'No aparece pagado en banco';\n }\n\n if (difference > 0) {\n return 'Se pagó de menos';\n }\n\n if (difference < 0) {\n return 'Se pagó de más';\n }\n\n return 'Revisar';\n}\n\nconst metadata = data.metadata || {};\nconst summary = data.summary || {};\nconst rows = Array.isArray(data.rows) ? data.rows : [];\nconst bankWithoutBamboo = Array.isArray(data.bankWithoutBamboo)\n ? data.bankWithoutBamboo\n : [];\n\nconst periodLabel =\n metadata.period_label ||\n `${metadata.year || ''}-${metadata.month || ''}-${metadata.period_type || ''}`;\n\nconst spreadsheetTitle =\n `Cruce de Cuentas GLM GT - ${periodLabel}`;\n\nconst mainReportSubtitle =\n `Diferencias de Monto Nómina vs. Banco · Guatemala · ${\n formatPeriodEnd(metadata.period_end || '')\n }`;\n\nconst bancoSinBambooSubtitle =\n `Pagos en banco sin empleado identificado en BambooHR · Guatemala · ${\n formatPeriodEnd(metadata.period_end || '')\n }`;\n\nconst cuentaMalDigitadaSubtitle =\n `Cuenta Mal Digitada en Nómina · Guatemala · ${\n formatPeriodEnd(metadata.period_end || '')\n }`;\n\nconst cuentaMalDigitadaCases = rows.filter(\n (row) => row.category === 'posible_cuenta_mal_digitada'\n);\n\nconst hasCuentaMalDigitada =\n cuentaMalDigitadaCases.length > 0;\n\nconst sheetIds = {\n nominaVsBanco: 101,\n bancoSinNomina: 102,\n bancoSinBamboo: 103,\n diferenciasNombreBanco: 104,\n cuentaMalDigitada: 105,\n resumen: 106,\n};\n\nconst sheetTitles = {\n nominaVsBanco: '01 Nómina vs Banco',\n bancoSinNomina: '02 Banco sin Nómina',\n bancoSinBamboo: '03 Banco sin Bamboo',\n diferenciasNombreBanco: '04 Diferencias nombre banco',\n cuentaMalDigitada: '05 Cuenta Mal Digitada',\n resumen: hasCuentaMalDigitada\n ? '06 Resumen'\n : '05 Resumen',\n};\n\nconst nominaVsBancoHeader = [\n '#',\n 'Empleado',\n 'Cuenta',\n 'Monto en Nómina (Q)',\n 'Monto en Banco (Q)',\n 'Diferencia (Q)',\n 'Sentido',\n 'Estado',\n 'Resolución',\n];\n\nconst mainReportRows = rows\n .filter((row) => row.category === 'discrepancia')\n .map((row, index) => {\n const payrollAmount = roundMoney(\n row.payroll_amount ?? row.payrollAmount ?? 0\n );\n const bankAmount = roundMoney(\n row.bank_amount ?? row.bankAmount ?? 0\n );\n const difference = roundMoney(\n row.difference ?? (payrollAmount - bankAmount)\n );\n\n return [\n index + 1,\n normalizeText(row.employee_name || row.employee || ''),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.payroll_account ||\n row.payrollAccount ||\n row.account ||\n ''\n ),\n payrollAmount,\n bankAmount,\n difference,\n mainReportSense(row, difference),\n normalizeText(row.status || 'Riesgo').toUpperCase(),\n '',\n ];\n });\n\nconst nominaVsBancoValues = [\n [\n 'GOMEZLEE MARKETING',\n '',\n '',\n '',\n '',\n '',\n '',\n '',\n '',\n ],\n [\n mainReportSubtitle,\n '',\n '',\n '',\n '',\n '',\n '',\n '',\n '',\n ],\n ['', '', '', '', '', '', '', '', ''],\n nominaVsBancoHeader,\n ...mainReportRows,\n];\n\nconst bancoSinNominaHeader = [\n 'Empleado Banco',\n 'Cuenta Banco',\n 'Moneda',\n 'Monto Banco',\n 'Estado',\n 'Observación',\n 'Resolución',\n];\n\nconst bancoSinNominaRows = rows\n .filter((row) => row.category === 'banco_sin_nomina')\n .map((row) => [\n normalizeText(row.employee_name || row.employee || ''),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n normalizeText(row.currency || 'QTZ'),\n roundMoney(row.bank_amount ?? row.bankAmount ?? 0),\n normalizeText(row.status || ''),\n normalizeText(row.observation || ''),\n '',\n ]);\n\nconst bancoSinBambooHeader = [\n '#',\n 'Nombre en banco',\n 'Nombre del cuentahabiente',\n 'Cuenta',\n 'Monto en banco (Q)',\n 'Número de envío',\n 'Estado',\n 'Resolución',\n];\n\nconst bancoSinBambooRows = bankWithoutBamboo.map((row, index) => [\n index + 1,\n normalizeText(row.bank_name_file || row.employee_name || ''),\n normalizeText(row.bank_account_holder || ''),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(row.bank_amount ?? row.bankAmount ?? 0),\n firstValue(row.shipment_numbers || row.shipment_number || ''),\n 'PENDIENTE REVISIÓN',\n '',\n]);\n\nconst bancoSinBambooValues = [\n [\n 'GOMEZLEE MARKETING',\n '',\n '',\n '',\n '',\n '',\n '',\n '',\n ],\n [\n bancoSinBambooSubtitle,\n '',\n '',\n '',\n '',\n '',\n '',\n '',\n ],\n ['', '', '', '', '', '', '', ''],\n bancoSinBambooHeader,\n ...bancoSinBambooRows,\n];\n\nconst diferenciasNombreHeader = [\n 'Nombre en Archivo',\n 'Nombre del Cuentahabiente',\n 'Cuenta Destino',\n 'Moneda',\n 'Monto',\n 'Número de envío',\n 'Número de plan',\n 'Archivo',\n 'Estado',\n 'Observación',\n 'Resolución',\n];\n\nconst diferenciasNombreRows = rows\n .filter((row) => row.category === 'diferencia_nombre_banco')\n .map((row) => [\n normalizeText(row.bank_name_file || row.employee_name || ''),\n normalizeText(row.bank_account_holder || ''),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n normalizeText(row.currency || 'QTZ'),\n roundMoney(row.bank_amount ?? row.bankAmount ?? 0),\n normalizeText(row.shipment_number || ''),\n normalizeText(row.plan_number || ''),\n normalizeText(row.source_file || ''),\n normalizeText(row.status || ''),\n normalizeText(row.observation || ''),\n '',\n ]);\n\n\nfunction accountValues(value) {\n const values = Array.isArray(value)\n ? value\n : String(value ?? '')\n .split(/\\s*(?:\\/|;|,|\\by\\b)\\s*/i);\n\n return values\n .map((item) =>\n String(item ?? '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim()\n )\n .filter((item) =>\n item.length >= 7 &&\n !/^0+$/.test(item)\n );\n}\n\nfunction payrollAccountsForWrongAccount(row) {\n const candidates = [\n row.payroll_account,\n row.payrollAccount,\n ...(Array.isArray(row.source_rows)\n ? row.source_rows.flatMap((sourceRow) => [\n sourceRow.account,\n sourceRow.payroll_account,\n sourceRow.payrollAccount,\n ])\n : []),\n ];\n\n return Array.from(new Set(\n candidates.flatMap(accountValues)\n ));\n}\n\nfunction bankAccountForWrongAccount(row) {\n return firstValue(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n );\n}\n\nfunction wrongAccountTotalStatus(row) {\n const payrollAmount = roundMoney(\n row.payroll_amount ?? row.payrollAmount ?? 0\n );\n const bankAmount = roundMoney(\n row.bank_amount ?? row.bankAmount ?? 0\n );\n const difference = roundMoney(\n payrollAmount - bankAmount\n );\n\n if (Math.abs(difference) <= 0.02) {\n return 'El total de nómina coincide con el total del banco.';\n }\n\n if (difference > 0) {\n return (\n `El total de nómina supera el total del banco por ` +\n `Q${Math.abs(difference).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n })}.`\n );\n }\n\n return (\n `El total pagado por el banco supera el total de nómina por ` +\n `Q${Math.abs(difference).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n })}.`\n );\n}\n\nfunction wrongAccountFinding(row) {\n const existing = normalizeText(row.observation || '');\n\n if (existing) {\n return existing;\n }\n\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n return (\n `El empleado presenta una posible inconsistencia entre ` +\n `la cuenta registrada en nómina ` +\n `(${payrollAccounts.join(' y ') || 'sin cuenta identificada'}) ` +\n `y la cuenta utilizada por el banco ` +\n `(${bankAccount || 'sin cuenta identificada'}).`\n );\n}\n\nconst cuentaMalDigitadaHeader = [\n '#',\n 'Campo',\n 'Detalle',\n 'Resolución',\n];\n\nconst cuentaMalDigitadaRows = [];\n\nfor (\n let index = 0;\n index < cuentaMalDigitadaCases.length;\n index++\n) {\n const row = cuentaMalDigitadaCases[index];\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n const fields = [\n [\n 'Empleado',\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n ],\n [\n 'Cuentas registradas en las hojas de nómina',\n payrollAccounts.join(' y ') ||\n 'No se identificó una cuenta válida en la nómina.',\n ],\n [\n 'Cuenta utilizada por el banco',\n bankAccount ||\n 'No se identificó una cuenta válida en el banco.',\n ],\n [\n 'Estado del total',\n wrongAccountTotalStatus(row),\n ],\n [\n 'Hallazgo',\n wrongAccountFinding(row),\n ],\n [\n 'Clasificación',\n 'Posible cuenta mal digitada — revisar y unificar la cuenta en las hojas de nómina.',\n ],\n ];\n\n fields.forEach((field, fieldIndex) => {\n cuentaMalDigitadaRows.push([\n fieldIndex === 0 ? index + 1 : '',\n field[0],\n field[1],\n '',\n ]);\n });\n}\n\nconst cuentaMalDigitadaValues = [\n [\n 'GOMEZLEE MARKETING',\n '',\n '',\n '',\n ],\n [\n cuentaMalDigitadaSubtitle,\n '',\n '',\n '',\n ],\n ['', '', '', ''],\n cuentaMalDigitadaHeader,\n ...cuentaMalDigitadaRows,\n];\n\nconst resumenHeader = ['Indicador', 'Valor'];\nconst resumenRows = [\n ['Período', periodLabel],\n ['Coincidencias', Number(summary.coincidencias || 0)],\n ['Discrepancias', Number(summary.discrepancias || 0)],\n ['Banco sin nómina', Number(summary.bancoSinNomina || 0)],\n ['Banco sin Bamboo', Number(summary.bancoSinBamboo || 0)],\n ['Nómina sin cuenta', Number(summary.nominaSinCuenta || 0)],\n [\n 'Diferencias nombre banco',\n Number(summary.diferenciasNombreBanco || 0),\n ],\n [\n 'Posibles cuentas mal digitadas',\n Number(summary.posiblesCuentasMalDigitadas || 0),\n ],\n ['Pendientes cruce principal', Number(summary.pendientes || 0)],\n [\n 'Empleados BambooHR Guatemala',\n Number(summary.empleadosBambooGT || 0),\n ],\n [\n 'Empleados BambooHR en el período',\n Number(summary.empleadosBambooEnPeriodo || 0),\n ],\n ['Filas válidas de nómina', Number(summary.filasNominaValidas || 0)],\n [\n 'Suplementos adjuntados',\n Number(summary.suplementosNominaAdjuntados || 0),\n ],\n [\n 'Suplementos sin coincidencia',\n Number(summary.suplementosNominaNoAdjuntados || 0),\n ],\n ['Transacciones bancarias', Number(summary.transaccionesBanco || 0)],\n ['Total nómina', roundMoney(summary.totalNomina || 0)],\n ['Total banco', roundMoney(summary.totalBanco || 0)],\n ['Diferencia total', roundMoney(summary.diferenciaTotal || 0)],\n];\n\nfunction buildSheetValues(header, bodyRows) {\n return [header, ...bodyRows];\n}\n\nconst valueData = [\n {\n range: `'${sheetTitles.nominaVsBanco}'!A1:I`,\n values: nominaVsBancoValues,\n },\n {\n range: `'${sheetTitles.bancoSinNomina}'!A1:G`,\n values: buildSheetValues(\n bancoSinNominaHeader,\n bancoSinNominaRows\n ),\n },\n {\n range: `'${sheetTitles.bancoSinBamboo}'!A1:H`,\n values: bancoSinBambooValues,\n },\n {\n range: `'${sheetTitles.diferenciasNombreBanco}'!A1:K`,\n values: buildSheetValues(\n diferenciasNombreHeader,\n diferenciasNombreRows\n ),\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n range:\n `'${sheetTitles.cuentaMalDigitada}'!A1:D`,\n values: cuentaMalDigitadaValues,\n },\n ]\n : []),\n {\n range: `'${sheetTitles.resumen}'!A1:B`,\n values: buildSheetValues(\n resumenHeader,\n resumenRows\n ),\n },\n];\n\nfunction headerFormatRequest(\n sheetId,\n endColumnIndex,\n startRowIndex = 0\n) {\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex: startRowIndex + 1,\n startColumnIndex: 0,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat: {\n backgroundColor: {\n red: 0.29,\n green: 0.49,\n blue: 0.58,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 1,\n green: 1,\n blue: 1,\n },\n },\n horizontalAlignment: 'CENTER',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n },\n },\n fields:\n 'userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,verticalAlignment,wrapStrategy)',\n },\n };\n}\n\nfunction freezeHeaderRequest(sheetId, frozenRowCount = 1) {\n return {\n updateSheetProperties: {\n properties: {\n sheetId,\n gridProperties: {\n frozenRowCount,\n },\n },\n fields: 'gridProperties.frozenRowCount',\n },\n };\n}\n\nfunction autoResizeRequest(sheetId, endColumnIndex) {\n return {\n autoResizeDimensions: {\n dimensions: {\n sheetId,\n dimension: 'COLUMNS',\n startIndex: 0,\n endIndex: endColumnIndex,\n },\n },\n };\n}\n\nfunction moneyFormatRequest(\n sheetId,\n startColumnIndex,\n endColumnIndex,\n startRowIndex = 1,\n pattern = '#,##0.00',\n endRowIndex = null\n) {\n const range = {\n sheetId,\n startRowIndex,\n startColumnIndex,\n endColumnIndex,\n };\n\n if (Number.isInteger(endRowIndex)) {\n range.endRowIndex = endRowIndex;\n }\n\n return {\n repeatCell: {\n range,\n cell: {\n userEnteredFormat: {\n numberFormat: {\n type: 'NUMBER',\n pattern,\n },\n },\n },\n fields: 'userEnteredFormat.numberFormat',\n },\n };\n}\n\nfunction basicFilterRequest(\n sheetId,\n endColumnIndex,\n startRowIndex = 0,\n endRowIndex = null\n) {\n const range = {\n sheetId,\n startRowIndex,\n startColumnIndex: 0,\n endColumnIndex,\n };\n\n if (Number.isInteger(endRowIndex)) {\n range.endRowIndex = endRowIndex;\n }\n\n return {\n setBasicFilter: {\n filter: {\n range,\n },\n },\n };\n}\n\nfunction mergeRowRequest(\n sheetId,\n rowIndex,\n endColumnIndex\n) {\n return {\n mergeCells: {\n range: {\n sheetId,\n startRowIndex: rowIndex,\n endRowIndex: rowIndex + 1,\n startColumnIndex: 0,\n endColumnIndex,\n },\n mergeType: 'MERGE_ALL',\n },\n };\n}\n\nfunction titleRowFormatRequest(\n sheetId,\n rowIndex,\n endColumnIndex,\n options = {}\n) {\n const {\n fontSize = 12,\n bold = true,\n italic = false,\n horizontalAlignment = 'LEFT',\n } = options;\n\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex: rowIndex,\n endRowIndex: rowIndex + 1,\n startColumnIndex: 0,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat: {\n backgroundColor: {\n red: 0.29,\n green: 0.49,\n blue: 0.58,\n },\n textFormat: {\n bold,\n italic,\n fontSize,\n foregroundColor: {\n red: 1,\n green: 1,\n blue: 1,\n },\n },\n horizontalAlignment,\n verticalAlignment: 'MIDDLE',\n },\n },\n fields:\n 'userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,verticalAlignment)',\n },\n };\n}\n\nfunction columnWidthRequest(\n sheetId,\n startIndex,\n endIndex,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'COLUMNS',\n startIndex,\n endIndex,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction rowHeightRequest(\n sheetId,\n startIndex,\n endIndex,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction bodyAlignmentRequest(\n sheetId,\n startColumnIndex,\n endColumnIndex,\n horizontalAlignment,\n endRowIndex\n) {\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex: 4,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat: {\n horizontalAlignment,\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n },\n },\n fields:\n 'userEnteredFormat(horizontalAlignment,verticalAlignment,wrapStrategy)',\n },\n };\n}\n\nfunction statusFormatRequest(sheetId, endRowIndex) {\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex: 4,\n endRowIndex,\n startColumnIndex: 7,\n endColumnIndex: 8,\n },\n cell: {\n userEnteredFormat: {\n backgroundColor: {\n red: 1,\n green: 0.92,\n blue: 0.92,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.85,\n green: 0.08,\n blue: 0.08,\n },\n },\n horizontalAlignment: 'CENTER',\n verticalAlignment: 'MIDDLE',\n },\n },\n fields:\n 'userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,verticalAlignment)',\n },\n };\n}\n\nfunction bambooStatusFormatRequest(\n sheetId,\n endRowIndex\n) {\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex: 4,\n endRowIndex,\n startColumnIndex: 6,\n endColumnIndex: 7,\n },\n cell: {\n userEnteredFormat: {\n backgroundColor: {\n red: 1,\n green: 0.97,\n blue: 0.82,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.70,\n green: 0.30,\n blue: 0.00,\n },\n },\n horizontalAlignment: 'CENTER',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n },\n },\n fields:\n 'userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,verticalAlignment,wrapStrategy)',\n },\n };\n}\n\n\nfunction mergeVerticalRequest(\n sheetId,\n startRowIndex,\n endRowIndex,\n columnIndex\n) {\n return {\n mergeCells: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex: columnIndex,\n endColumnIndex: columnIndex + 1,\n },\n mergeType: 'MERGE_ALL',\n },\n };\n}\n\nfunction rangeFormatRequest(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n userEnteredFormat\n) {\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat,\n },\n fields:\n 'userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,verticalAlignment,wrapStrategy)',\n },\n };\n}\n\nfunction conditionalDifferenceRequest(\n sheetId,\n formula,\n backgroundColor,\n textColor,\n ranges,\n index\n) {\n return {\n addConditionalFormatRule: {\n index,\n rule: {\n ranges,\n booleanRule: {\n condition: {\n type: 'CUSTOM_FORMULA',\n values: [\n {\n userEnteredValue: formula,\n },\n ],\n },\n format: {\n backgroundColor,\n textFormat: {\n bold: true,\n foregroundColor: textColor,\n },\n },\n },\n },\n },\n };\n}\n\nfunction borderRequest(\n sheetId,\n startRowIndex,\n endRowIndex,\n endColumnIndex\n) {\n const border = {\n style: 'SOLID',\n color: {\n red: 0.72,\n green: 0.82,\n blue: 0.76,\n },\n };\n\n return {\n updateBorders: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex,\n },\n top: border,\n bottom: border,\n left: border,\n right: border,\n innerHorizontal: border,\n innerVertical: border,\n },\n };\n}\n\nconst mainReportEndRow = Math.max(\n 4 + mainReportRows.length,\n 4\n);\n\nconst bancoSinBambooEndRow = Math.max(\n 4 + bancoSinBambooRows.length,\n 4\n);\n\nconst mainConditionalRanges = mainReportRows.length\n ? [\n {\n sheetId: sheetIds.nominaVsBanco,\n startRowIndex: 4,\n endRowIndex: mainReportEndRow,\n startColumnIndex: 5,\n endColumnIndex: 7,\n },\n ]\n : [];\n\nconst mainDataFormatRequests = mainReportRows.length\n ? [\n moneyFormatRequest(\n sheetIds.nominaVsBanco,\n 3,\n 6,\n 4,\n 'Q#,##0.00;[Red](Q#,##0.00)',\n mainReportEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.nominaVsBanco,\n 0,\n 1,\n 'CENTER',\n mainReportEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.nominaVsBanco,\n 1,\n 2,\n 'LEFT',\n mainReportEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.nominaVsBanco,\n 2,\n 3,\n 'CENTER',\n mainReportEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.nominaVsBanco,\n 3,\n 6,\n 'RIGHT',\n mainReportEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.nominaVsBanco,\n 6,\n 8,\n 'CENTER',\n mainReportEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.nominaVsBanco,\n 8,\n 9,\n 'LEFT',\n mainReportEndRow\n ),\n statusFormatRequest(\n sheetIds.nominaVsBanco,\n mainReportEndRow\n ),\n conditionalDifferenceRequest(\n sheetIds.nominaVsBanco,\n '=$F5>0',\n {\n red: 1,\n green: 0.97,\n blue: 0.82,\n },\n {\n red: 0.25,\n green: 0.25,\n blue: 0.25,\n },\n mainConditionalRanges,\n 0\n ),\n conditionalDifferenceRequest(\n sheetIds.nominaVsBanco,\n '=$F5<0',\n {\n red: 1,\n green: 0.89,\n blue: 0.89,\n },\n {\n red: 0.9,\n green: 0.05,\n blue: 0.05,\n },\n mainConditionalRanges,\n 1\n ),\n rowHeightRequest(\n sheetIds.nominaVsBanco,\n 4,\n mainReportEndRow,\n 28\n ),\n ]\n : [];\n\n\nconst cuentaMalDigitadaEndRow = Math.max(\n 4 + cuentaMalDigitadaRows.length,\n 4\n);\n\nconst cuentaMalDigitadaCaseRequests =\n hasCuentaMalDigitada\n ? cuentaMalDigitadaCases.flatMap(\n (_, caseIndex) => {\n const startRowIndex =\n 4 + caseIndex * 6;\n const endRowIndex =\n startRowIndex + 6;\n\n return [\n mergeVerticalRequest(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex,\n 0\n ),\n mergeVerticalRequest(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex,\n 3\n ),\n rowHeightRequest(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n startRowIndex + 4,\n 34\n ),\n rowHeightRequest(\n sheetIds.cuentaMalDigitada,\n startRowIndex + 4,\n startRowIndex + 5,\n 76\n ),\n rowHeightRequest(\n sheetIds.cuentaMalDigitada,\n startRowIndex + 5,\n endRowIndex,\n 54\n ),\n rangeFormatRequest(\n sheetIds.cuentaMalDigitada,\n startRowIndex + 4,\n startRowIndex + 5,\n 2,\n 3,\n {\n backgroundColor: {\n red: 0.97,\n green: 0.97,\n blue: 0.97,\n },\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n rangeFormatRequest(\n sheetIds.cuentaMalDigitada,\n startRowIndex + 5,\n endRowIndex,\n 2,\n 3,\n {\n backgroundColor: {\n red: 1,\n green: 0.97,\n blue: 0.82,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.55,\n green: 0.30,\n blue: 0.00,\n },\n },\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n ];\n }\n )\n : [];\n\nconst cuentaMalDigitadaFormatRequests =\n hasCuentaMalDigitada\n ? [\n mergeRowRequest(\n sheetIds.cuentaMalDigitada,\n 0,\n 4\n ),\n mergeRowRequest(\n sheetIds.cuentaMalDigitada,\n 1,\n 4\n ),\n titleRowFormatRequest(\n sheetIds.cuentaMalDigitada,\n 0,\n 4,\n {\n fontSize: 12,\n bold: true,\n italic: false,\n horizontalAlignment: 'LEFT',\n }\n ),\n titleRowFormatRequest(\n sheetIds.cuentaMalDigitada,\n 1,\n 4,\n {\n fontSize: 10,\n bold: false,\n italic: true,\n horizontalAlignment: 'LEFT',\n }\n ),\n headerFormatRequest(\n sheetIds.cuentaMalDigitada,\n 4,\n 3\n ),\n freezeHeaderRequest(\n sheetIds.cuentaMalDigitada,\n 4\n ),\n borderRequest(\n sheetIds.cuentaMalDigitada,\n 3,\n cuentaMalDigitadaEndRow,\n 4\n ),\n rowHeightRequest(\n sheetIds.cuentaMalDigitada,\n 0,\n 1,\n 30\n ),\n rowHeightRequest(\n sheetIds.cuentaMalDigitada,\n 1,\n 2,\n 26\n ),\n rowHeightRequest(\n sheetIds.cuentaMalDigitada,\n 3,\n 4,\n 40\n ),\n rangeFormatRequest(\n sheetIds.cuentaMalDigitada,\n 4,\n cuentaMalDigitadaEndRow,\n 0,\n 1,\n {\n backgroundColor: {\n red: 0.91,\n green: 0.95,\n blue: 0.99,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment: 'CENTER',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n rangeFormatRequest(\n sheetIds.cuentaMalDigitada,\n 4,\n cuentaMalDigitadaEndRow,\n 1,\n 2,\n {\n backgroundColor: {\n red: 0.93,\n green: 0.97,\n blue: 0.90,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n rangeFormatRequest(\n sheetIds.cuentaMalDigitada,\n 4,\n cuentaMalDigitadaEndRow,\n 2,\n 3,\n {\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n rangeFormatRequest(\n sheetIds.cuentaMalDigitada,\n 4,\n cuentaMalDigitadaEndRow,\n 3,\n 4,\n {\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n columnWidthRequest(\n sheetIds.cuentaMalDigitada,\n 0,\n 1,\n 48\n ),\n columnWidthRequest(\n sheetIds.cuentaMalDigitada,\n 1,\n 2,\n 285\n ),\n columnWidthRequest(\n sheetIds.cuentaMalDigitada,\n 2,\n 3,\n 520\n ),\n columnWidthRequest(\n sheetIds.cuentaMalDigitada,\n 3,\n 4,\n 260\n ),\n ...cuentaMalDigitadaCaseRequests,\n ]\n : [];\n\n\nfunction wrapRangeRequest(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex\n) {\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat: {\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n },\n },\n fields:\n 'userEnteredFormat(verticalAlignment,wrapStrategy)',\n },\n };\n}\n\nfunction autoResizeRowsRequest(\n sheetId,\n startIndex,\n endIndex\n) {\n return {\n autoResizeDimensions: {\n dimensions: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n },\n };\n}\n\nconst formatRequests = [\n mergeRowRequest(sheetIds.nominaVsBanco, 0, 9),\n mergeRowRequest(sheetIds.nominaVsBanco, 1, 9),\n titleRowFormatRequest(\n sheetIds.nominaVsBanco,\n 0,\n 9,\n {\n fontSize: 12,\n bold: true,\n italic: false,\n horizontalAlignment: 'LEFT',\n }\n ),\n titleRowFormatRequest(\n sheetIds.nominaVsBanco,\n 1,\n 9,\n {\n fontSize: 10,\n bold: false,\n italic: true,\n horizontalAlignment: 'LEFT',\n }\n ),\n headerFormatRequest(\n sheetIds.nominaVsBanco,\n 9,\n 3\n ),\n freezeHeaderRequest(\n sheetIds.nominaVsBanco,\n 4\n ),\n basicFilterRequest(\n sheetIds.nominaVsBanco,\n 9,\n 3,\n mainReportEndRow\n ),\n borderRequest(\n sheetIds.nominaVsBanco,\n 3,\n mainReportEndRow,\n 9\n ),\n rowHeightRequest(\n sheetIds.nominaVsBanco,\n 0,\n 1,\n 30\n ),\n rowHeightRequest(\n sheetIds.nominaVsBanco,\n 1,\n 2,\n 26\n ),\n rowHeightRequest(\n sheetIds.nominaVsBanco,\n 3,\n 4,\n 42\n ),\n ...mainDataFormatRequests,\n\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 0,\n 1,\n 48\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 1,\n 2,\n 260\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 2,\n 3,\n 130\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 3,\n 6,\n 130\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 6,\n 7,\n 155\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 7,\n 8,\n 110\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 8,\n 9,\n 260\n ),\n\n headerFormatRequest(sheetIds.bancoSinNomina, 7),\n freezeHeaderRequest(sheetIds.bancoSinNomina),\n autoResizeRequest(sheetIds.bancoSinNomina, 7),\n moneyFormatRequest(sheetIds.bancoSinNomina, 3, 4),\n basicFilterRequest(sheetIds.bancoSinNomina, 7),\n columnWidthRequest(\n sheetIds.bancoSinNomina,\n 6,\n 7,\n 260\n ),\n\n mergeRowRequest(sheetIds.bancoSinBamboo, 0, 8),\n mergeRowRequest(sheetIds.bancoSinBamboo, 1, 8),\n titleRowFormatRequest(\n sheetIds.bancoSinBamboo,\n 0,\n 8,\n {\n fontSize: 12,\n bold: true,\n italic: false,\n horizontalAlignment: 'LEFT',\n }\n ),\n titleRowFormatRequest(\n sheetIds.bancoSinBamboo,\n 1,\n 8,\n {\n fontSize: 10,\n bold: false,\n italic: true,\n horizontalAlignment: 'LEFT',\n }\n ),\n headerFormatRequest(\n sheetIds.bancoSinBamboo,\n 8,\n 3\n ),\n freezeHeaderRequest(\n sheetIds.bancoSinBamboo,\n 4\n ),\n basicFilterRequest(\n sheetIds.bancoSinBamboo,\n 8,\n 3,\n bancoSinBambooEndRow\n ),\n borderRequest(\n sheetIds.bancoSinBamboo,\n 3,\n bancoSinBambooEndRow,\n 8\n ),\n rowHeightRequest(\n sheetIds.bancoSinBamboo,\n 0,\n 1,\n 30\n ),\n rowHeightRequest(\n sheetIds.bancoSinBamboo,\n 1,\n 2,\n 26\n ),\n rowHeightRequest(\n sheetIds.bancoSinBamboo,\n 3,\n 4,\n 42\n ),\n moneyFormatRequest(\n sheetIds.bancoSinBamboo,\n 4,\n 5,\n 4,\n 'Q#,##0.00',\n bancoSinBambooEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.bancoSinBamboo,\n 0,\n 1,\n 'CENTER',\n bancoSinBambooEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.bancoSinBamboo,\n 1,\n 3,\n 'LEFT',\n bancoSinBambooEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.bancoSinBamboo,\n 3,\n 4,\n 'CENTER',\n bancoSinBambooEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.bancoSinBamboo,\n 4,\n 5,\n 'RIGHT',\n bancoSinBambooEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.bancoSinBamboo,\n 5,\n 7,\n 'CENTER',\n bancoSinBambooEndRow\n ),\n bodyAlignmentRequest(\n sheetIds.bancoSinBamboo,\n 7,\n 8,\n 'LEFT',\n bancoSinBambooEndRow\n ),\n bambooStatusFormatRequest(\n sheetIds.bancoSinBamboo,\n bancoSinBambooEndRow\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 0,\n 1,\n 48\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 1,\n 2,\n 220\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 2,\n 3,\n 270\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 3,\n 4,\n 135\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 4,\n 5,\n 130\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 5,\n 6,\n 110\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 6,\n 7,\n 140\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 7,\n 8,\n 260\n ),\n\n headerFormatRequest(\n sheetIds.diferenciasNombreBanco,\n 11\n ),\n freezeHeaderRequest(\n sheetIds.diferenciasNombreBanco\n ),\n autoResizeRequest(\n sheetIds.diferenciasNombreBanco,\n 11\n ),\n moneyFormatRequest(\n sheetIds.diferenciasNombreBanco,\n 4,\n 5\n ),\n basicFilterRequest(\n sheetIds.diferenciasNombreBanco,\n 11\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 10,\n 11,\n 260\n ),\n\n ...cuentaMalDigitadaFormatRequests,\n\n headerFormatRequest(sheetIds.resumen, 2),\n freezeHeaderRequest(sheetIds.resumen),\n autoResizeRequest(sheetIds.resumen, 2),\n moneyFormatRequest(sheetIds.resumen, 1, 2),\n];\n\n\nconst bancoSinNominaEndRow =\n 1 + bancoSinNominaRows.length;\n\nconst diferenciasNombreEndRow =\n 1 + diferenciasNombreRows.length;\n\nconst resumenReadabilityEndRow =\n 1 + resumenRows.length;\n\n/*\n * Ajuste final de legibilidad.\n *\n * Se ejecuta al final para que los anchos definitivos ya estén aplicados\n * cuando Google Sheets calcule automáticamente la altura de cada fila.\n * Así, cualquier texto largo queda envuelto y visible sin que el usuario\n * tenga que expandir columnas o filas manualmente.\n */\nformatRequests.push(\n // 01 Nómina vs Banco\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 1,\n 2,\n 300\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 2,\n 3,\n 150\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 3,\n 6,\n 145\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 6,\n 7,\n 220\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 7,\n 8,\n 140\n ),\n columnWidthRequest(\n sheetIds.nominaVsBanco,\n 8,\n 9,\n 320\n ),\n ...(mainReportRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.nominaVsBanco,\n 4,\n mainReportEndRow,\n 0,\n 9\n ),\n autoResizeRowsRequest(\n sheetIds.nominaVsBanco,\n 4,\n mainReportEndRow\n ),\n ]\n : []),\n\n // 02 Banco sin Nómina\n columnWidthRequest(\n sheetIds.bancoSinNomina,\n 0,\n 1,\n 320\n ),\n columnWidthRequest(\n sheetIds.bancoSinNomina,\n 1,\n 2,\n 165\n ),\n columnWidthRequest(\n sheetIds.bancoSinNomina,\n 2,\n 3,\n 95\n ),\n columnWidthRequest(\n sheetIds.bancoSinNomina,\n 3,\n 4,\n 140\n ),\n columnWidthRequest(\n sheetIds.bancoSinNomina,\n 4,\n 5,\n 170\n ),\n columnWidthRequest(\n sheetIds.bancoSinNomina,\n 5,\n 6,\n 560\n ),\n columnWidthRequest(\n sheetIds.bancoSinNomina,\n 6,\n 7,\n 320\n ),\n autoResizeRowsRequest(\n sheetIds.bancoSinNomina,\n 0,\n 1\n ),\n ...(bancoSinNominaRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.bancoSinNomina,\n 1,\n bancoSinNominaEndRow,\n 0,\n 7\n ),\n autoResizeRowsRequest(\n sheetIds.bancoSinNomina,\n 1,\n bancoSinNominaEndRow\n ),\n ]\n : []),\n\n // 03 Banco sin Bamboo\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 1,\n 2,\n 300\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 2,\n 3,\n 330\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 3,\n 4,\n 150\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 4,\n 5,\n 145\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 5,\n 6,\n 150\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 6,\n 7,\n 170\n ),\n columnWidthRequest(\n sheetIds.bancoSinBamboo,\n 7,\n 8,\n 320\n ),\n ...(bancoSinBambooRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.bancoSinBamboo,\n 4,\n bancoSinBambooEndRow,\n 0,\n 8\n ),\n autoResizeRowsRequest(\n sheetIds.bancoSinBamboo,\n 4,\n bancoSinBambooEndRow\n ),\n ]\n : []),\n\n // 04 Diferencias nombre banco\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 0,\n 1,\n 300\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 1,\n 2,\n 330\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 2,\n 3,\n 155\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 3,\n 4,\n 95\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 4,\n 5,\n 140\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 5,\n 7,\n 150\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 7,\n 8,\n 260\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 8,\n 9,\n 170\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 9,\n 10,\n 650\n ),\n columnWidthRequest(\n sheetIds.diferenciasNombreBanco,\n 10,\n 11,\n 320\n ),\n autoResizeRowsRequest(\n sheetIds.diferenciasNombreBanco,\n 0,\n 1\n ),\n ...(diferenciasNombreRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.diferenciasNombreBanco,\n 1,\n diferenciasNombreEndRow,\n 0,\n 11\n ),\n autoResizeRowsRequest(\n sheetIds.diferenciasNombreBanco,\n 1,\n diferenciasNombreEndRow\n ),\n ]\n : []),\n\n // 05 Cuenta Mal Digitada\n ...(hasCuentaMalDigitada\n ? [\n columnWidthRequest(\n sheetIds.cuentaMalDigitada,\n 1,\n 2,\n 300\n ),\n columnWidthRequest(\n sheetIds.cuentaMalDigitada,\n 2,\n 3,\n 600\n ),\n columnWidthRequest(\n sheetIds.cuentaMalDigitada,\n 3,\n 4,\n 320\n ),\n ]\n : []),\n\n // Resumen\n columnWidthRequest(\n sheetIds.resumen,\n 0,\n 1,\n 380\n ),\n columnWidthRequest(\n sheetIds.resumen,\n 1,\n 2,\n 320\n ),\n autoResizeRowsRequest(\n sheetIds.resumen,\n 0,\n 1\n ),\n ...(resumenRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.resumen,\n 1,\n resumenReadabilityEndRow,\n 0,\n 2\n ),\n autoResizeRowsRequest(\n sheetIds.resumen,\n 1,\n resumenReadabilityEndRow\n ),\n ]\n : [])\n);\n\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'preparar_google_sheet',\n metadata,\n summary,\n spreadsheetTitle,\n sheetIds,\n sheetTitles,\n createSpreadsheetBody: {\n properties: {\n title: spreadsheetTitle,\n },\n sheets: [\n {\n properties: {\n sheetId: sheetIds.nominaVsBanco,\n title: sheetTitles.nominaVsBanco,\n },\n },\n {\n properties: {\n sheetId: sheetIds.bancoSinNomina,\n title: sheetTitles.bancoSinNomina,\n },\n },\n {\n properties: {\n sheetId: sheetIds.bancoSinBamboo,\n title: sheetTitles.bancoSinBamboo,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.diferenciasNombreBanco,\n title:\n sheetTitles.diferenciasNombreBanco,\n },\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n properties: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n title:\n sheetTitles.cuentaMalDigitada,\n },\n },\n ]\n : []),\n {\n properties: {\n sheetId: sheetIds.resumen,\n title: sheetTitles.resumen,\n },\n },\n ],\n },\n valueBatchBody: {\n valueInputOption: 'USER_ENTERED',\n data: valueData,\n },\n formatBatchBody: {\n requests: formatRequests,\n },\n originalResponse: data,\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 22048, - 26512 - ], - "id": "5c8239ca-2f93-41a3-be89-cb6fce94d2b2", - "name": "Preparar Google Sheet" - }, - { - "parameters": { - "method": "POST", - "url": "https://sheets.googleapis.com/v4/spreadsheets", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "googleOAuth2Api", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{\n(() => {\n const prepared =\n $('Preparar Google Sheet').first().json || {};\n\n const createBody =\n prepared.createSpreadsheetBody || {};\n\n if (\n !Array.isArray(createBody.sheets) ||\n createBody.sheets.length === 0\n ) {\n throw new Error(\n 'Preparar Google Sheet no devolvió las hojas que deben crearse.'\n );\n }\n\n return {\n properties: {\n ...(createBody.properties || {}),\n timeZone: 'America/Guatemala',\n },\n\n sheets: createBody.sheets.map((sheet) => ({\n properties: {\n ...(sheet.properties || {}),\n\n gridProperties: {\n ...((sheet.properties || {}).gridProperties || {}),\n frozenRowCount: 1,\n },\n },\n })),\n };\n})()\n}}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 22256, - 26512 - ], - "id": "937c7727-c0e4-49c7-ab93-199919b81763", - "name": "Crear Google Sheet", - "credentials": { - "httpBasicAuth": { - "id": "nIxZ7elcHvuzsRKW", - "name": "Neo4j" - }, - "googleOAuth2Api": { - "id": "eHseMeH39kRcXgOF", - "name": "Google account 2" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "={{ 'https://sheets.googleapis.com/v4/spreadsheets/' + $('Crear Google Sheet').first().json.spreadsheetId + '/values:batchUpdate' }}", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "googleOAuth2Api", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $('Preparar Google Sheet').first().json.valueBatchBody }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 22464, - 26512 - ], - "id": "c71323e4-d046-44c9-b073-e8f389ffc86f", - "name": "Escribir Google Sheet", - "credentials": { - "googleOAuth2Api": { - "id": "dQ1MJSJSWcoWYcb8", - "name": "Google account - Isaac Producción" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "={{ 'https://sheets.googleapis.com/v4/spreadsheets/' + $('Crear Google Sheet').first().json.spreadsheetId + ':batchUpdate' }}", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "googleOAuth2Api", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $('Preparar Google Sheet').first().json.formatBatchBody }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 22672, - 26512 - ], - "id": "21d98cca-0e31-4471-a0ab-b2ad0f0bf7a3", - "name": "Formatear Google Sheet", - "credentials": { - "googleOAuth2Api": { - "id": "dQ1MJSJSWcoWYcb8", - "name": "Google account - Isaac Producción" - } - } - }, - { - "parameters": { - "jsCode": "const prepared = $('Preparar Google Sheet').first().json || {};\nconst createdSheet = $('Crear Google Sheet').first().json || {};\n\nconst original =\n prepared.originalResponse ||\n prepared.original_response ||\n prepared.response ||\n {};\n\nconst spreadsheetId = createdSheet.spreadsheetId || '';\nconst reportUrl =\n createdSheet.spreadsheetUrl ||\n (spreadsheetId ? `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit` : null);\n\nreturn [\n {\n json: {\n ok: original.ok ?? true,\n message: reportUrl\n ? 'Cruce procesado correctamente. Google Sheet generado.'\n : 'Cruce procesado correctamente, pero no se recibió URL del Google Sheet.',\n stage: reportUrl ? 'cruce_completado_con_reporte' : 'cruce_completado_sin_reporte',\n errors: original.errors || [],\n metadata: original.metadata || {},\n summary: original.summary || {},\n rows: original.rows || [],\n bankWithoutBamboo:\n original.bankWithoutBamboo || [],\n bambooSummary:\n original.bambooSummary || {},\n reportUrl,\n googleSheet: {\n spreadsheetId,\n spreadsheetUrl: reportUrl,\n },\n debug: {\n rows_returned: Array.isArray(original.rows) ? original.rows.length : 0,\n coincidencias: original.summary?.coincidencias ?? 0,\n discrepancias: original.summary?.discrepancias ?? 0,\n bancoSinBamboo:\n original.summary?.bancoSinBamboo ?? 0,\n bancoSinBambooRows:\n Array.isArray(original.bankWithoutBamboo)\n ? original.bankWithoutBamboo.length\n : 0,\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 24800, - 26528 - ], - "id": "aa72c7f0-6d16-483e-b991-292c01415eef", - "name": "Preparar respuesta final" - }, - { - "parameters": { - "jsCode": "const createdSheet = $('Crear Google Sheet').first().json || {};\nconst spreadsheetId = createdSheet.spreadsheetId;\n\nif (!spreadsheetId) {\n throw new Error('No se recibió spreadsheetId desde Crear Google Sheet.');\n}\n\nconst allowedEmails = [\n 'iaracena@gomezleemarketing.com',\n 'ymadera@gomezleemarketing.com',\n 'mgomez@gomezleemarketing.com',\n 'jgomez@gomezleemarketing.com',\n];\n\nreturn allowedEmails.map((email) => ({\n json: {\n spreadsheetId,\n email,\n permissionBody: {\n type: 'user',\n role: 'writer',\n emailAddress: email,\n },\n },\n}));" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 22880, - 26512 - ], - "id": "e2b64399-0b45-40b9-b2be-b7b88bdfa002", - "name": "Preparar permisos Google Sheet" - }, - { - "parameters": { - "method": "POST", - "url": "={{ 'https://www.googleapis.com/drive/v3/files/' + $json.spreadsheetId + '/permissions?sendNotificationEmail=false' }}", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "googleOAuth2Api", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $json.permissionBody }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 23088, - 26512 - ], - "id": "d9026869-a053-4a40-8d5f-fe92e6c11c7d", - "name": "Compartir Google Sheet", - "credentials": { - "googleOAuth2Api": { - "id": "dQ1MJSJSWcoWYcb8", - "name": "Google account - Isaac Producción" - } - } - }, - { - "parameters": { - "jsCode": "const cruce = $('Cruzar Nómina vs Banco').first().json || {};\nconst createdSheet = $('Crear Google Sheet').first().json || {};\n\nconst metadata = cruce.metadata || {};\nconst summary = cruce.summary || {};\nconst debug = cruce.debug || {};\n\nconst spreadsheetId = createdSheet.spreadsheetId || cruce.spreadsheetId || '';\nconst reportUrl =\n createdSheet.spreadsheetUrl ||\n createdSheet.spreadsheet_url ||\n (spreadsheetId ? `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit` : null);\n\nfunction toNumber(value) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction buildPeriodKey(periodMetadata) {\n const country = periodMetadata.country || 'GT';\n const year = periodMetadata.year || '';\n const month = String(periodMetadata.month || '').padStart(2, '0');\n const periodType = periodMetadata.period_type || 'periodo';\n return `${country}-${year}-${month}-${periodType}`;\n}\n\nconst discrepancias = toNumber(summary.discrepancias);\nconst bancoSinNomina = toNumber(summary.bancoSinNomina);\nconst nominaSinCuenta = toNumber(summary.nominaSinCuenta);\nconst diferenciasNombreBanco =\n toNumber(summary.diferenciasNombreBanco);\nconst bancoSinBamboo =\n toNumber(summary.bancoSinBamboo);\nconst pendientes = toNumber(summary.pendientes) || (\n discrepancias +\n bancoSinNomina +\n nominaSinCuenta +\n diferenciasNombreBanco\n);\n\n// Banco sin Bamboo es una revisión independiente del cruce principal.\nconst requiereRevision =\n pendientes > 0 || bancoSinBamboo > 0;\n\n// La app final solo manejará Pendiente revisión y Resuelto.\nconst estado = requiereRevision\n ? 'pendiente_revision'\n : 'resuelto';\n\nconst payload = {\n source_app: metadata.source_app || 'cruce-cuentas-glm-guatemala',\n country: metadata.country || 'GT',\n country_name: metadata.country_name || 'Guatemala',\n\n year: toNumber(metadata.year),\n month: toNumber(metadata.month),\n period_type: metadata.period_type || '',\n period_label: metadata.period_label || '',\n period_start: metadata.period_start || null,\n period_end: metadata.period_end || null,\n period_key: buildPeriodKey(metadata),\n\n payroll_file_name: metadata.payroll_file_name || '',\n bank_file_names: metadata.bank_file_names || [],\n\n coincidencias: toNumber(summary.coincidencias),\n discrepancias,\n\n banco_sin_bamboo: bancoSinBamboo,\n detalle_banco_sin_bamboo:\n Array.isArray(cruce.bankWithoutBamboo)\n ? cruce.bankWithoutBamboo\n : [],\n banco_sin_nomina: bancoSinNomina,\n nomina_sin_cuenta: nominaSinCuenta,\n nomina_sin_bamboo: 0,\n bamboo_sin_nomina: 0,\n\n filas_nomina_validas: toNumber(summary.filasNominaValidas),\n cuentas_nomina_agrupadas: toNumber(summary.cuentasNominaAgrupadas),\n transacciones_banco: toNumber(summary.transaccionesBanco),\n cuentas_banco_agrupadas: toNumber(summary.cuentasBancoAgrupadas),\n\n total_nomina: toNumber(summary.totalNomina),\n total_banco: toNumber(summary.totalBanco),\n diferencia_total: toNumber(summary.diferenciaTotal),\n\n report_url: reportUrl,\n spreadsheet_id: spreadsheetId,\n estado,\n\n ejecutado_por_nombre: metadata.requested_by_name || 'Usuario GLM',\n ejecutado_por_email: metadata.requested_by_email || '',\n\n metadata: {\n ...metadata,\n diferencias_nombre_banco:\n diferenciasNombreBanco,\n banco_sin_bamboo:\n bancoSinBamboo,\n pendientes_cruce_principal:\n pendientes,\n requiere_revision:\n requiereRevision,\n },\n summary,\n debug: {\n sheet_summaries: debug.sheet_summaries || [],\n attached_supplements: debug.attached_supplements || [],\n unattached_supplements: debug.unattached_supplements || [],\n bank_name_differences_preview:\n debug.bank_name_differences_preview || [],\n bamboo_matches:\n debug.bamboo_matches || [],\n bamboo_excluded_payments:\n debug.bamboo_excluded_payments || [],\n banco_sin_bamboo:\n cruce.bankWithoutBamboo || [],\n },\n};\n\nreturn [\n {\n json: {\n ...cruce,\n supabaseTable: 'cruces_cuentas_gt_reportes',\n supabasePayload: payload,\n reportUrl,\n spreadsheetId,\n },\n },\n];\n" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 24240, - 26528 - ], - "id": "471f36ca-d750-4e92-8807-4794ce0cc4a0", - "name": "Preparar histórico Supabase" - }, - { - "parameters": { - "method": "POST", - "url": "https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes", - "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" - }, - { - "name": "Prefer", - "value": "return=representation" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $json.supabasePayload }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 24448, - 26528 - ], - "id": "69a0dfa0-702a-4ed6-93cc-01a0b97d9676", - "name": "Insertar histórico Supabase", - "onError": "continueRegularOutput" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": false, - "sheetName": "Temporales WMC" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 15664, - 27632 - ], - "id": "f545108c-5069-4208-be51-531a235a81e2", - "name": "Extract - Temporales WMC", - "retryOnFail": false, - "onError": "continueRegularOutput" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 19232, - 27296 - ], - "id": "e4f39bb4-ad63-4611-8ddc-09a44817eaa1", - "name": "Merge Hojas 10" - }, - { - "parameters": { - "method": "POST", - "url": "https://glm.bamboohr.com/api/v1/reports/custom?format=JSON&onlyCurrent=false", - "authentication": "genericCredentialType", - "genericAuthType": "httpBasicAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Accept", - "value": "application/json" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": { - "title": "Información de BambooHR - Cruce de Cuentas GT", - "fields": [ - "firstName", - "middleName", - "lastName", - "displayName", - "department", - "division", - "location", - "customPosicion-Cliente", - "hireDate", - "originalHireDate", - "status", - "employeeNumber" - ] - }, - "options": { - "response": { - "response": { - "responseFormat": "json" - } - }, - "timeout": 300000 - } - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 15216, - 25568 - ], - "id": "4487c967-fd95-4865-977b-b232f1a7b69d", - "name": "HTTP - Empleados BambooHR GT", - "retryOnFail": true, - "maxTries": 3, - "waitBetweenTries": 3000, - "credentials": { - "httpBasicAuth": { - "id": "7VrpNZ2jBLmiJ35q", - "name": "BambooHR GLM Full Access" - } - } - }, - { - "parameters": { - "jsCode": "const inputItems = $input.all();\nconst base = $('Preparar entrada app').first().json || {};\nconst reconciliationData = $('Merge').first().json || {};\nconst metadata = base.metadata || {};\n\nconst IGNORED_NAME_TOKENS = new Set([\n 'de', 'del', 'la', 'las', 'los',\n 'y', 'e', 'el', 'da', 'do',\n 'dos', 'das', 'van', 'von',\n]);\n\nfunction clean(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\u00A0/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalize(value) {\n return clean(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/['’`-]/g, ' ')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction unique(values) {\n const result = [];\n const seen = new Set();\n\n for (const value of values) {\n const cleaned = clean(value);\n\n if (!cleaned || seen.has(cleaned)) {\n continue;\n }\n\n seen.add(cleaned);\n result.push(cleaned);\n }\n\n return result;\n}\n\nfunction nameTokens(value) {\n return normalize(value)\n .split(' ')\n .filter(\n (token) =>\n token.length > 1 &&\n !IGNORED_NAME_TOKENS.has(token)\n );\n}\n\nfunction uniqueNameTokens(value) {\n return Array.from(\n new Set(nameTokens(value))\n );\n}\n\nfunction parseDate(value) {\n const raw = clean(value);\n if (!raw || raw === '0000-00-00') return null;\n\n const direct = raw.match(\n /^(\\d{4})-(\\d{2})-(\\d{2})/\n );\n\n if (direct) {\n return `${direct[1]}-${direct[2]}-${direct[3]}`;\n }\n\n const date = new Date(raw);\n if (Number.isNaN(date.getTime())) return null;\n\n return date.toISOString().slice(0, 10);\n}\n\nfunction parseBoolean(value) {\n if (typeof value === 'boolean') return value;\n\n return [\n 'true', 'yes', 'si', 'sí', '1', 'y',\n ].includes(normalize(value));\n}\n\nfunction isTargetCountry(employee) {\n const country = normalize(employee.country);\n const location = normalize(\n employee.location ||\n employee.jobInformationLocation ||\n employee.jobLocation\n );\n\n return (\n country === 'gt' ||\n country === 'gtm' ||\n country.includes('guatemala') ||\n location === 'gt' ||\n location === 'gtm' ||\n location.includes('guatemala')\n );\n}\n\nfunction overlapsPeriod(\n hireDate,\n terminationDate,\n periodStart,\n periodEnd\n) {\n if (!periodStart || !periodEnd) return false;\n\n const hiredBeforeEnd =\n !hireDate || hireDate <= periodEnd;\n\n const notTerminatedBeforeStart =\n !terminationDate ||\n terminationDate >= periodStart;\n\n return hiredBeforeEnd && notTerminatedBeforeStart;\n}\n\nfunction collectPageObjects(value, pages) {\n if (!value) return;\n\n if (Array.isArray(value)) {\n for (const entry of value) {\n collectPageObjects(entry, pages);\n }\n return;\n }\n\n if (typeof value !== 'object') return;\n\n if (value.body && typeof value.body === 'object') {\n collectPageObjects(value.body, pages);\n return;\n }\n\n if (\n Array.isArray(value.data) ||\n Array.isArray(value.employees)\n ) {\n pages.push(value);\n return;\n }\n\n if (value.json && typeof value.json === 'object') {\n collectPageObjects(value.json, pages);\n }\n}\n\nfunction profileContains(\n leftTokens,\n leftTokenSet,\n rightTokens,\n rightTokenSet\n) {\n if (\n leftTokens.length < 3 ||\n rightTokens.length < 3\n ) {\n return false;\n }\n\n const leftInsideRight =\n leftTokens.every((token) =>\n rightTokenSet.has(token)\n );\n\n if (leftInsideRight) return true;\n\n return rightTokens.every((token) =>\n leftTokenSet.has(token)\n );\n}\n\nfunction employeeKey(employee) {\n return (\n employee.bamboo_id ||\n employee.employee_number ||\n normalize(employee.full_name)\n );\n}\n\nconst pageObjects = [];\n\nfor (const item of inputItems) {\n collectPageObjects(item.json, pageObjects);\n}\n\nconst employeeMap = new Map();\nlet expectedTotal = 0;\nlet restrictedFields = 0;\n\nfor (const page of pageObjects) {\n const pageEmployees =\n Array.isArray(page.data)\n ? page.data\n : Array.isArray(page.employees)\n ? page.employees\n : [];\n\n const pageTotal = Number(\n page.meta?.total ||\n page.total ||\n 0\n );\n\n if (Number.isFinite(pageTotal)) {\n expectedTotal = Math.max(\n expectedTotal,\n pageTotal\n );\n }\n\n for (const employee of pageEmployees) {\n const key =\n clean(employee.employeeId || employee.id) ||\n clean(employee.employeeNumber) ||\n clean(employee.bestEmail).toLowerCase() ||\n [\n clean(employee.firstName),\n clean(employee.middleName),\n clean(employee.lastName),\n ].filter(Boolean).join('|').toLowerCase();\n\n if (!key) continue;\n\n employeeMap.set(key, employee);\n\n restrictedFields += Array.isArray(\n employee._restrictedFields\n )\n ? employee._restrictedFields.length\n : 0;\n }\n}\n\nconst rawEmployees = Array.from(\n employeeMap.values()\n);\n\nconst periodStart = clean(metadata.period_start);\nconst periodEnd = clean(metadata.period_end);\n\nconst targetEmployees = [];\nconst outsideRecords = [];\nlet normalizedEmployeesCount = 0;\n\nfor (const employee of rawEmployees) {\n const firstName = clean(employee.firstName);\n const middleName = clean(employee.middleName);\n const lastName = clean(employee.lastName);\n const preferredName = clean(\n employee.preferredName\n );\n\n const constructedFullName = [\n firstName,\n middleName,\n lastName,\n ].filter(Boolean).join(' ');\n\n const aliases = unique([\n employee.displayName,\n employee.fullName1,\n employee.fullName2,\n employee.fullName3,\n employee.fullName4,\n employee.fullName5,\n constructedFullName,\n [preferredName, lastName]\n .filter(Boolean)\n .join(' '),\n [firstName, lastName]\n .filter(Boolean)\n .join(' '),\n ]);\n\n const hireDate = parseDate(\n employee.hireDate ||\n employee.originalHireDate\n );\n\n const terminationDate = parseDate(\n employee.terminationDate\n );\n\n const status = clean(\n employee.status ||\n employee.employmentStatus ||\n employee.employmentHistoryStatus\n );\n\n const employeeNumber = clean(\n employee.employeeNumber ||\n employee.employee_number\n );\n\n const normalizedEmployee = {\n bamboo_id: clean(\n employee.employeeId ||\n employee.id\n ),\n employee_number: employeeNumber,\n first_name: firstName,\n middle_name: middleName,\n last_name: lastName,\n preferred_name: preferredName,\n full_name:\n clean(employee.displayName) ||\n clean(employee.fullName1) ||\n constructedFullName,\n aliases,\n normalized_aliases:\n aliases.map(normalize).filter(Boolean),\n status,\n hire_date: hireDate,\n termination_date: terminationDate,\n location: clean(\n employee.location ||\n employee.jobInformationLocation ||\n employee.jobLocation\n ),\n country: clean(employee.country),\n include_in_payroll:\n parseBoolean(employee.includeInPayroll),\n work_email:\n clean(employee.workEmail).toLowerCase(),\n home_email:\n clean(employee.homeEmail).toLowerCase(),\n best_email: clean(\n employee.bestEmail ||\n employee.workEmail ||\n employee.homeEmail\n ).toLowerCase(),\n exists_in_bamboo: true,\n overlaps_period: overlapsPeriod(\n hireDate,\n terminationDate,\n periodStart,\n periodEnd\n ),\n };\n\n normalizedEmployeesCount += 1;\n\n if (isTargetCountry(normalizedEmployee)) {\n targetEmployees.push({\n ...normalizedEmployee,\n validation_eligible: true,\n validation_scope:\n 'guatemala_country_or_location',\n });\n continue;\n }\n\n const aliasProfiles = aliases\n .map((rawAlias) => {\n const tokens =\n uniqueNameTokens(rawAlias);\n\n return {\n raw: rawAlias,\n tokens,\n token_set: new Set(tokens),\n };\n })\n .filter(\n (profile) =>\n profile.tokens.length >= 3\n );\n\n const searchTokens = new Set();\n\n for (const profile of aliasProfiles) {\n for (const token of profile.tokens) {\n searchTokens.add(token);\n }\n }\n\n outsideRecords.push({\n employee: normalizedEmployee,\n alias_profiles: aliasProfiles,\n search_tokens: searchTokens,\n });\n}\n\nconst relevantNameMap = new Map();\n\nfunction addRelevantName(value) {\n const cleaned = clean(value);\n const normalized = normalize(cleaned);\n\n if (!normalized) return;\n\n const tokens = uniqueNameTokens(cleaned);\n const current =\n relevantNameMap.get(normalized);\n\n if (\n !current ||\n tokens.length > current.tokens.length\n ) {\n relevantNameMap.set(\n normalized,\n {\n raw: cleaned,\n tokens,\n token_set: new Set(tokens),\n }\n );\n }\n}\n\nfor (const row of reconciliationData.bank?.rows || []) {\n addRelevantName(row.bank_name_file);\n addRelevantName(row.bank_account_holder);\n addRelevantName(row.participant_name);\n}\n\nfor (\n const row of\n reconciliationData.bank?.grouped_by_account || []\n) {\n addRelevantName(row.bank_name_file);\n addRelevantName(row.bank_account_holder);\n\n for (const name of row.bank_name_files || []) {\n addRelevantName(name);\n }\n\n for (\n const name of\n row.bank_account_holders || []\n ) {\n addRelevantName(name);\n }\n}\n\nfor (const row of [\n ...(reconciliationData.payroll?.rows || []),\n ...(reconciliationData.payroll?.grouped_by_account || []),\n ...(reconciliationData.payroll?.no_account_rows || []),\n]) {\n addRelevantName(\n row.employee_name ||\n row.employee ||\n ''\n );\n}\n\n/*\n * Índice invertido para rescatar perfiles con país/localidad incorrectos.\n *\n * La versión anterior comparaba cada nombre relevante contra todos los\n * empleados fuera de Guatemala y todas sus variantes de nombre. Con casi\n * 10,000 perfiles, eso provocaba millones de normalizaciones y bloqueaba el\n * task runner. Aquí cada palabra apunta directamente a los pocos empleados\n * que la contienen; luego solo se revisan candidatos con al menos tres\n * palabras compartidas.\n */\nconst outsideTokenIndex = new Map();\n\nfor (\n let index = 0;\n index < outsideRecords.length;\n index++\n) {\n for (\n const token of\n outsideRecords[index].search_tokens\n ) {\n let bucket =\n outsideTokenIndex.get(token);\n\n if (!bucket) {\n bucket = [];\n outsideTokenIndex.set(\n token,\n bucket\n );\n }\n\n bucket.push(index);\n }\n}\n\nconst targetExactAliasSet = new Set();\n\nfor (const employee of targetEmployees) {\n for (\n const normalizedAlias of\n employee.normalized_aliases || []\n ) {\n if (normalizedAlias) {\n targetExactAliasSet.add(\n normalizedAlias\n );\n }\n }\n}\n\nlet contextualOutsideSkippedByTargetExact = 0;\nconst contextualOutsideMap = new Map();\n\nfor (\n const relevantProfile of\n relevantNameMap.values()\n) {\n if (relevantProfile.tokens.length < 3) {\n continue;\n }\n\n /*\n * Prioridad absoluta al país objetivo:\n *\n * Cuando el nombre completo recibido existe exactamente en Guatemala,\n * no se incorpora un perfil externo cuyo nombre más corto esté contenido\n * dentro de ese mismo texto. Esto evita conflictos entre dos personas\n * distintas, por ejemplo:\n *\n * - JORGE LUIS MORALES PEREZ · Guatemala\n * - Jorge Luis Morales · otro país\n *\n * El rescate fuera del país permanece activo cuando no existe una\n * coincidencia exacta entre los perfiles de Guatemala.\n */\n if (\n targetExactAliasSet.has(\n normalize(relevantProfile.raw)\n )\n ) {\n contextualOutsideSkippedByTargetExact += 1;\n continue;\n }\n\n const sharedTokenCounts = new Map();\n\n for (const token of relevantProfile.tokens) {\n for (\n const outsideIndex of\n outsideTokenIndex.get(token) || []\n ) {\n sharedTokenCounts.set(\n outsideIndex,\n (\n sharedTokenCounts.get(\n outsideIndex\n ) || 0\n ) + 1\n );\n }\n }\n\n const uniqueMatches = new Map();\n\n for (\n const [\n outsideIndex,\n sharedTokenCount,\n ] of sharedTokenCounts\n ) {\n if (sharedTokenCount < 3) {\n continue;\n }\n\n const record =\n outsideRecords[outsideIndex];\n\n const matches =\n record.alias_profiles.some(\n (aliasProfile) =>\n profileContains(\n relevantProfile.tokens,\n relevantProfile.token_set,\n aliasProfile.tokens,\n aliasProfile.token_set\n )\n );\n\n if (!matches) continue;\n\n const key =\n employeeKey(record.employee);\n\n if (key) {\n uniqueMatches.set(\n key,\n record.employee\n );\n }\n }\n\n /*\n * Solo se rescata un perfil fuera del país cuando un nombre informativo\n * identifica exactamente a una única persona. Así se corrigen localidades\n * erróneas sin convertir nombres comunes en falsos positivos.\n */\n if (uniqueMatches.size !== 1) {\n continue;\n }\n\n const employee =\n uniqueMatches.values().next().value;\n\n const key = employeeKey(employee);\n\n if (!key) continue;\n\n contextualOutsideMap.set(key, {\n ...employee,\n validation_eligible: true,\n validation_scope:\n 'outside_country_unique_informative_name',\n });\n}\n\nconst validationEmployeeMap = new Map();\n\nfor (const employee of [\n ...targetEmployees,\n ...contextualOutsideMap.values(),\n]) {\n const key = employeeKey(employee);\n\n if (key) {\n validationEmployeeMap.set(\n key,\n employee\n );\n }\n}\n\nconst validationEmployees =\n Array.from(\n validationEmployeeMap.values()\n );\n\n\n/*\n * Resolución previa de nombres contra BambooHR.\n *\n * Cada nombre distinto recibido desde banco y nómina se resuelve una sola\n * vez, usando índices de alias y palabras. El resultado queda disponible\n * para el nodo de cruce mediante resolved_name_matches.\n */\nconst CONFIRMED_BAMBOO_NAME_ALIASES = new Map([\n [normalize(\"CARLOS DE LEON\"), normalize(\"Carlos Alexander De leon chajon\")],\n [normalize(\"CARLOS ALEXANDER DE LEON CHAJON\"), normalize(\"Carlos Alexander De leon chajon\")],\n [normalize(\"LISANDRO LINARES\"), normalize(\"Lisandro Antonio Linares giron\")],\n [normalize(\"LISANDRO ANTONIO LINARES GIRON\"), normalize(\"Lisandro Antonio Linares giron\")],\n [normalize(\"JULIO VELASQUEZ\"), normalize(\"Julio Francisco Velásquez\")],\n [normalize(\"JULIO FRANCISCO VELASQUEZ RAMIREZ\"), normalize(\"Julio Francisco Velásquez\")],\n [normalize(\"PABLO GIRON\"), normalize(\"Pablo Augusto Giron Robles\")],\n [normalize(\"PABLO AUGUSTO GIRON ROBLES\"), normalize(\"Pablo Augusto Giron Robles\")],\n [normalize(\"VERONICA GARCIA\"), normalize(\"Verónica Beatriz García Quan\")],\n [normalize(\"VERONICA BEATRIZ GARCIA\"), normalize(\"Verónica Beatriz García Quan\")],\n [normalize(\"VERONICA BEATRIZ GARCIA QUAN\"), normalize(\"Verónica Beatriz García Quan\")],\n [normalize(\"SANDRA LOPEZ\"), normalize(\"Sandra Lisbeth López Godoy\")],\n [normalize(\"SANDRA LIZBETH LOPEZ GODOY\"), normalize(\"Sandra Lisbeth López Godoy\")],\n [normalize(\"SANDRA LISBETH LOPEZ GODOY\"), normalize(\"Sandra Lisbeth López Godoy\")]\n]);\n\nfunction relevantEntryRaw(entry) {\n if (typeof entry === 'string') return clean(entry);\n return clean(entry?.raw || entry?.name || '');\n}\n\nfunction bambooResolutionEmployeeKey(employee) {\n return (\n clean(employee.bamboo_id) ||\n clean(employee.employee_number) ||\n normalize(employee.full_name)\n );\n}\n\nfunction bambooResolutionEditDistance(left, right) {\n const a = String(left || '');\n const b = String(right || '');\n\n if (a === b) return 0;\n if (!a) return b.length;\n if (!b) return a.length;\n\n let previous = Array.from(\n { length: b.length + 1 },\n (_, index) => index\n );\n\n for (let row = 1; row <= a.length; row++) {\n const current = [row];\n\n for (let column = 1; column <= b.length; column++) {\n const cost =\n a[row - 1] === b[column - 1]\n ? 0\n : 1;\n\n current[column] = Math.min(\n current[column - 1] + 1,\n previous[column] + 1,\n previous[column - 1] + cost\n );\n }\n\n previous = current;\n }\n\n return previous[b.length];\n}\nfunction bambooResolutionTokenSimilarity(left, right) {\n const a = String(left || '');\n const b = String(right || '');\n\n if (!a || !b) return 0;\n if (a === b) return 1;\n\n const minimumLength = Math.min(\n a.length,\n b.length\n );\n\n const maximumLength = Math.max(\n a.length,\n b.length\n );\n\n const distance =\n bambooResolutionEditDistance(a, b);\n\n if (\n minimumLength >= 4 &&\n distance <= 1\n ) {\n return Math.max(\n 0.90,\n 1 - distance / maximumLength\n );\n }\n\n if (\n minimumLength >= 6 &&\n distance <= 2\n ) {\n return Math.max(\n 0.82,\n 1 - distance / maximumLength\n );\n }\n\n const prefixOrSuffix =\n a.startsWith(b) ||\n b.startsWith(a) ||\n a.endsWith(b) ||\n b.endsWith(a);\n\n if (\n prefixOrSuffix &&\n minimumLength >= 4\n ) {\n return Math.max(\n 0.78,\n minimumLength / maximumLength\n );\n }\n\n return 0;\n}\n\nfunction bambooResolutionAliasDetails(\n queryName,\n aliasProfile\n) {\n const queryWords = Array.from(\n new Set(nameTokens(queryName))\n );\n\n const aliasWords =\n aliasProfile.words;\n\n if (\n queryWords.length < 2 ||\n aliasWords.length < 2\n ) {\n return null;\n }\n\n const aliasWordSet =\n aliasProfile.word_set;\n\n const queryWordSet =\n new Set(queryWords);\n\n const queryInsideAlias =\n queryWords.every((word) =>\n aliasWordSet.has(word)\n );\n\n const aliasInsideQuery =\n aliasWords.every((word) =>\n queryWordSet.has(word)\n );\n\n const usedAliasIndexes = new Set();\n const usedQueryIndexes = new Set();\n const similarities = new Array(\n queryWords.length\n ).fill(0);\n\n let exactMatches = 0;\n\n for (\n let queryIndex = 0;\n queryIndex < queryWords.length;\n queryIndex++\n ) {\n const aliasIndex =\n aliasWords.findIndex(\n (aliasWord, currentAliasIndex) =>\n !usedAliasIndexes.has(\n currentAliasIndex\n ) &&\n aliasWord ===\n queryWords[queryIndex]\n );\n\n if (aliasIndex < 0) continue;\n\n usedQueryIndexes.add(queryIndex);\n usedAliasIndexes.add(aliasIndex);\n similarities[queryIndex] = 1;\n exactMatches += 1;\n }\n\n const remainingQueryIndexes =\n queryWords\n .map((word, index) => ({\n word,\n index,\n }))\n .filter((entry) =>\n !usedQueryIndexes.has(entry.index)\n )\n .sort((left, right) =>\n right.word.length -\n left.word.length\n );\n\n for (const queryEntry of remainingQueryIndexes) {\n let bestSimilarity = 0;\n let bestAliasIndex = -1;\n\n for (\n let aliasIndex = 0;\n aliasIndex < aliasWords.length;\n aliasIndex++\n ) {\n if (\n usedAliasIndexes.has(\n aliasIndex\n )\n ) {\n continue;\n }\n\n const similarity =\n bambooResolutionTokenSimilarity(\n queryEntry.word,\n aliasWords[aliasIndex]\n );\n\n if (similarity > bestSimilarity) {\n bestSimilarity = similarity;\n bestAliasIndex = aliasIndex;\n }\n }\n\n if (\n bestAliasIndex >= 0 &&\n bestSimilarity >= 0.78\n ) {\n usedAliasIndexes.add(\n bestAliasIndex\n );\n similarities[queryEntry.index] =\n bestSimilarity;\n }\n }\n\n const matchedTokens =\n similarities.filter(\n (value) => value >= 0.78\n ).length;\n\n const queryCoverage =\n similarities.reduce(\n (sum, value) => sum + value,\n 0\n ) / queryWords.length;\n\n const aliasCoverage =\n matchedTokens /\n aliasWords.length;\n\n const lengthBalance =\n Math.min(\n queryWords.length,\n aliasWords.length\n ) /\n Math.max(\n queryWords.length,\n aliasWords.length\n );\n\n const score =\n queryCoverage * 0.65 +\n aliasCoverage * 0.20 +\n (\n exactMatches /\n queryWords.length\n ) * 0.10 +\n lengthBalance * 0.05;\n\n return {\n score,\n exact_matches: exactMatches,\n matched_tokens: matchedTokens,\n query_tokens:\n queryWords.length,\n alias_tokens:\n aliasWords.length,\n query_coverage:\n queryCoverage,\n alias_coverage:\n aliasCoverage,\n containment:\n queryInsideAlias ||\n aliasInsideQuery,\n };\n}\n\nconst bambooResolutionProfiles =\n validationEmployees.map(\n (employee, employeeIndex) => {\n const aliases = [];\n const seenAliases = new Set();\n\n for (\n const rawAlias of\n employee.aliases || []\n ) {\n const normalizedAlias =\n normalize(rawAlias);\n\n if (\n !normalizedAlias ||\n seenAliases.has(\n normalizedAlias\n )\n ) {\n continue;\n }\n\n seenAliases.add(\n normalizedAlias\n );\n\n const words = Array.from(\n new Set(nameTokens(rawAlias))\n );\n\n if (!words.length) continue;\n\n aliases.push({\n raw: clean(rawAlias),\n normalized:\n normalizedAlias,\n words,\n word_set:\n new Set(words),\n });\n }\n\n return {\n employee,\n employee_index:\n employeeIndex,\n employee_key:\n bambooResolutionEmployeeKey(\n employee\n ),\n aliases,\n };\n }\n );\n\nconst bambooResolutionExactAliasSets =\n new Map();\n\nconst bambooResolutionTokenSets =\n new Map();\n\nconst bambooResolutionTokenShapeSets =\n new Map();\n\nfor (\n let employeeIndex = 0;\n employeeIndex <\n bambooResolutionProfiles.length;\n employeeIndex++\n) {\n const profile =\n bambooResolutionProfiles[\n employeeIndex\n ];\n\n for (const alias of profile.aliases) {\n let exactSet =\n bambooResolutionExactAliasSets\n .get(alias.normalized);\n\n if (!exactSet) {\n exactSet = new Set();\n bambooResolutionExactAliasSets\n .set(\n alias.normalized,\n exactSet\n );\n }\n\n exactSet.add(employeeIndex);\n\n for (const token of alias.words) {\n if (token.length < 3) continue;\n\n let tokenSet =\n bambooResolutionTokenSets\n .get(token);\n\n if (!tokenSet) {\n tokenSet = new Set();\n bambooResolutionTokenSets\n .set(token, tokenSet);\n }\n\n tokenSet.add(employeeIndex);\n\n const tokenShape =\n `${token[0]}:${token.length}`;\n\n let shapeSet =\n bambooResolutionTokenShapeSets\n .get(tokenShape);\n\n if (!shapeSet) {\n shapeSet = new Set();\n bambooResolutionTokenShapeSets\n .set(\n tokenShape,\n shapeSet\n );\n }\n\n shapeSet.add(employeeIndex);\n }\n }\n}\n\nconst bambooResolutionExactAliasMap =\n new Map();\n\nfor (\n const [alias, indexes] of\n bambooResolutionExactAliasSets\n) {\n bambooResolutionExactAliasMap.set(\n alias,\n Array.from(indexes)\n );\n}\n\nfunction bambooResolutionDecision(\n queryName\n) {\n const rawQuery = clean(queryName);\n const normalizedQuery =\n normalize(rawQuery);\n\n const queryWords = Array.from(\n new Set(nameTokens(rawQuery))\n );\n\n if (\n !normalizedQuery ||\n queryWords.length < 2\n ) {\n return {\n found: false,\n matched_by: null,\n confidence: 0,\n reason:\n 'insufficient_name_tokens',\n };\n }\n\n const confirmedCanonical =\n CONFIRMED_BAMBOO_NAME_ALIASES\n .get(normalizedQuery);\n\n if (confirmedCanonical) {\n const confirmedIndexes =\n bambooResolutionExactAliasMap\n .get(confirmedCanonical) || [];\n\n if (confirmedIndexes.length === 1) {\n const employeeIndex =\n confirmedIndexes[0];\n\n return {\n found: true,\n matched_by:\n 'confirmed_alias_catalog',\n confidence: 1,\n employee_index:\n employeeIndex,\n employee_key:\n bambooResolutionProfiles[\n employeeIndex\n ].employee_key,\n query_name:\n rawQuery,\n bamboo_alias:\n bambooResolutionProfiles[\n employeeIndex\n ].aliases.find(\n (alias) =>\n alias.normalized ===\n confirmedCanonical\n )?.raw ||\n bambooResolutionProfiles[\n employeeIndex\n ].employee.full_name ||\n '',\n };\n }\n }\n\n const exactIndexes =\n bambooResolutionExactAliasMap\n .get(normalizedQuery) || [];\n\n if (exactIndexes.length === 1) {\n const employeeIndex =\n exactIndexes[0];\n\n return {\n found: true,\n matched_by:\n 'exact_precomputed_name',\n confidence: 1,\n employee_index:\n employeeIndex,\n employee_key:\n bambooResolutionProfiles[\n employeeIndex\n ].employee_key,\n query_name:\n rawQuery,\n bamboo_alias:\n bambooResolutionProfiles[\n employeeIndex\n ].aliases.find(\n (alias) =>\n alias.normalized ===\n normalizedQuery\n )?.raw ||\n bambooResolutionProfiles[\n employeeIndex\n ].employee.full_name ||\n '',\n };\n }\n\n const candidateVotes = new Map();\n\n function addCandidateVotes(\n indexes,\n weight\n ) {\n for (const employeeIndex of indexes) {\n candidateVotes.set(\n employeeIndex,\n (\n candidateVotes.get(\n employeeIndex\n ) || 0\n ) + weight\n );\n }\n }\n\n for (const token of queryWords) {\n addCandidateVotes(\n bambooResolutionTokenSets\n .get(token) || [],\n 4\n );\n\n for (\n let lengthOffset = -2;\n lengthOffset <= 2;\n lengthOffset++\n ) {\n const candidateLength =\n token.length + lengthOffset;\n\n if (candidateLength < 3) {\n continue;\n }\n\n addCandidateVotes(\n bambooResolutionTokenShapeSets\n .get(\n `${token[0]}:${candidateLength}`\n ) || [],\n 1\n );\n }\n }\n\n const candidateIndexes =\n Array.from(\n candidateVotes.entries()\n )\n .sort((left, right) =>\n right[1] - left[1]\n )\n .slice(0, 120)\n .map(([employeeIndex]) =>\n employeeIndex\n );\n\n const rankedCandidates = [];\n\n for (\n const employeeIndex of\n candidateIndexes\n ) {\n const profile =\n bambooResolutionProfiles[\n employeeIndex\n ];\n\n let bestDetails = null;\n let bestAlias = '';\n\n for (const alias of profile.aliases) {\n const details =\n bambooResolutionAliasDetails(\n rawQuery,\n alias\n );\n\n if (\n details &&\n (\n !bestDetails ||\n details.score >\n bestDetails.score\n )\n ) {\n bestDetails = details;\n bestAlias = alias.raw;\n }\n }\n\n if (!bestDetails) continue;\n\n rankedCandidates.push({\n employee_index:\n employeeIndex,\n employee_key:\n profile.employee_key,\n details:\n bestDetails,\n bamboo_alias:\n bestAlias,\n });\n }\n\n rankedCandidates.sort(\n (left, right) => {\n if (\n right.details.score !==\n left.details.score\n ) {\n return (\n right.details.score -\n left.details.score\n );\n }\n\n if (\n right.details.exact_matches !==\n left.details.exact_matches\n ) {\n return (\n right.details.exact_matches -\n left.details.exact_matches\n );\n }\n\n return (\n right.details.query_coverage -\n left.details.query_coverage\n );\n }\n );\n\n const best =\n rankedCandidates[0] || null;\n\n const second =\n rankedCandidates[1] || null;\n\n const margin =\n best\n ? best.details.score -\n (\n second?.details.score ||\n 0\n )\n : 0;\n\n const details =\n best?.details || null;\n\n const exactContainment =\n Boolean(\n details?.containment &&\n details.exact_matches >= 2\n );\n\n const strongTwoTokenName =\n Boolean(\n details &&\n details.query_tokens === 2 &&\n details.matched_tokens === 2 &&\n details.exact_matches >= 1 &&\n details.query_coverage >= 0.90 &&\n details.score >= 0.88\n );\n\n const strongLongName =\n Boolean(\n details &&\n details.query_tokens >= 3 &&\n details.matched_tokens >=\n Math.min(\n 3,\n details.query_tokens\n ) &&\n details.exact_matches >= 2 &&\n details.query_coverage >= 0.85 &&\n details.score >= 0.84\n );\n\n const acceptableMargin =\n !second ||\n margin >= (\n exactContainment\n ? 0.04\n : 0.06\n ) ||\n (\n details?.exact_matches || 0\n ) >\n (\n second?.details\n ?.exact_matches || 0\n );\n\n if (\n best &&\n acceptableMargin &&\n (\n exactContainment ||\n strongTwoTokenName ||\n strongLongName\n )\n ) {\n return {\n found: true,\n matched_by:\n exactContainment\n ? 'unique_precomputed_containment'\n : 'strong_precomputed_fuzzy_name',\n confidence:\n Math.min(\n 1,\n details.score\n ),\n employee_index:\n best.employee_index,\n employee_key:\n best.employee_key,\n query_name:\n rawQuery,\n bamboo_alias:\n best.bamboo_alias,\n margin,\n exact_matches:\n details.exact_matches,\n matched_tokens:\n details.matched_tokens,\n };\n }\n\n return {\n found: false,\n matched_by: null,\n confidence:\n details?.score || 0,\n reason:\n best\n ? (\n acceptableMargin\n ? 'insufficient_name_evidence'\n : 'ambiguous_name'\n )\n : 'no_candidate',\n best_candidate:\n best\n ? {\n employee_index:\n best.employee_index,\n employee_key:\n best.employee_key,\n bamboo_alias:\n best.bamboo_alias,\n score:\n best.details.score,\n }\n : null,\n second_candidate:\n second\n ? {\n employee_index:\n second.employee_index,\n employee_key:\n second.employee_key,\n bamboo_alias:\n second.bamboo_alias,\n score:\n second.details.score,\n }\n : null,\n };\n}\n\nconst resolvedNameMatches = {};\nlet resolvedNameMatchesFound = 0;\n\nfor (\n const [\n normalizedRelevantName,\n relevantEntry,\n ] of relevantNameMap\n) {\n const rawRelevantName =\n relevantEntryRaw(relevantEntry);\n\n const decision =\n bambooResolutionDecision(\n rawRelevantName\n );\n\n resolvedNameMatches[\n normalizedRelevantName\n ] = decision;\n\n if (decision.found) {\n resolvedNameMatchesFound += 1;\n }\n}\n\n\nconst fetchedEmployeesCount =\n rawEmployees.length;\n\nconst fetchComplete =\n expectedTotal > 0\n ? fetchedEmployeesCount >= expectedTotal\n : (\n pageObjects.length > 0 &&\n !pageObjects.some(\n (page) =>\n Boolean(\n page?._links?.next?.href\n )\n )\n );\n\nconst errors = [];\n\nif (!pageObjects.length) {\n errors.push(\n 'BambooHR no devolvió páginas de empleados.'\n );\n}\n\nif (!fetchedEmployeesCount) {\n errors.push(\n 'BambooHR no devolvió empleados.'\n );\n}\n\nif (\n expectedTotal > 0 &&\n fetchedEmployeesCount < expectedTotal\n) {\n errors.push(\n `La descarga de BambooHR quedó incompleta: ` +\n `${fetchedEmployeesCount} de ${expectedTotal} empleados.`\n );\n}\n\nif (!targetEmployees.length) {\n errors.push(\n 'No se encontraron empleados de Guatemala en BambooHR.'\n );\n}\n\nreturn [\n {\n json: {\n ...base,\n ok:\n Boolean(base.ok ?? true) &&\n errors.length === 0,\n stage:\n errors.length === 0\n ? 'bamboohr_gt_normalizado'\n : 'bamboohr_gt_incompleto',\n errors: [\n ...(Array.isArray(base.errors)\n ? base.errors\n : []),\n ...errors,\n ],\n bamboo: {\n source:\n 'bamboohr_custom_report_only_current_false',\n period_start: periodStart,\n period_end: periodEnd,\n pages_fetched: pageObjects.length,\n expected_total: expectedTotal,\n raw_employees_count:\n fetchedEmployeesCount,\n employees_count:\n normalizedEmployeesCount,\n guatemala_count:\n targetEmployees.length,\n active_in_period_count:\n targetEmployees.filter(\n (employee) =>\n employee.overlaps_period\n ).length,\n active_status_count:\n targetEmployees.filter(\n (employee) =>\n normalize(employee.status) ===\n 'active'\n ).length,\n contextual_outside_country_count:\n contextualOutsideMap.size,\n contextual_outside_skipped_by_target_exact_count:\n contextualOutsideSkippedByTargetExact,\n validation_candidates_count:\n validationEmployees.length,\n resolved_name_matches:\n resolvedNameMatches,\n resolved_name_matches_count:\n Object.keys(\n resolvedNameMatches\n ).length,\n resolved_name_matches_found:\n resolvedNameMatchesFound,\n name_resolution_strategy:\n 'precomputed_indexed_fuzzy_matching_with_target_exact_precedence',\n fetch_complete: fetchComplete,\n validation_available:\n fetchComplete &&\n validationEmployees.length > 0,\n validation_rule:\n 'Target country/location first; outside-country contextual rescue only when no exact target alias exists',\n performance_strategy:\n 'precomputed_alias_tokens_and_inverted_index',\n employees:\n validationEmployees,\n restricted_fields:\n restrictedFields,\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 15440, - 25568 - ], - "id": "a0b69501-6075-4d1c-819e-a7b15a683ddb", - "name": "Normalizar BambooHR GT" - }, - { - "parameters": { - "mode": "combine", - "combineBy": "combineByPosition", - "options": {} - }, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 21472, - 26512 - ], - "id": "4ba30bd6-5082-4299-aaaa-4082014f781e", - "name": "Merge - Agregar BambooHR" - }, - { - "parameters": { - "content": "# 📥 ENTRADA Y EXTRACCIÓN DE DATOS — GUATEMALA\n\nRecibe desde el Portal de Verificación de Nóminas los archivos y parámetros necesarios para ejecutar el cruce de Guatemala.\n\nFuentes procesadas:\n\n- Directorio de empleados de BambooHR.\n- Archivo CSV del banco.\n- Archivo Excel de nómina con múltiples hojas.\n- Hojas adicionales de pagos, bonos, viáticos, combustibles, auditorías, temporales y otras unidades.\n\nEste bloque:\n\n1. Recibe la solicitud de la aplicación.\n2. Normaliza año, mes y tipo de período.\n3. Consulta los empleados disponibles en BambooHR.\n4. Estandariza nombres, correos e identificadores.\n5. Convierte el CSV bancario en registros procesables.\n6. Extrae cada hoja relevante del archivo Excel.\n7. Mantiene la hoja de origen de cada registro para facilitar validaciones.\n\nReglas:\n\n- No iniciar el cruce sin los archivos obligatorios.\n- Mantener separados banco, nómina y BambooHR.\n- No asumir que todas las hojas tienen la misma estructura.\n- No perder la procedencia de los registros.\n- Preparar todas las fuentes en un formato compatible con la consolidación.", - "height": 2992, - "width": 1760, - "color": "#2D305D" - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - 14432, - 24992 - ], - "id": "a9eedba9-b326-4097-b31a-9cb977f38400", - "name": "Sticky Note" - }, - { - "parameters": { - "content": "# 🧩 CONSOLIDACIÓN DE NÓMINA — GUATEMALA\n\nUne progresivamente todas las hojas extraídas hasta construir una sola nómina consolidada del período.\n\nDurante la consolidación:\n\n- Se agregan las hojas en una secuencia controlada.\n- Se conserva la unidad o pestaña de procedencia.\n- Se eliminan filas completamente vacías.\n- Se estandarizan encabezados y tipos de datos.\n- Se normalizan nombres de empleados.\n- Se limpian espacios, símbolos y caracteres especiales.\n- Se convierten montos y fechas a formatos consistentes.\n\nLa salida de este bloque representa la nómina completa que será comparada con el banco y BambooHR.\n\nReglas:\n\n- No sobrescribir registros de hojas anteriores.\n- No eliminar empleados únicamente porque aparezcan en más de una hoja.\n- Identificar correctamente posibles duplicados reales.\n- Mantener los valores originales para revisión.\n- No enviar hojas individualmente a la etapa de cruce.", - "height": 2336, - "width": 3984, - "color": "#216353" - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - 16368, - 25136 - ], - "id": "458cfcea-4a35-4a73-bfd9-c2689bb88d0b", - "name": "Sticky Note1" - }, - { - "parameters": { - "content": "# 🔍 CRUCE Y REPORTE FINAL — GUATEMALA\n\nCombina la nómina consolidada con el archivo bancario y la información oficial de BambooHR.\n\nEl cruce permite detectar:\n\n- Diferencias entre nómina y banco.\n- Empleados presentes solamente en nómina.\n- Registros presentes solamente en el banco.\n- Empleados no encontrados en BambooHR.\n- Posibles diferencias de nombres, cuentas o identificadores.\n- Diferencias en montos pagados.\n- Registros que requieren revisión manual.\n\nDespués del análisis:\n\n1. Se organizan los resultados por hoja y categoría.\n2. Se prepara la estructura del reporte.\n3. Se crea un nuevo Google Sheet.\n4. Se escriben encabezados, resultados y resúmenes.\n5. Se aplican formatos de moneda, fechas y columnas.\n6. Se configuran los permisos.\n7. Se comparte el archivo con los usuarios autorizados.\n\nReglas:\n\n- No depender únicamente del nombre para relacionar empleados.\n- No ocultar registros sin coincidencia.\n- No compartir el Sheet antes de finalizar su escritura.\n- No devolver el enlace hasta confirmar que el archivo existe.\n- Google Sheets funciona como entregable; las fuentes originales siguen siendo BambooHR, nómina y banco.", - "height": 608, - "width": 2608, - "color": "#5F721D" - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - 20752, - 26288 - ], - "id": "74dfb3d9-327a-428e-a0d8-400436289f6c", - "name": "Sticky Note2" - }, - { - "parameters": { - "content": "# 🗂️ HISTÓRICO Y RESPUESTA FINAL\n\nRegistra en Supabase la ejecución completada y devuelve el resultado al Portal de Verificación de Nóminas.\n\nEl histórico puede almacenar:\n\n- País: Guatemala.\n- Año y mes procesados.\n- Tipo de período.\n- Fecha de ejecución.\n- Usuario que inició el proceso.\n- Cantidad de registros analizados.\n- Cantidad de diferencias o hallazgos.\n- Enlace del Google Sheet.\n- Estado inicial del reporte.\n- Identificador de la ejecución.\n\nDespués del registro:\n\n1. Se construye la respuesta para la aplicación.\n2. Se incluye el enlace al reporte generado.\n3. Se devuelve el resumen de resultados.\n4. Se informa si el procesamiento terminó correctamente.\n5. Se cierra la solicitud mediante Respond to Webhook.\n\nReglas:\n\n- Registrar el histórico solamente después de crear el reporte.\n- No declarar éxito si falló el Sheet o Supabase.\n- No devolver credenciales ni información interna.\n- Mantener una estructura estable para la aplicación.\n- Supabase es la fuente oficial de los históricos mostrados en el portal.", - "height": 656, - "width": 1760, - "color": 3 - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - 23536, - 26256 - ], - "id": "86284c0d-1d6b-4ed5-84a6-2dea3aaed6e8", - "name": "Sticky Note3" - } - ], - "pinData": { - "Webhook": [ - { - "json": { - "headers": { - "host": "agenteit.digitalcompass.agency", - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", - "content-length": "823089", - "accept": "*/*", - "accept-encoding": "gzip, deflate, br, zstd", - "accept-language": "es-ES,es;q=0.9", - "content-type": "multipart/form-data; boundary=----WebKitFormBoundaryVu8EZkp7sVCkGzUo", - "origin": "https://digitalcompass.agency", - "priority": "u=1, i", - "referer": "https://digitalcompass.agency/", - "sec-ch-ua": "\"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"150\", \"Google Chrome\";v=\"150\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "same-site", - "x-forwarded-for": "186.7.36.83", - "x-forwarded-host": "agenteit.digitalcompass.agency", - "x-forwarded-port": "443", - "x-forwarded-proto": "https", - "x-forwarded-server": "07b4f09d2c65", - "x-real-ip": "186.7.36.83" - }, - "params": {}, - "query": {}, - "body": { - "metadata": "{\"country\":\"GT\",\"country_name\":\"Guatemala\",\"year\":2026,\"month\":6,\"period_type\":\"quincena_30\",\"period_label\":\"Junio 2026 · Quincena 30 / fin de mes\",\"period_start\":\"2026-06-16\",\"period_end\":\"2026-06-30\",\"payroll_file_name\":\"2Q GT_Nómina P&G, Nestle, Purina, Whirpool, Motorola_GLM_JUNIO 30 2026.xlsx\",\"bank_file_names\":[\"Consulta Detalle de Envío (61).csv\",\"Consulta Detalle de Envío (62).csv\",\"Consulta Detalle de Envío (63).csv\",\"Consulta Detalle de Envío (64).csv\",\"Consulta Detalle de Envío (65).csv\",\"Consulta Detalle de Envío (66).csv\",\"Consulta Detalle de Envío (68).csv\",\"Consulta Detalle de Envío (69).csv\",\"Consulta Detalle de Envío (70).csv\",\"Consulta Detalle de Envío (71).csv\",\"Consulta Detalle de Envío (73).csv\",\"Consulta Detalle de Envío (74).csv\",\"Consulta Detalle de Envío (75).csv\",\"Consulta Detalle de Envío (76).csv\",\"Consulta Detalle de Envío (77).csv\",\"Consulta Detalle de Envío (78).csv\",\"Consulta Detalle de Envío (79).csv\",\"Consulta Detalle de Envío (80).csv\",\"Consulta Detalle de Envío (83).csv\"],\"requested_by_name\":\"Isaac Aracena\",\"requested_by_email\":\"iaracena@gomezleemarketing.com\",\"auth_mode\":\"supabase_google\",\"source_app\":\"portal-cruce-cuentas-glm\"}", - "country": "GT", - "country_name": "Guatemala", - "year": "2026", - "month": "6", - "period_type": "quincena_30", - "period_label": "Junio 2026 · Quincena 30 / fin de mes", - "period_start": "2026-06-16", - "period_end": "2026-06-30", - "payroll_file_name": "2Q GT_Nómina P&G, Nestle, Purina, Whirpool, Motorola_GLM_JUNIO 30 2026.xlsx", - "bank_file_names": "[\"Consulta Detalle de Envío (61).csv\",\"Consulta Detalle de Envío (62).csv\",\"Consulta Detalle de Envío (63).csv\",\"Consulta Detalle de Envío (64).csv\",\"Consulta Detalle de Envío (65).csv\",\"Consulta Detalle de Envío (66).csv\",\"Consulta Detalle de Envío (68).csv\",\"Consulta Detalle de Envío (69).csv\",\"Consulta Detalle de Envío (70).csv\",\"Consulta Detalle de Envío (71).csv\",\"Consulta Detalle de Envío (73).csv\",\"Consulta Detalle de Envío (74).csv\",\"Consulta Detalle de Envío (75).csv\",\"Consulta Detalle de Envío (76).csv\",\"Consulta Detalle de Envío (77).csv\",\"Consulta Detalle de Envío (78).csv\",\"Consulta Detalle de Envío (79).csv\",\"Consulta Detalle de Envío (80).csv\",\"Consulta Detalle de Envío (83).csv\"]", - "requested_by_name": "Isaac Aracena", - "requested_by_email": "iaracena@gomezleemarketing.com", - "auth_mode": "supabase_google", - "source_app": "portal-cruce-cuentas-glm" - }, - "webhookUrl": "https://agenteit.digitalcompass.agency/webhook/nominagt-bamboo-test", - "executionMode": "production" - }, - "binary": { - "payroll_file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "fileExtension": "xlsx", - "data": "filesystem-v2", - "fileName": "2Q GT_Nómina P&G, Nestle, Purina, Whirpool, Motorola_GLM_JUNIO 30 2026.xlsx", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/2a413d82-90aa-44bf-a25f-2fbc8c6825b0", - "fileSize": "636 kB", - "bytes": 636171 - }, - "bank_files0": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (61).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/7870646d-3320-49e8-a86a-f49021356b44", - "fileSize": "1.74 kB", - "bytes": 1739 - }, - "bank_files1": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (62).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/5f70d84b-10a6-4d41-8418-cc6ed32ba09d", - "fileSize": "881 B", - "bytes": 881 - }, - "bank_files2": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (63).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/0ace2ca2-8c32-430a-af36-54383bca5262", - "fileSize": "683 B", - "bytes": 683 - }, - "bank_files3": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (64).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/a94897d8-b548-44ce-8897-7516ecc56307", - "fileSize": "36.5 kB", - "bytes": 36547 - }, - "bank_files4": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (66).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/9792f09c-8e4b-455c-85fa-b36d7d46fe7a", - "fileSize": "1.01 kB", - "bytes": 1012 - }, - "bank_files5": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (68).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/04fcc1f2-ea0f-472f-a3d7-6932157d7f6a", - "fileSize": "1.01 kB", - "bytes": 1012 - }, - "bank_files6": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (69).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/07dbf710-b954-4308-835b-13dbe821bdb5", - "fileSize": "11.4 kB", - "bytes": 11445 - }, - "bank_files7": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (70).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/42bd4686-90ed-433a-a869-2bc53bfba8a7", - "fileSize": "7.74 kB", - "bytes": 7743 - }, - "bank_files8": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (65).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/a5e274a1-de2a-4eff-b0b2-16f71f33706e", - "fileSize": "5.65 kB", - "bytes": 5650 - }, - "bank_files9": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (71).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/ad1109c0-150c-49ab-b1c0-397f4e741919", - "fileSize": "2.74 kB", - "bytes": 2739 - }, - "bank_files10": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (73).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/9cc8023d-a12a-425e-a106-70cc982b1d0d", - "fileSize": "3.41 kB", - "bytes": 3413 - }, - "bank_files11": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (75).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/ad5e812a-7602-496b-a425-ad6f47f140bb", - "fileSize": "2.44 kB", - "bytes": 2443 - }, - "bank_files12": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (76).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/4e95c954-5903-42ea-b25d-2f7b919d6cbc", - "fileSize": "2.39 kB", - "bytes": 2393 - }, - "bank_files13": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (77).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/d04ec36f-71f5-4d09-8a75-295d2217a752", - "fileSize": "1.21 kB", - "bytes": 1208 - }, - "bank_files14": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (74).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/32750d16-6ae1-4297-8243-9b8895217384", - "fileSize": "3.23 kB", - "bytes": 3225 - }, - "bank_files15": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (78).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/c6098992-9b14-4078-a4b0-ce473be356ab", - "fileSize": "2.4 kB", - "bytes": 2402 - }, - "bank_files16": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (79).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/6b322b42-66ad-4b2c-bebf-eda89c484d2b", - "fileSize": "2.04 kB", - "bytes": 2043 - }, - "bank_files17": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (80).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/7c91ea6c-73d6-488a-9c5c-2b1f287b54a6", - "fileSize": "672 B", - "bytes": 672 - }, - "bank_files18": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (83).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/a49f2f66-49c0-4e12-800b-901aa0b48f52", - "fileSize": "1.01 kB", - "bytes": 1006 - }, - "bank_file_1": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (61).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/355c42f8-a692-4368-be11-15bad1cc6eb8", - "fileSize": "1.74 kB", - "bytes": 1739 - }, - "bank_file_2": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (62).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/9d4d9b3b-dd63-4a05-879b-bb0b588b61c3", - "fileSize": "881 B", - "bytes": 881 - }, - "bank_file_3": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (63).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/6f8b69af-f9a7-4c12-b5d3-fe23c5990fb9", - "fileSize": "683 B", - "bytes": 683 - }, - "bank_file_4": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (64).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/b4ab07ef-c57f-4b9d-b122-353d00702b49", - "fileSize": "36.5 kB", - "bytes": 36547 - }, - "bank_file_5": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (65).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/9cc78cdc-0d37-4992-b175-f5f90822a3ce", - "fileSize": "5.65 kB", - "bytes": 5650 - }, - "bank_file_6": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (66).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/b9d27045-f83c-4f5f-9837-935afcaae2d3", - "fileSize": "1.01 kB", - "bytes": 1012 - }, - "bank_file_7": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (68).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/de2db25a-ef53-4b06-a001-ad0f90ca6c6a", - "fileSize": "1.01 kB", - "bytes": 1012 - }, - "bank_file_8": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (69).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/54850c19-b3d2-4ad1-9388-61bb28472607", - "fileSize": "11.4 kB", - "bytes": 11445 - }, - "bank_file_9": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (70).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/77a463eb-250d-4740-984a-68ec2668bcaf", - "fileSize": "7.74 kB", - "bytes": 7743 - }, - "bank_file_10": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (71).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/f7c33325-205b-40e4-943e-0e0029da8349", - "fileSize": "2.74 kB", - "bytes": 2739 - }, - "bank_file_11": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (73).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/2881deb6-29e7-4bd9-af9b-7163a9ddabf0", - "fileSize": "3.41 kB", - "bytes": 3413 - }, - "bank_file_12": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (74).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/bac0f5cf-62fb-421a-9163-3429c21ad86d", - "fileSize": "3.23 kB", - "bytes": 3225 - }, - "bank_file_13": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (75).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/4ef606c3-08cf-4c72-b3f3-9d5afd36160a", - "fileSize": "2.44 kB", - "bytes": 2443 - }, - "bank_file_14": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (76).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/8eb4d533-0480-40e8-ac2f-c3ebf99fdfb9", - "fileSize": "2.39 kB", - "bytes": 2393 - }, - "bank_file_15": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (77).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/9983752c-c90f-43ce-a8df-7b4ef0598a48", - "fileSize": "1.21 kB", - "bytes": 1208 - }, - "bank_file_16": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (78).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/7c94ec4b-737d-4648-9785-7c4f395d7ccb", - "fileSize": "2.4 kB", - "bytes": 2402 - }, - "bank_file_17": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (79).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/3d1593bd-3e61-498f-a065-44d70e0c959a", - "fileSize": "2.04 kB", - "bytes": 2043 - }, - "bank_file_18": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (80).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/4c3ef003-1018-4de6-bcfd-c7aac67abf2a", - "fileSize": "672 B", - "bytes": 672 - }, - "bank_file_19": { - "mimeType": "text/csv", - "fileType": "text", - "fileExtension": "csv", - "data": "filesystem-v2", - "fileName": "Consulta Detalle de Envío (83).csv", - "id": "filesystem-v2:workflows/d3vXYr7ucbaiU5ct/executions/37128/binary_data/451f19d0-dc67-4f5e-9696-0ff8eab5d2d3", - "fileSize": "1.01 kB", - "bytes": 1006 - } - }, - "pairedItem": { - "item": 0 - } - } - ] - }, - "connections": { - "Webhook": { - "main": [ - [ - { - "node": "Preparar entrada app", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar entrada app": { - "main": [ - [ - { - "node": "Parsear CSV banco GT", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Nomina General", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Temporales", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Auditorias", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Bono Mariana", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Movilidad WP", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Viaticos PMI", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Combustible Purina", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Combustible PG", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Combustibles Liquidables", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Mot Variable Abril", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Temporales WMC", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parsear CSV banco GT": { - "main": [ - [ - { - "node": "Merge", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge": { - "main": [ - [ - { - "node": "Merge - Agregar BambooHR", - "type": "main", - "index": 0 - }, - { - "node": "HTTP - Empleados BambooHR GT", - "type": "main", - "index": 0 - } - ] - ] - }, - "Cruzar Nómina vs Banco": { - "main": [ - [ - { - "node": "Preparar Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - Nomina General": { - "main": [ - [ - { - "node": "Merge Hojas 01-02", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - Temporales": { - "main": [ - [ - { - "node": "Merge Hojas 01-02", - "type": "main", - "index": 1 - } - ] - ] - }, - "Extract - Auditorias": { - "main": [ - [ - { - "node": "Merge Hojas 03", - "type": "main", - "index": 1 - } - ] - ] - }, - "Extract - Bono Mariana": { - "main": [ - [ - { - "node": "Merge Hojas 04", - "type": "main", - "index": 1 - } - ] - ] - }, - "Extract - Movilidad WP": { - "main": [ - [ - { - "node": "Merge Hojas 05", - "type": "main", - "index": 1 - } - ] - ] - }, - "Extract - Viaticos PMI": { - "main": [ - [ - { - "node": "Merge Hojas 06", - "type": "main", - "index": 1 - } - ] - ] - }, - "Extract - Combustible Purina": { - "main": [ - [ - { - "node": "Merge Hojas 07", - "type": "main", - "index": 1 - } - ] - ] - }, - "Extract - Combustible PG": { - "main": [ - [ - { - "node": "Merge Hojas 08", - "type": "main", - "index": 1 - } - ] - ] - }, - "Extract - Combustibles Liquidables": { - "main": [ - [ - { - "node": "Merge Hojas 09", - "type": "main", - "index": 1 - } - ] - ] - }, - "Normalizar Nómina Completa": { - "main": [ - [ - { - "node": "Merge", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge Hojas 01-02": { - "main": [ - [ - { - "node": "Merge Hojas 03", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Hojas 03": { - "main": [ - [ - { - "node": "Merge Hojas 04", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Hojas 04": { - "main": [ - [ - { - "node": "Merge Hojas 05", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Hojas 05": { - "main": [ - [ - { - "node": "Merge Hojas 06", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Hojas 06": { - "main": [ - [ - { - "node": "Merge Hojas 07", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Hojas 07": { - "main": [ - [ - { - "node": "Merge Hojas 08", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Hojas 08": { - "main": [ - [ - { - "node": "Merge Hojas 09", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Hojas 09": { - "main": [ - [ - { - "node": "Merge Hojas ", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - Mot Variable Abril": { - "main": [ - [ - { - "node": "Merge Hojas ", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge Hojas ": { - "main": [ - [ - { - "node": "Merge Hojas 10", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar Google Sheet": { - "main": [ - [ - { - "node": "Crear Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Crear Google Sheet": { - "main": [ - [ - { - "node": "Escribir Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Escribir Google Sheet": { - "main": [ - [ - { - "node": "Formatear Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Formatear Google Sheet": { - "main": [ - [ - { - "node": "Preparar permisos Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar respuesta final": { - "main": [ - [ - { - "node": "Respond to Webhook", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar permisos Google Sheet": { - "main": [ - [ - { - "node": "Compartir Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Compartir Google Sheet": { - "main": [ - [ - { - "node": "Preparar histórico Supabase", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar histórico Supabase": { - "main": [ - [ - { - "node": "Insertar histórico Supabase", - "type": "main", - "index": 0 - } - ] - ] - }, - "Insertar histórico Supabase": { - "main": [ - [ - { - "node": "Preparar respuesta final", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - Temporales WMC": { - "main": [ - [ - { - "node": "Merge Hojas 10", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge Hojas 10": { - "main": [ - [ - { - "node": "Normalizar Nómina Completa", - "type": "main", - "index": 0 - } - ] - ] - }, - "HTTP - Empleados BambooHR GT": { - "main": [ - [ - { - "node": "Normalizar BambooHR GT", - "type": "main", - "index": 0 - } - ] - ] - }, - "Normalizar BambooHR GT": { - "main": [ - [ - { - "node": "Merge - Agregar BambooHR", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge - Agregar BambooHR": { - "main": [ - [ - { - "node": "Cruzar Nómina vs Banco", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1", - "binaryMode": "separate", - "availableInMCP": true, - "timeSavedMode": "fixed", - "errorWorkflow": "puF4LUczoSz3hcek", - "timezone": "America/Santo_Domingo", - "callerPolicy": "workflowsFromSameOwner" - }, - "versionId": "afef0d83-31fc-44b2-87dd-05536d3520f2", - "meta": { - "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" - }, - "id": "d3vXYr7ucbaiU5ct", - "tags": [] -} \ No newline at end of file diff --git a/Flujo de n8n: Portal de Verificación de Nómina - TT.json b/Flujo de n8n: Portal de Verificación de Nómina - TT.json deleted file mode 100644 index d2d8e86..0000000 --- a/Flujo de n8n: Portal de Verificación de Nómina - TT.json +++ /dev/null @@ -1,1066 +0,0 @@ -{ - "name": "Portal de Verificación de Nómina - TT", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "nominatt-bamboo-test", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2.1, - "position": [ - 2848, - 7344 - ], - "id": "0a3764cc-5cc2-4c62-93f8-1d201ec45e9d", - "name": "Webhook", - "webhookId": "9c730860-7790-43a5-a3c0-bf5984ced244" - }, - { - "parameters": { - "jsCode": "const item = $input.first();\n\nconst body = item.json.body || {};\nconst binary = item.binary || {};\n\nlet metadata = {};\n\ntry {\n metadata = typeof body.metadata === 'string'\n ? JSON.parse(body.metadata)\n : body.metadata || {};\n} catch (error) {\n metadata = {};\n}\n\nconst binaryKeys = Object.keys(binary);\n\nconst payrollKey = binaryKeys.find(\n (key) => key === 'payroll_file'\n);\n\nconst bankKeys = binaryKeys\n .filter((key) => key.startsWith('bank_files'))\n .sort();\n\nconst payrollFile = payrollKey\n ? {\n binary_key: payrollKey,\n file_name: binary[payrollKey].fileName,\n file_extension: binary[payrollKey].fileExtension,\n mime_type: binary[payrollKey].mimeType,\n file_size: binary[payrollKey].fileSize,\n }\n : null;\n\nconst bankFiles = bankKeys.map((key) => ({\n binary_key: key,\n file_name: binary[key].fileName,\n file_extension: binary[key].fileExtension,\n mime_type: binary[key].mimeType,\n file_size: binary[key].fileSize,\n}));\n\nconst receivedCountry = String(\n metadata.country || ''\n).trim().toUpperCase();\n\nconst errors = [];\n\nif (!['TT', 'TTO'].includes(receivedCountry)) {\n errors.push(\n 'El país recibido no es Trinidad y Tobago.'\n );\n}\n\nif (!metadata.year) {\n errors.push('No se recibió el año del cruce.');\n}\n\nif (!metadata.month) {\n errors.push('No se recibió el mes del cruce.');\n}\n\nif (!metadata.period_type) {\n errors.push('No se recibió el tipo de quincena.');\n}\n\nif (!metadata.period_start || !metadata.period_end) {\n errors.push('No se recibió el período calculado.');\n}\n\nif (!payrollFile) {\n errors.push('No se recibió el archivo de nómina.');\n}\n\nif (bankFiles.length === 0) {\n errors.push(\n 'No se recibió ningún archivo CSV del banco.'\n );\n}\n\nconst normalizedMetadata = {\n ...metadata,\n country: 'TT',\n country_name: 'Trinidad y Tobago',\n source_app:\n metadata.source_app ||\n 'cruce-cuentas-glm-trinidad-tobago',\n payroll_file_name:\n metadata.payroll_file_name ||\n payrollFile?.file_name ||\n '',\n bank_file_names:\n metadata.bank_file_names ||\n bankFiles.map((file) => file.file_name),\n};\n\nreturn [\n {\n json: {\n ok: errors.length === 0,\n stage: 'entrada_tt_recibida',\n errors,\n metadata: normalizedMetadata,\n payroll_file: payrollFile,\n bank_files: bankFiles,\n summary: {\n payroll_files_count:\n payrollFile ? 1 : 0,\n bank_files_count: bankFiles.length,\n },\n },\n binary,\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3088, - 7344 - ], - "id": "bdfa28aa-5a9e-4c08-9869-0bafa2a8cb52", - "name": "Preparar entrada app" - }, - { - "parameters": { - "jsCode": "const input = $input.first();\nconst json = input.json || {};\nconst binary = input.binary || {};\n\nfunction parseCsvLine(line) {\n const result = [];\n let current = '';\n let insideQuotes = false;\n\n for (let index = 0; index < line.length; index++) {\n const character = line[index];\n const nextCharacter = line[index + 1];\n\n if (\n character === '\"' &&\n insideQuotes &&\n nextCharacter === '\"'\n ) {\n current += '\"';\n index += 1;\n continue;\n }\n\n if (character === '\"') {\n insideQuotes = !insideQuotes;\n continue;\n }\n\n if (character === ',' && !insideQuotes) {\n result.push(current.trim());\n current = '';\n continue;\n }\n\n current += character;\n }\n\n result.push(current.trim());\n return result;\n}\n\nfunction normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\u00A0/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeForCompare(value) {\n return normalizeText(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/['’`-]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeAccount(value) {\n return String(value ?? '')\n .replace(/\\u00A0/g, '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim();\n}\n\nfunction isValidAccount(value) {\n const account = normalizeAccount(value);\n return (\n account.length >= 6 &&\n !/^0+$/.test(account)\n );\n}\n\nfunction parseMoney(value) {\n const cleaned = String(value ?? '')\n .replace(/TTD/gi, '')\n .replace(/TT\\$/gi, '')\n .replace(/\\$/g, '')\n .replace(/,/g, '')\n .replace(/\\s+/g, '')\n .trim();\n\n const parsed = Number.parseFloat(cleaned);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction roundMoney(value) {\n return Math.round(\n (Number(value) || 0) * 100\n ) / 100;\n}\n\nfunction getColumnIndex(headers, names) {\n const normalizedHeaders =\n headers.map(normalizeForCompare);\n\n for (const name of names) {\n const expected = normalizeForCompare(name);\n const index = normalizedHeaders.findIndex(\n (header) => header === expected\n );\n\n if (index >= 0) return index;\n }\n\n return -1;\n}\n\nconst bankKeys = Object.keys(binary)\n .filter((key) => key.startsWith('bank_files'))\n .sort();\n\nconst allBankRows = [];\nconst fileSummaries = [];\n\nfor (const key of bankKeys) {\n const file = binary[key];\n const buffer =\n await this.helpers.getBinaryDataBuffer(0, key);\n\n let text = buffer.toString('utf8');\n\n if (text.includes('\\uFFFD')) {\n text = buffer.toString('latin1');\n }\n\n const lines = text\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean);\n\n const headerIndex = lines.findIndex((line) => {\n const normalized = normalizeForCompare(line);\n\n return (\n normalized.includes('identifier') &&\n normalized.includes('account number') &&\n normalized.includes('amount') &&\n normalized.includes('participant name')\n );\n });\n\n if (headerIndex < 0) {\n fileSummaries.push({\n file_name: file.fileName,\n ok: false,\n rows_count: 0,\n total_amount: 0,\n error:\n 'No se encontró el encabezado esperado del archivo bancario de Trinidad y Tobago.',\n });\n continue;\n }\n\n const headers = parseCsvLine(\n lines[headerIndex]\n ).map(normalizeText);\n\n const indexIdentifier = getColumnIndex(\n headers,\n ['Identifier']\n );\n const indexAccount = getColumnIndex(\n headers,\n ['Account Number']\n );\n const indexAccountType = getColumnIndex(\n headers,\n ['Account type']\n );\n const indexAmount = getColumnIndex(\n headers,\n ['Amount']\n );\n const indexInstitution = getColumnIndex(\n headers,\n ['Financial Institution ID']\n );\n const indexParticipantId = getColumnIndex(\n headers,\n ['Participant ID']\n );\n const indexParticipantName = getColumnIndex(\n headers,\n ['Participant Name']\n );\n const indexTransactionType = getColumnIndex(\n headers,\n ['TR Type']\n );\n const indexAddenda = getColumnIndex(\n headers,\n ['Addenda']\n );\n\n const rowsFromFile = [];\n\n for (\n let lineIndex = headerIndex + 1;\n lineIndex < lines.length;\n lineIndex++\n ) {\n const values = parseCsvLine(lines[lineIndex]);\n\n const identifier = normalizeText(\n indexIdentifier >= 0\n ? values[indexIdentifier]\n : ''\n ).toUpperCase();\n\n // T = transacción. C = fila de control/totales.\n if (identifier !== 'T') continue;\n\n const account = normalizeAccount(\n indexAccount >= 0\n ? values[indexAccount]\n : ''\n );\n\n const amount = roundMoney(\n parseMoney(\n indexAmount >= 0\n ? values[indexAmount]\n : ''\n )\n );\n\n const participantName = normalizeText(\n indexParticipantName >= 0\n ? values[indexParticipantName]\n : ''\n );\n\n if (amount <= 0 || !participantName) {\n continue;\n }\n\n const accountIsValid =\n isValidAccount(account);\n\n const groupKey = accountIsValid\n ? `ACCOUNT:${account}:TTD`\n : `ROW:${file.fileName}:${lineIndex + 1}:TTD`;\n\n const row = {\n source_file: file.fileName,\n row_number: lineIndex + 1,\n group_key: groupKey,\n account,\n raw_account: account,\n account_is_valid: accountIsValid,\n bank_name_file: participantName,\n bank_account_holder: '',\n participant_name: participantName,\n participant_id: normalizeText(\n indexParticipantId >= 0\n ? values[indexParticipantId]\n : ''\n ),\n financial_institution_id:\n normalizeText(\n indexInstitution >= 0\n ? values[indexInstitution]\n : ''\n ),\n account_type: normalizeText(\n indexAccountType >= 0\n ? values[indexAccountType]\n : ''\n ),\n transaction_type: normalizeText(\n indexTransactionType >= 0\n ? values[indexTransactionType]\n : ''\n ),\n reference: normalizeText(\n indexAddenda >= 0\n ? values[indexAddenda]\n : ''\n ),\n addenda: normalizeText(\n indexAddenda >= 0\n ? values[indexAddenda]\n : ''\n ),\n shipment_number: '',\n plan_number: '',\n amount,\n currency: 'TTD',\n status: 'Procesado',\n };\n\n rowsFromFile.push(row);\n allBankRows.push(row);\n }\n\n fileSummaries.push({\n file_name: file.fileName,\n ok: true,\n rows_count: rowsFromFile.length,\n total_amount: roundMoney(\n rowsFromFile.reduce(\n (sum, row) => sum + row.amount,\n 0\n )\n ),\n error: null,\n });\n}\n\nconst groupedMap = new Map();\n\nfor (const row of allBankRows) {\n const current =\n groupedMap.get(row.group_key) || {\n group_key: row.group_key,\n account: row.account,\n raw_account: row.raw_account,\n account_is_valid: row.account_is_valid,\n amount: 0,\n currency: 'TTD',\n transactions_count: 0,\n bank_name_files: new Set(),\n bank_account_holders: new Set(),\n source_files: new Set(),\n institution_ids: new Set(),\n source_rows: [],\n };\n\n current.amount = roundMoney(\n current.amount + row.amount\n );\n current.transactions_count += 1;\n\n if (row.bank_name_file) {\n current.bank_name_files.add(\n row.bank_name_file\n );\n }\n\n if (row.source_file) {\n current.source_files.add(row.source_file);\n }\n\n if (row.financial_institution_id) {\n current.institution_ids.add(\n row.financial_institution_id\n );\n }\n\n current.source_rows.push(row);\n groupedMap.set(row.group_key, current);\n}\n\nconst groupedByAccount = Array.from(\n groupedMap.values()\n).map((row) => {\n const names = Array.from(\n row.bank_name_files\n );\n\n return {\n ...row,\n bank_name_file: names[0] || '',\n bank_account_holder: '',\n bank_name_files: names,\n bank_account_holders: [],\n source_files: Array.from(\n row.source_files\n ),\n institution_ids: Array.from(\n row.institution_ids\n ),\n };\n});\n\nconst totalAmount = roundMoney(\n allBankRows.reduce(\n (sum, row) => sum + row.amount,\n 0\n )\n);\n\nreturn [\n {\n json: {\n ...json,\n stage: 'banco_tt_parseado',\n bank: {\n source:\n 'csv_ach_trinidad_tobago',\n files_count: bankKeys.length,\n valid_files_count:\n fileSummaries.filter(\n (file) => file.ok\n ).length,\n rows_count: allBankRows.length,\n grouped_accounts_count:\n groupedByAccount.length,\n total_amount: totalAmount,\n totals_by_currency: {\n TTD: totalAmount,\n },\n name_differences_count: 0,\n name_differences: [],\n file_summaries: fileSummaries,\n rows: allBankRows,\n grouped_by_account:\n groupedByAccount,\n },\n },\n binary,\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3424, - 7040 - ], - "id": "46e46f3c-85f9-40ef-a3cc-ee20acc46d73", - "name": "Parsear CSV banco TT" - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "BICE" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 3424, - 7440 - ], - "id": "444192a4-bd82-4086-a87f-ab116517f723", - "name": "Extract - BICE", - "retryOnFail": false - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "Goldey Samuel" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 3424, - 7616 - ], - "id": "15b0af6e-5e38-4c0c-9d30-496ad9df9413", - "name": "Extract - Goldey Samuel", - "retryOnFail": false - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "P&G" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 3424, - 7776 - ], - "id": "22845609-e9b5-486b-88ce-bc5d73b96a2e", - "name": "Extract - P&G", - "retryOnFail": false - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "Whirlpool" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 3424, - 7952 - ], - "id": "9a39edbc-1e2e-4d75-83ed-9ce48c808abf", - "name": "Extract - Whirlpool", - "retryOnFail": false - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "KAD" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 3424, - 8128 - ], - "id": "1710469a-3b4a-4a3b-9c61-77d7d3c4fffb", - "name": "Extract - KAD", - "retryOnFail": false - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "GLM People" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 3424, - 8288 - ], - "id": "72c876ce-f1e4-4f82-ac7d-7d409eb18e64", - "name": "Extract - GLM People", - "retryOnFail": false - }, - { - "parameters": { - "operation": "xlsx", - "binaryPropertyName": "payroll_file", - "options": { - "headerRow": true, - "sheetName": "GLM" - } - }, - "type": "n8n-nodes-base.extractFromFile", - "typeVersion": 1.1, - "position": [ - 3424, - 8464 - ], - "id": "26d185f0-3f60-44cd-b20e-1fbfbae48fc8", - "name": "Extract - GLM", - "retryOnFail": false - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 5088, - 7536 - ], - "id": "b98e1002-cf47-4cde-94d7-08777b928d36", - "name": "Merge Hojas TT 01-02" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 5088, - 7696 - ], - "id": "c7c11485-0d25-4959-ba19-cf631591472a", - "name": "Merge Hojas TT 03" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 5088, - 7872 - ], - "id": "706b5c48-c1f0-4a12-be90-51abd68f32f3", - "name": "Merge Hojas TT 04" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 5088, - 8032 - ], - "id": "a6cff463-cf7d-4e70-b13d-213dbcefa388", - "name": "Merge Hojas TT 05" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 5088, - 8208 - ], - "id": "25f84f87-c966-4b81-ac10-9021b7eeadc3", - "name": "Merge Hojas TT 06" - }, - { - "parameters": {}, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 5088, - 8384 - ], - "id": "75156223-c7d3-4b98-bdc5-aa62a9185db1", - "name": "Merge Hojas TT 07" - }, - { - "parameters": { - "jsCode": "function normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\u00A0/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeForCompare(value) {\n return normalizeText(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/['’`-]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalizeAccount(value) {\n if (\n value === null ||\n value === undefined ||\n value === ''\n ) {\n return '';\n }\n\n if (typeof value === 'number') {\n return String(Math.trunc(value));\n }\n\n return String(value)\n .replace(/\\u00A0/g, '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim();\n}\n\nfunction parseMoney(value) {\n if (typeof value === 'number') {\n return Number.isFinite(value)\n ? value\n : 0;\n }\n\n const cleaned = String(value ?? '')\n .replace(/TTD/gi, '')\n .replace(/TT\\$/gi, '')\n .replace(/\\$/g, '')\n .replace(/,/g, '')\n .replace(/\\s+/g, '')\n .trim();\n\n const parsed = Number.parseFloat(cleaned);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction roundMoney(value) {\n return Math.round(\n (Number(value) || 0) * 100\n ) / 100;\n}\n\nfunction getValue(row, possibleKeys) {\n for (const key of possibleKeys) {\n const value = row[key];\n\n if (\n value !== undefined &&\n value !== null &&\n value !== ''\n ) {\n return value;\n }\n }\n\n const rowKeys = Object.keys(row || {});\n\n for (const expected of possibleKeys) {\n const normalizedExpected =\n normalizeForCompare(expected);\n\n const matchingKey = rowKeys.find(\n (key) =>\n normalizeForCompare(key) ===\n normalizedExpected\n );\n\n if (!matchingKey) continue;\n\n const value = row[matchingKey];\n\n if (\n value !== undefined &&\n value !== null &&\n value !== ''\n ) {\n return value;\n }\n }\n\n return '';\n}\n\nfunction getNodeRows(nodeName) {\n try {\n return $items(nodeName)\n .map((item) => item.json || {})\n .filter((row) => {\n if (row.error) return false;\n\n const text = JSON.stringify(\n row || {}\n ).toLowerCase();\n\n return !(\n text.includes(\n 'spreadsheet does not contain sheet'\n ) ||\n text.includes('no sheet')\n );\n });\n } catch (error) {\n return [];\n }\n}\n\nfunction validEmployeeName(value) {\n const name = normalizeText(value);\n const normalized = normalizeForCompare(name);\n\n if (!name) return false;\n if (/^[\\d.,\\s]+$/.test(name)) return false;\n\n const invalid = [\n 'total',\n 'subtotal',\n 'gran total',\n 'total general',\n 'variable',\n 'empleado',\n 'first name',\n 'nombre',\n 'diferencia',\n 'total dias',\n ];\n\n return !invalid.some(\n (token) =>\n normalized === token ||\n normalized.startsWith(`${token} `)\n );\n}\n\nfunction validAccount(value) {\n const account = normalizeAccount(value);\n\n return (\n account.length >= 6 &&\n !/^0+$/.test(account)\n );\n}\n\nconst sheetConfigs = [\n {\n node: 'Extract - BICE',\n sheet: 'BICE',\n },\n {\n node: 'Extract - Goldey Samuel',\n sheet: 'Goldey Samuel',\n },\n {\n node: 'Extract - P&G',\n sheet: 'P&G',\n },\n {\n node: 'Extract - Whirlpool',\n sheet: 'Whirlpool',\n },\n {\n node: 'Extract - KAD',\n sheet: 'KAD',\n },\n {\n node: 'Extract - GLM People',\n sheet: 'GLM People',\n },\n {\n node: 'Extract - GLM',\n sheet: 'GLM',\n },\n];\n\nconst payrollRows = [];\nconst noAccountRows = [];\nconst ignoredRows = [];\nconst sheetSummaries = [];\n\nfor (const config of sheetConfigs) {\n const sourceRows = getNodeRows(\n config.node\n );\n\n let validRows = 0;\n let noAccountCount = 0;\n let ignoredCount = 0;\n let sheetTotal = 0;\n\n sourceRows.forEach((sourceRow, index) => {\n const period = normalizeText(\n getValue(sourceRow, ['Periodo'])\n );\n\n const employeeName = normalizeText(\n getValue(sourceRow, [\n 'First Name',\n 'Nombre completo',\n 'Empleado',\n 'Name',\n ])\n );\n\n const account = normalizeAccount(\n getValue(sourceRow, [\n 'Account #',\n 'Account Number',\n 'Cuenta bancaria',\n 'Cuenta Bancaria',\n ])\n );\n\n const email = normalizeText(\n getValue(sourceRow, [\n 'EMAIL',\n 'Email',\n 'Correo',\n ])\n ).toLowerCase();\n\n const amount = roundMoney(\n parseMoney(\n getValue(sourceRow, [\n 'NETO A PAGAR',\n 'Neto a Pagar',\n 'Net Pay',\n ])\n )\n );\n\n const client = normalizeText(\n getValue(sourceRow, ['Cuenta'])\n );\n\n const rowNumber = index + 2;\n\n const normalized = {\n source_sheet: config.sheet,\n row_number: rowNumber,\n period,\n employee_name: employeeName,\n employee_number: null,\n account,\n email,\n client,\n payroll_amount: amount,\n currency: 'TTD',\n };\n\n if (\n !period ||\n !validEmployeeName(employeeName) ||\n amount <= 0 ||\n amount > 500000\n ) {\n ignoredRows.push({\n ...normalized,\n reason:\n !period\n ? 'period_empty'\n : !validEmployeeName(employeeName)\n ? 'invalid_employee_name'\n : amount <= 0\n ? 'amount_zero_or_invalid'\n : 'suspicious_large_amount',\n });\n\n ignoredCount += 1;\n return;\n }\n\n sheetTotal = roundMoney(\n sheetTotal + amount\n );\n\n if (!validAccount(account)) {\n noAccountRows.push({\n ...normalized,\n account: '',\n });\n\n noAccountCount += 1;\n return;\n }\n\n payrollRows.push(normalized);\n validRows += 1;\n });\n\n sheetSummaries.push({\n sheet: config.sheet,\n node: config.node,\n raw_rows_count: sourceRows.length,\n valid_rows_count: validRows,\n no_account_rows_count:\n noAccountCount,\n ignored_rows_count: ignoredCount,\n total_amount: sheetTotal,\n });\n}\n\nconst groupedMap = new Map();\n\nfor (const row of payrollRows) {\n const groupKey =\n `${row.account}:${row.currency}`;\n\n const current =\n groupedMap.get(groupKey) || {\n group_key: groupKey,\n account: row.account,\n employee_name: row.employee_name,\n employee_number: null,\n email: row.email,\n currency: 'TTD',\n payroll_amount: 0,\n rows_count: 0,\n source_sheets: new Set(),\n source_rows: [],\n };\n\n current.payroll_amount = roundMoney(\n current.payroll_amount +\n row.payroll_amount\n );\n\n current.rows_count += 1;\n\n if (!current.email && row.email) {\n current.email = row.email;\n }\n\n current.source_sheets.add(\n row.source_sheet\n );\n\n current.source_rows.push({\n source_sheet: row.source_sheet,\n row_number: row.row_number,\n account: row.account,\n amount: row.payroll_amount,\n employee_name: row.employee_name,\n });\n\n groupedMap.set(groupKey, current);\n}\n\nconst groupedByAccount = Array.from(\n groupedMap.values()\n).map((row) => ({\n ...row,\n source_sheets: Array.from(\n row.source_sheets\n ),\n}));\n\nconst totalAmount = roundMoney(\n payrollRows.reduce(\n (sum, row) => sum + row.payroll_amount,\n 0\n ) +\n noAccountRows.reduce(\n (sum, row) => sum + row.payroll_amount,\n 0\n )\n);\n\nreturn [\n {\n json: {\n payroll: {\n source:\n 'template_trinidad_tobago',\n sheets_count:\n sheetConfigs.length,\n sheet_summaries:\n sheetSummaries,\n raw_rows_count:\n sheetSummaries.reduce(\n (sum, sheet) =>\n sum + sheet.raw_rows_count,\n 0\n ),\n valid_rows_count:\n payrollRows.length,\n no_account_rows_count:\n noAccountRows.length,\n ignored_rows_count:\n ignoredRows.length,\n grouped_accounts_count:\n groupedByAccount.length,\n attached_supplements_count: 0,\n potential_supplements_count: 0,\n potential_supplements: [],\n unattached_supplements_count: 0,\n total_amount: totalAmount,\n totals_by_currency: {\n TTD: totalAmount,\n },\n rows: payrollRows,\n no_account_rows:\n noAccountRows,\n grouped_by_account:\n groupedByAccount,\n },\n debug_payroll: {\n attached_supplements: [],\n potential_supplements: [],\n unattached_supplements: [],\n ignored_rows_preview:\n ignoredRows.slice(0, 100),\n no_account_rows_preview:\n noAccountRows.slice(0, 50),\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5456, - 7952 - ], - "id": "d036e80f-bf9c-4024-9bdb-f9222d8ee057", - "name": "Normalizar Nómina TT" - }, - { - "parameters": { - "mode": "combine", - "combineBy": "combineByPosition", - "options": {} - }, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 5712, - 7296 - ], - "id": "8d2116b6-d720-47a0-b532-aec6f16966c1", - "name": "Merge Banco + Nómina TT" - }, - { - "parameters": { - "method": "POST", - "url": "https://glm.bamboohr.com/api/v1/reports/custom?format=JSON&onlyCurrent=false", - "authentication": "genericCredentialType", - "genericAuthType": "httpBasicAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Accept", - "value": "application/json" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": { - "title": "Información de BambooHR - Cruce de Cuentas TT", - "fields": [ - "firstName", - "middleName", - "lastName", - "displayName", - "department", - "division", - "location", - "customPosicion-Cliente", - "hireDate", - "originalHireDate", - "status", - "employeeNumber" - ] - }, - "options": { - "response": { - "response": { - "responseFormat": "json" - } - }, - "timeout": 300000 - } - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 3424, - 6816 - ], - "id": "84b74d83-a969-4f62-a11b-ace145d64e8c", - "name": "HTTP - Empleados BambooHR TT", - "retryOnFail": true, - "maxTries": 3, - "waitBetweenTries": 3000, - "credentials": { - "httpBasicAuth": { - "id": "7VrpNZ2jBLmiJ35q", - "name": "BambooHR GLM Full Access" - } - } - }, - { - "parameters": { - "jsCode": "const inputItems = $input.all();\nconst base = $('Preparar entrada app').first().json || {};\nconst reconciliationData = $('Merge Banco + Nómina TT').first().json || {};\nconst metadata = base.metadata || {};\n\nfunction clean(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\u00A0/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction normalize(value) {\n return clean(value)\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/['’`-]/g, ' ')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction unique(values) {\n return Array.from(\n new Set(\n values\n .map(clean)\n .filter(Boolean)\n )\n );\n}\n\nfunction nameTokens(value) {\n const ignored = new Set([\n 'de', 'del', 'la', 'las', 'los',\n 'y', 'e', 'el', 'da', 'do',\n 'dos', 'das', 'van', 'von',\n ]);\n\n return normalize(value)\n .split(' ')\n .filter(\n (token) =>\n token.length > 1 &&\n !ignored.has(token)\n );\n}\n\nfunction parseDate(value) {\n const raw = clean(value);\n if (!raw || raw === '0000-00-00') return null;\n\n const direct = raw.match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n if (direct) {\n return `${direct[1]}-${direct[2]}-${direct[3]}`;\n }\n\n const date = new Date(raw);\n if (Number.isNaN(date.getTime())) return null;\n\n return date.toISOString().slice(0, 10);\n}\n\nfunction parseBoolean(value) {\n if (typeof value === 'boolean') return value;\n\n return [\n 'true', 'yes', 'si', 'sí', '1', 'y',\n ].includes(normalize(value));\n}\n\nfunction isTargetCountry(employee) {\n const country = normalize(employee.country);\n const location = normalize(\n employee.location ||\n employee.jobInformationLocation ||\n employee.jobLocation\n );\n\n return (\n country === 'tt' ||\n country === 'tto' ||\n country.includes('trinidad') ||\n country.includes('tobago') ||\n location === 'tt' ||\n location === 'tto' ||\n location.includes('trinidad') ||\n location.includes('tobago')\n );\n}\n\nfunction overlapsPeriod(\n hireDate,\n terminationDate,\n periodStart,\n periodEnd\n) {\n if (!periodStart || !periodEnd) return false;\n\n const hiredBeforeEnd =\n !hireDate || hireDate <= periodEnd;\n\n const notTerminatedBeforeStart =\n !terminationDate ||\n terminationDate >= periodStart;\n\n return hiredBeforeEnd && notTerminatedBeforeStart;\n}\n\nfunction collectPageObjects(value, pages) {\n if (!value) return;\n\n if (Array.isArray(value)) {\n for (const entry of value) {\n collectPageObjects(entry, pages);\n }\n return;\n }\n\n if (typeof value !== 'object') return;\n\n if (value.body && typeof value.body === 'object') {\n collectPageObjects(value.body, pages);\n return;\n }\n\n if (\n Array.isArray(value.data) ||\n Array.isArray(value.employees)\n ) {\n pages.push(value);\n return;\n }\n\n if (value.json && typeof value.json === 'object') {\n collectPageObjects(value.json, pages);\n }\n}\n\nfunction strictInformativeContainment(\n left,\n right\n) {\n const leftTokens =\n Array.from(new Set(nameTokens(left)));\n const rightTokens =\n Array.from(new Set(nameTokens(right)));\n\n if (\n leftTokens.length < 3 ||\n rightTokens.length < 3\n ) {\n return false;\n }\n\n const leftSet = new Set(leftTokens);\n const rightSet = new Set(rightTokens);\n\n const leftInsideRight =\n leftTokens.every((token) =>\n rightSet.has(token)\n );\n\n const rightInsideLeft =\n rightTokens.every((token) =>\n leftSet.has(token)\n );\n\n return leftInsideRight || rightInsideLeft;\n}\n\nconst pageObjects = [];\n\nfor (const item of inputItems) {\n collectPageObjects(item.json, pageObjects);\n}\n\nconst employeeMap = new Map();\nlet expectedTotal = 0;\nlet restrictedFields = 0;\n\nfor (const page of pageObjects) {\n const pageEmployees =\n Array.isArray(page.data)\n ? page.data\n : Array.isArray(page.employees)\n ? page.employees\n : [];\n\n const pageTotal = Number(\n page.meta?.total ||\n page.total ||\n 0\n );\n\n if (Number.isFinite(pageTotal)) {\n expectedTotal = Math.max(\n expectedTotal,\n pageTotal\n );\n }\n\n for (const employee of pageEmployees) {\n const key =\n clean(employee.employeeId || employee.id) ||\n clean(employee.employeeNumber) ||\n clean(employee.bestEmail).toLowerCase() ||\n [\n clean(employee.firstName),\n clean(employee.middleName),\n clean(employee.lastName),\n ].filter(Boolean).join('|').toLowerCase();\n\n if (!key) continue;\n\n employeeMap.set(key, employee);\n\n restrictedFields += Array.isArray(\n employee._restrictedFields\n )\n ? employee._restrictedFields.length\n : 0;\n }\n}\n\nconst rawEmployees = Array.from(\n employeeMap.values()\n);\n\nconst periodStart = clean(metadata.period_start);\nconst periodEnd = clean(metadata.period_end);\n\nconst allNormalized = rawEmployees.map((employee) => {\n const firstName = clean(employee.firstName);\n const middleName = clean(employee.middleName);\n const lastName = clean(employee.lastName);\n const preferredName = clean(\n employee.preferredName\n );\n\n const constructedFullName = [\n firstName,\n middleName,\n lastName,\n ].filter(Boolean).join(' ');\n\n const aliases = unique([\n employee.displayName,\n employee.fullName1,\n employee.fullName2,\n employee.fullName3,\n employee.fullName4,\n employee.fullName5,\n constructedFullName,\n [preferredName, lastName]\n .filter(Boolean)\n .join(' '),\n [firstName, lastName]\n .filter(Boolean)\n .join(' '),\n ]);\n\n const hireDate = parseDate(\n employee.hireDate ||\n employee.originalHireDate\n );\n\n const terminationDate = parseDate(\n employee.terminationDate\n );\n\n const status = clean(\n employee.status ||\n employee.employmentStatus ||\n employee.employmentHistoryStatus\n );\n\n const employeeNumber = clean(\n employee.employeeNumber ||\n employee.employee_number\n );\n\n return {\n bamboo_id: clean(\n employee.employeeId ||\n employee.id\n ),\n employee_number: employeeNumber,\n first_name: firstName,\n middle_name: middleName,\n last_name: lastName,\n preferred_name: preferredName,\n full_name:\n clean(employee.displayName) ||\n clean(employee.fullName1) ||\n constructedFullName,\n aliases,\n normalized_aliases:\n aliases.map(normalize).filter(Boolean),\n status,\n hire_date: hireDate,\n termination_date: terminationDate,\n location: clean(\n employee.location ||\n employee.jobInformationLocation ||\n employee.jobLocation\n ),\n country: clean(employee.country),\n include_in_payroll:\n parseBoolean(employee.includeInPayroll),\n work_email:\n clean(employee.workEmail).toLowerCase(),\n home_email:\n clean(employee.homeEmail).toLowerCase(),\n best_email: clean(\n employee.bestEmail ||\n employee.workEmail ||\n employee.homeEmail\n ).toLowerCase(),\n exists_in_bamboo: true,\n overlaps_period: overlapsPeriod(\n hireDate,\n terminationDate,\n periodStart,\n periodEnd\n ),\n };\n});\n\nconst relevantNameMap = new Map();\n\nfunction addRelevantName(value) {\n const cleaned = clean(value);\n const normalized = normalize(cleaned);\n\n if (!normalized) return;\n\n const current =\n relevantNameMap.get(normalized);\n\n if (\n !current ||\n nameTokens(cleaned).length >\n nameTokens(current).length\n ) {\n relevantNameMap.set(\n normalized,\n cleaned\n );\n }\n}\n\nfor (const row of reconciliationData.bank?.rows || []) {\n addRelevantName(row.bank_name_file);\n addRelevantName(row.bank_account_holder);\n addRelevantName(row.participant_name);\n}\n\nfor (\n const row of\n reconciliationData.bank?.grouped_by_account || []\n) {\n addRelevantName(row.bank_name_file);\n addRelevantName(row.bank_account_holder);\n\n for (const name of row.bank_name_files || []) {\n addRelevantName(name);\n }\n\n for (\n const name of\n row.bank_account_holders || []\n ) {\n addRelevantName(name);\n }\n}\n\nfor (const row of [\n ...(reconciliationData.payroll?.rows || []),\n ...(reconciliationData.payroll?.grouped_by_account || []),\n ...(reconciliationData.payroll?.no_account_rows || []),\n]) {\n addRelevantName(\n row.employee_name ||\n row.employee ||\n ''\n );\n}\n\nconst targetEmployees =\n allNormalized\n .filter(isTargetCountry)\n .map((employee) => ({\n ...employee,\n validation_eligible: true,\n validation_scope:\n 'trinidad_tobago_country_or_location',\n }));\n\nconst outsideEmployees =\n allNormalized.filter(\n (employee) =>\n !isTargetCountry(employee)\n );\n\nconst contextualOutsideMap = new Map();\n\nfor (\n const relevantName of\n relevantNameMap.values()\n) {\n if (\n nameTokens(relevantName).length < 3\n ) {\n continue;\n }\n\n const matches = outsideEmployees\n .filter((employee) =>\n (employee.aliases || []).some(\n (alias) =>\n strictInformativeContainment(\n relevantName,\n alias\n )\n )\n );\n\n const uniqueMatches = new Map();\n\n for (const employee of matches) {\n const key =\n employee.bamboo_id ||\n employee.employee_number ||\n normalize(employee.full_name);\n\n if (key) {\n uniqueMatches.set(key, employee);\n }\n }\n\n // Solo se rescata un perfil fuera del país cuando un nombre\n // informativo identifica exactamente a una única persona.\n if (uniqueMatches.size !== 1) {\n continue;\n }\n\n const employee =\n uniqueMatches.values().next().value;\n\n const key =\n employee.bamboo_id ||\n employee.employee_number ||\n normalize(employee.full_name);\n\n contextualOutsideMap.set(key, {\n ...employee,\n validation_eligible: true,\n validation_scope:\n 'outside_country_unique_informative_name',\n });\n}\n\nconst validationEmployeeMap = new Map();\n\nfor (const employee of [\n ...targetEmployees,\n ...contextualOutsideMap.values(),\n]) {\n const key =\n employee.bamboo_id ||\n employee.employee_number ||\n normalize(employee.full_name);\n\n if (key) {\n validationEmployeeMap.set(\n key,\n employee\n );\n }\n}\n\nconst validationEmployees =\n Array.from(\n validationEmployeeMap.values()\n );\n\n\n/*\n * Resolución previa de nombres contra BambooHR.\n *\n * Cada nombre distinto recibido desde banco y nómina se resuelve una sola\n * vez, usando índices de alias y palabras. El resultado queda disponible\n * para el nodo de cruce mediante resolved_name_matches.\n */\nconst CONFIRMED_BAMBOO_NAME_ALIASES = new Map([\n [normalize(\"ISAAC ST BERNARD\"), normalize(\"Isaac St Bernard\")],\n [normalize(\"VICTORIA ALPHONSO\"), normalize(\"Victoria Alphanso\")],\n [normalize(\"VICTORIA ALPHANSO\"), normalize(\"Victoria Alphanso\")],\n [normalize(\"ONELA FARREL\"), normalize(\"Onela Farrell\")],\n [normalize(\"ONELA FARRELL\"), normalize(\"Onela Farrell\")],\n [normalize(\"JESHAUGHN LOUIS\"), normalize(\"Je'Shaugn Louis\")],\n [normalize(\"JESHAUGN LOUIS\"), normalize(\"Je'Shaugn Louis\")],\n [normalize(\"JE SHAUGN LOUIS\"), normalize(\"Je'Shaugn Louis\")],\n [normalize(\"ANESSA ALI\"), normalize(\"Annesa Marina Ali\")],\n [normalize(\"ANNESA ALI\"), normalize(\"Annesa Marina Ali\")],\n [normalize(\"ANNESA MARINA ALI\"), normalize(\"Annesa Marina Ali\")],\n [normalize(\"ALANA KERCELUS\"), normalize(\"Alana Kercelus-Inalsingh\")],\n [normalize(\"ALANA KERCELUS INALSINGH\"), normalize(\"Alana Kercelus-Inalsingh\")]\n]);\n\nfunction relevantEntryRaw(entry) {\n if (typeof entry === 'string') return clean(entry);\n return clean(entry?.raw || entry?.name || '');\n}\n\nfunction bambooResolutionEmployeeKey(employee) {\n return (\n clean(employee.bamboo_id) ||\n clean(employee.employee_number) ||\n normalize(employee.full_name)\n );\n}\n\nfunction bambooResolutionEditDistance(left, right) {\n const a = String(left || '');\n const b = String(right || '');\n\n if (a === b) return 0;\n if (!a) return b.length;\n if (!b) return a.length;\n\n let previous = Array.from(\n { length: b.length + 1 },\n (_, index) => index\n );\n\n for (let row = 1; row <= a.length; row++) {\n const current = [row];\n\n for (let column = 1; column <= b.length; column++) {\n const cost =\n a[row - 1] === b[column - 1]\n ? 0\n : 1;\n\n current[column] = Math.min(\n current[column - 1] + 1,\n previous[column] + 1,\n previous[column - 1] + cost\n );\n }\n\n previous = current;\n }\n\n return previous[b.length];\n}\nfunction bambooResolutionTokenSimilarity(left, right) {\n const a = String(left || '');\n const b = String(right || '');\n\n if (!a || !b) return 0;\n if (a === b) return 1;\n\n const minimumLength = Math.min(\n a.length,\n b.length\n );\n\n const maximumLength = Math.max(\n a.length,\n b.length\n );\n\n const distance =\n bambooResolutionEditDistance(a, b);\n\n if (\n minimumLength >= 4 &&\n distance <= 1\n ) {\n return Math.max(\n 0.90,\n 1 - distance / maximumLength\n );\n }\n\n if (\n minimumLength >= 6 &&\n distance <= 2\n ) {\n return Math.max(\n 0.82,\n 1 - distance / maximumLength\n );\n }\n\n const prefixOrSuffix =\n a.startsWith(b) ||\n b.startsWith(a) ||\n a.endsWith(b) ||\n b.endsWith(a);\n\n if (\n prefixOrSuffix &&\n minimumLength >= 4\n ) {\n return Math.max(\n 0.78,\n minimumLength / maximumLength\n );\n }\n\n return 0;\n}\n\nfunction bambooResolutionAliasDetails(\n queryName,\n aliasProfile\n) {\n const queryWords = Array.from(\n new Set(nameTokens(queryName))\n );\n\n const aliasWords =\n aliasProfile.words;\n\n if (\n queryWords.length < 2 ||\n aliasWords.length < 2\n ) {\n return null;\n }\n\n const aliasWordSet =\n aliasProfile.word_set;\n\n const queryWordSet =\n new Set(queryWords);\n\n const queryInsideAlias =\n queryWords.every((word) =>\n aliasWordSet.has(word)\n );\n\n const aliasInsideQuery =\n aliasWords.every((word) =>\n queryWordSet.has(word)\n );\n\n const usedAliasIndexes = new Set();\n const usedQueryIndexes = new Set();\n const similarities = new Array(\n queryWords.length\n ).fill(0);\n\n let exactMatches = 0;\n\n for (\n let queryIndex = 0;\n queryIndex < queryWords.length;\n queryIndex++\n ) {\n const aliasIndex =\n aliasWords.findIndex(\n (aliasWord, currentAliasIndex) =>\n !usedAliasIndexes.has(\n currentAliasIndex\n ) &&\n aliasWord ===\n queryWords[queryIndex]\n );\n\n if (aliasIndex < 0) continue;\n\n usedQueryIndexes.add(queryIndex);\n usedAliasIndexes.add(aliasIndex);\n similarities[queryIndex] = 1;\n exactMatches += 1;\n }\n\n const remainingQueryIndexes =\n queryWords\n .map((word, index) => ({\n word,\n index,\n }))\n .filter((entry) =>\n !usedQueryIndexes.has(entry.index)\n )\n .sort((left, right) =>\n right.word.length -\n left.word.length\n );\n\n for (const queryEntry of remainingQueryIndexes) {\n let bestSimilarity = 0;\n let bestAliasIndex = -1;\n\n for (\n let aliasIndex = 0;\n aliasIndex < aliasWords.length;\n aliasIndex++\n ) {\n if (\n usedAliasIndexes.has(\n aliasIndex\n )\n ) {\n continue;\n }\n\n const similarity =\n bambooResolutionTokenSimilarity(\n queryEntry.word,\n aliasWords[aliasIndex]\n );\n\n if (similarity > bestSimilarity) {\n bestSimilarity = similarity;\n bestAliasIndex = aliasIndex;\n }\n }\n\n if (\n bestAliasIndex >= 0 &&\n bestSimilarity >= 0.78\n ) {\n usedAliasIndexes.add(\n bestAliasIndex\n );\n similarities[queryEntry.index] =\n bestSimilarity;\n }\n }\n\n const matchedTokens =\n similarities.filter(\n (value) => value >= 0.78\n ).length;\n\n const queryCoverage =\n similarities.reduce(\n (sum, value) => sum + value,\n 0\n ) / queryWords.length;\n\n const aliasCoverage =\n matchedTokens /\n aliasWords.length;\n\n const lengthBalance =\n Math.min(\n queryWords.length,\n aliasWords.length\n ) /\n Math.max(\n queryWords.length,\n aliasWords.length\n );\n\n const score =\n queryCoverage * 0.65 +\n aliasCoverage * 0.20 +\n (\n exactMatches /\n queryWords.length\n ) * 0.10 +\n lengthBalance * 0.05;\n\n return {\n score,\n exact_matches: exactMatches,\n matched_tokens: matchedTokens,\n query_tokens:\n queryWords.length,\n alias_tokens:\n aliasWords.length,\n query_coverage:\n queryCoverage,\n alias_coverage:\n aliasCoverage,\n containment:\n queryInsideAlias ||\n aliasInsideQuery,\n };\n}\n\nconst bambooResolutionProfiles =\n validationEmployees.map(\n (employee, employeeIndex) => {\n const aliases = [];\n const seenAliases = new Set();\n\n for (\n const rawAlias of\n employee.aliases || []\n ) {\n const normalizedAlias =\n normalize(rawAlias);\n\n if (\n !normalizedAlias ||\n seenAliases.has(\n normalizedAlias\n )\n ) {\n continue;\n }\n\n seenAliases.add(\n normalizedAlias\n );\n\n const words = Array.from(\n new Set(nameTokens(rawAlias))\n );\n\n if (!words.length) continue;\n\n aliases.push({\n raw: clean(rawAlias),\n normalized:\n normalizedAlias,\n words,\n word_set:\n new Set(words),\n });\n }\n\n return {\n employee,\n employee_index:\n employeeIndex,\n employee_key:\n bambooResolutionEmployeeKey(\n employee\n ),\n aliases,\n };\n }\n );\n\nconst bambooResolutionExactAliasSets =\n new Map();\n\nconst bambooResolutionTokenSets =\n new Map();\n\nconst bambooResolutionTokenShapeSets =\n new Map();\n\nfor (\n let employeeIndex = 0;\n employeeIndex <\n bambooResolutionProfiles.length;\n employeeIndex++\n) {\n const profile =\n bambooResolutionProfiles[\n employeeIndex\n ];\n\n for (const alias of profile.aliases) {\n let exactSet =\n bambooResolutionExactAliasSets\n .get(alias.normalized);\n\n if (!exactSet) {\n exactSet = new Set();\n bambooResolutionExactAliasSets\n .set(\n alias.normalized,\n exactSet\n );\n }\n\n exactSet.add(employeeIndex);\n\n for (const token of alias.words) {\n if (token.length < 3) continue;\n\n let tokenSet =\n bambooResolutionTokenSets\n .get(token);\n\n if (!tokenSet) {\n tokenSet = new Set();\n bambooResolutionTokenSets\n .set(token, tokenSet);\n }\n\n tokenSet.add(employeeIndex);\n\n const tokenShape =\n `${token[0]}:${token.length}`;\n\n let shapeSet =\n bambooResolutionTokenShapeSets\n .get(tokenShape);\n\n if (!shapeSet) {\n shapeSet = new Set();\n bambooResolutionTokenShapeSets\n .set(\n tokenShape,\n shapeSet\n );\n }\n\n shapeSet.add(employeeIndex);\n }\n }\n}\n\nconst bambooResolutionExactAliasMap =\n new Map();\n\nfor (\n const [alias, indexes] of\n bambooResolutionExactAliasSets\n) {\n bambooResolutionExactAliasMap.set(\n alias,\n Array.from(indexes)\n );\n}\n\nfunction bambooResolutionDecision(\n queryName\n) {\n const rawQuery = clean(queryName);\n const normalizedQuery =\n normalize(rawQuery);\n\n const queryWords = Array.from(\n new Set(nameTokens(rawQuery))\n );\n\n if (\n !normalizedQuery ||\n queryWords.length < 2\n ) {\n return {\n found: false,\n matched_by: null,\n confidence: 0,\n reason:\n 'insufficient_name_tokens',\n };\n }\n\n const confirmedCanonical =\n CONFIRMED_BAMBOO_NAME_ALIASES\n .get(normalizedQuery);\n\n if (confirmedCanonical) {\n const confirmedIndexes =\n bambooResolutionExactAliasMap\n .get(confirmedCanonical) || [];\n\n if (confirmedIndexes.length === 1) {\n const employeeIndex =\n confirmedIndexes[0];\n\n return {\n found: true,\n matched_by:\n 'confirmed_alias_catalog',\n confidence: 1,\n employee_index:\n employeeIndex,\n employee_key:\n bambooResolutionProfiles[\n employeeIndex\n ].employee_key,\n query_name:\n rawQuery,\n bamboo_alias:\n bambooResolutionProfiles[\n employeeIndex\n ].aliases.find(\n (alias) =>\n alias.normalized ===\n confirmedCanonical\n )?.raw ||\n bambooResolutionProfiles[\n employeeIndex\n ].employee.full_name ||\n '',\n };\n }\n }\n\n const exactIndexes =\n bambooResolutionExactAliasMap\n .get(normalizedQuery) || [];\n\n if (exactIndexes.length === 1) {\n const employeeIndex =\n exactIndexes[0];\n\n return {\n found: true,\n matched_by:\n 'exact_precomputed_name',\n confidence: 1,\n employee_index:\n employeeIndex,\n employee_key:\n bambooResolutionProfiles[\n employeeIndex\n ].employee_key,\n query_name:\n rawQuery,\n bamboo_alias:\n bambooResolutionProfiles[\n employeeIndex\n ].aliases.find(\n (alias) =>\n alias.normalized ===\n normalizedQuery\n )?.raw ||\n bambooResolutionProfiles[\n employeeIndex\n ].employee.full_name ||\n '',\n };\n }\n\n const candidateVotes = new Map();\n\n function addCandidateVotes(\n indexes,\n weight\n ) {\n for (const employeeIndex of indexes) {\n candidateVotes.set(\n employeeIndex,\n (\n candidateVotes.get(\n employeeIndex\n ) || 0\n ) + weight\n );\n }\n }\n\n for (const token of queryWords) {\n addCandidateVotes(\n bambooResolutionTokenSets\n .get(token) || [],\n 4\n );\n\n for (\n let lengthOffset = -2;\n lengthOffset <= 2;\n lengthOffset++\n ) {\n const candidateLength =\n token.length + lengthOffset;\n\n if (candidateLength < 3) {\n continue;\n }\n\n addCandidateVotes(\n bambooResolutionTokenShapeSets\n .get(\n `${token[0]}:${candidateLength}`\n ) || [],\n 1\n );\n }\n }\n\n const candidateIndexes =\n Array.from(\n candidateVotes.entries()\n )\n .sort((left, right) =>\n right[1] - left[1]\n )\n .slice(0, 120)\n .map(([employeeIndex]) =>\n employeeIndex\n );\n\n const rankedCandidates = [];\n\n for (\n const employeeIndex of\n candidateIndexes\n ) {\n const profile =\n bambooResolutionProfiles[\n employeeIndex\n ];\n\n let bestDetails = null;\n let bestAlias = '';\n\n for (const alias of profile.aliases) {\n const details =\n bambooResolutionAliasDetails(\n rawQuery,\n alias\n );\n\n if (\n details &&\n (\n !bestDetails ||\n details.score >\n bestDetails.score\n )\n ) {\n bestDetails = details;\n bestAlias = alias.raw;\n }\n }\n\n if (!bestDetails) continue;\n\n rankedCandidates.push({\n employee_index:\n employeeIndex,\n employee_key:\n profile.employee_key,\n details:\n bestDetails,\n bamboo_alias:\n bestAlias,\n });\n }\n\n rankedCandidates.sort(\n (left, right) => {\n if (\n right.details.score !==\n left.details.score\n ) {\n return (\n right.details.score -\n left.details.score\n );\n }\n\n if (\n right.details.exact_matches !==\n left.details.exact_matches\n ) {\n return (\n right.details.exact_matches -\n left.details.exact_matches\n );\n }\n\n return (\n right.details.query_coverage -\n left.details.query_coverage\n );\n }\n );\n\n const best =\n rankedCandidates[0] || null;\n\n const second =\n rankedCandidates[1] || null;\n\n const margin =\n best\n ? best.details.score -\n (\n second?.details.score ||\n 0\n )\n : 0;\n\n const details =\n best?.details || null;\n\n const exactContainment =\n Boolean(\n details?.containment &&\n details.exact_matches >= 2\n );\n\n const strongTwoTokenName =\n Boolean(\n details &&\n details.query_tokens === 2 &&\n details.matched_tokens === 2 &&\n details.exact_matches >= 1 &&\n details.query_coverage >= 0.90 &&\n details.score >= 0.88\n );\n\n const strongLongName =\n Boolean(\n details &&\n details.query_tokens >= 3 &&\n details.matched_tokens >=\n Math.min(\n 3,\n details.query_tokens\n ) &&\n details.exact_matches >= 2 &&\n details.query_coverage >= 0.85 &&\n details.score >= 0.84\n );\n\n const acceptableMargin =\n !second ||\n margin >= (\n exactContainment\n ? 0.04\n : 0.06\n ) ||\n (\n details?.exact_matches || 0\n ) >\n (\n second?.details\n ?.exact_matches || 0\n );\n\n if (\n best &&\n acceptableMargin &&\n (\n exactContainment ||\n strongTwoTokenName ||\n strongLongName\n )\n ) {\n return {\n found: true,\n matched_by:\n exactContainment\n ? 'unique_precomputed_containment'\n : 'strong_precomputed_fuzzy_name',\n confidence:\n Math.min(\n 1,\n details.score\n ),\n employee_index:\n best.employee_index,\n employee_key:\n best.employee_key,\n query_name:\n rawQuery,\n bamboo_alias:\n best.bamboo_alias,\n margin,\n exact_matches:\n details.exact_matches,\n matched_tokens:\n details.matched_tokens,\n };\n }\n\n return {\n found: false,\n matched_by: null,\n confidence:\n details?.score || 0,\n reason:\n best\n ? (\n acceptableMargin\n ? 'insufficient_name_evidence'\n : 'ambiguous_name'\n )\n : 'no_candidate',\n best_candidate:\n best\n ? {\n employee_index:\n best.employee_index,\n employee_key:\n best.employee_key,\n bamboo_alias:\n best.bamboo_alias,\n score:\n best.details.score,\n }\n : null,\n second_candidate:\n second\n ? {\n employee_index:\n second.employee_index,\n employee_key:\n second.employee_key,\n bamboo_alias:\n second.bamboo_alias,\n score:\n second.details.score,\n }\n : null,\n };\n}\n\nconst resolvedNameMatches = {};\nlet resolvedNameMatchesFound = 0;\n\nfor (\n const [\n normalizedRelevantName,\n relevantEntry,\n ] of relevantNameMap\n) {\n const rawRelevantName =\n relevantEntryRaw(relevantEntry);\n\n const decision =\n bambooResolutionDecision(\n rawRelevantName\n );\n\n resolvedNameMatches[\n normalizedRelevantName\n ] = decision;\n\n if (decision.found) {\n resolvedNameMatchesFound += 1;\n }\n}\n\n\nconst fetchedEmployeesCount =\n rawEmployees.length;\n\nconst fetchComplete =\n expectedTotal > 0\n ? fetchedEmployeesCount >= expectedTotal\n : (\n pageObjects.length > 0 &&\n !pageObjects.some(\n (page) =>\n Boolean(\n page?._links?.next?.href\n )\n )\n );\n\nconst errors = [];\n\nif (!pageObjects.length) {\n errors.push(\n 'BambooHR no devolvió páginas de empleados.'\n );\n}\n\nif (!fetchedEmployeesCount) {\n errors.push(\n 'BambooHR no devolvió empleados.'\n );\n}\n\nif (\n expectedTotal > 0 &&\n fetchedEmployeesCount < expectedTotal\n) {\n errors.push(\n `La descarga de BambooHR quedó incompleta: ` +\n `${fetchedEmployeesCount} de ${expectedTotal} empleados.`\n );\n}\n\nif (!targetEmployees.length) {\n errors.push(\n 'No se encontraron empleados de Trinidad y Tobago en BambooHR.'\n );\n}\n\nreturn [\n {\n json: {\n ...base,\n ok:\n Boolean(base.ok ?? true) &&\n errors.length === 0,\n stage:\n errors.length === 0\n ? 'bamboohr_tt_normalizado'\n : 'bamboohr_tt_incompleto',\n errors: [\n ...(Array.isArray(base.errors)\n ? base.errors\n : []),\n ...errors,\n ],\n bamboo: {\n source:\n 'bamboohr_custom_report_only_current_false',\n period_start: periodStart,\n period_end: periodEnd,\n pages_fetched: pageObjects.length,\n expected_total: expectedTotal,\n raw_employees_count:\n fetchedEmployeesCount,\n employees_count:\n allNormalized.length,\n trinidad_tobago_count:\n targetEmployees.length,\n active_in_period_count:\n targetEmployees.filter(\n (employee) =>\n employee.overlaps_period\n ).length,\n active_status_count:\n targetEmployees.filter(\n (employee) =>\n normalize(employee.status) ===\n 'active'\n ).length,\n contextual_outside_country_count:\n contextualOutsideMap.size,\n validation_candidates_count:\n validationEmployees.length,\n resolved_name_matches:\n resolvedNameMatches,\n resolved_name_matches_count:\n Object.keys(\n resolvedNameMatches\n ).length,\n resolved_name_matches_found:\n resolvedNameMatchesFound,\n name_resolution_strategy:\n 'precomputed_indexed_fuzzy_matching_with_confirmed_aliases',\n fetch_complete: fetchComplete,\n validation_available:\n fetchComplete &&\n validationEmployees.length > 0,\n validation_rule:\n 'Target country/location plus a unique informative contextual name outside the country',\n employees:\n validationEmployees,\n restricted_fields:\n restrictedFields,\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 5088, - 6816 - ], - "id": "d6d947b2-4a0e-4917-855c-7b69a27dae4e", - "name": "Normalizar BambooHR TT" - }, - { - "parameters": { - "mode": "combine", - "combineBy": "combineByPosition", - "options": {} - }, - "type": "n8n-nodes-base.merge", - "typeVersion": 3.2, - "position": [ - 5968, - 7296 - ], - "id": "9760d7dd-776e-4ae7-b56b-2f8ac85cc768", - "name": "Merge - Agregar BambooHR TT" - }, - { - "parameters": { - "jsCode": "const data = $input.first().json || {};\n\nfunction roundMoney(value) {\n return Math.round((Number(value) || 0) * 100) / 100;\n}\n\nfunction moneyDiff(a, b) {\n return roundMoney((Number(a) || 0) - (Number(b) || 0));\n}\n\nfunction moneyEquals(a, b, tolerance = 0.02) {\n return Math.abs(roundMoney(a) - roundMoney(b)) <= tolerance;\n}\n\nfunction normalizeAccount(value) {\n return String(value ?? '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim();\n}\n\nfunction normalizeName(value) {\n return String(value ?? '')\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/['’`-]/g, '')\n .replace(/[^a-z0-9 ]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction nameWords(value) {\n const ignored = new Set(['de', 'del', 'la', 'las', 'los', 'y', 'e', 'el']);\n return normalizeName(value)\n .split(' ')\n .filter((word) => word.length > 1 && !ignored.has(word));\n}\n\nfunction editDistance(a, b) {\n if (a === b) return 0;\n if (!a) return b.length;\n if (!b) return a.length;\n\n const previous = Array.from({ length: b.length + 1 }, (_, index) => index);\n\n for (let i = 1; i <= a.length; i++) {\n const current = [i];\n\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n\n current[j] = Math.min(\n current[j - 1] + 1,\n previous[j] + 1,\n previous[j - 1] + cost\n );\n }\n\n for (let j = 0; j < current.length; j++) {\n previous[j] = current[j];\n }\n }\n\n return previous[b.length];\n}\n\nfunction tokenMatches(a, b) {\n if (a === b) return true;\n\n const minLength = Math.min(a.length, b.length);\n\n if (minLength >= 8 && editDistance(a, b) <= 2) return true;\n if (minLength >= 5 && editDistance(a, b) <= 1) return true;\n\n return false;\n}\n\nfunction bambooTokenMatches(a, b) {\n if (tokenMatches(a, b)) return true;\n\n const minLength = Math.min(a.length, b.length);\n const maxLength = Math.max(a.length, b.length);\n const distance = editDistance(a, b);\n\n // Tolera variaciones pequeñas de escritura entre Banco/Nómina y BambooHR,\n // por ejemplo Anessa <-> Annesa, sin flexibilizar el cruce principal.\n if (minLength >= 6 && distance <= 2) {\n return true;\n }\n\n // Permite apellidos compuestos como BeharrySingh vs Singh,\n // pero evita aceptar coincidencias demasiado amplias.\n return (\n minLength >= 4 &&\n maxLength - minLength <= 10 &&\n (\n a.startsWith(b) ||\n b.startsWith(a) ||\n a.endsWith(b) ||\n b.endsWith(a)\n )\n );\n}\n\nfunction samePersonName(a, b) {\n const normalizedA = normalizeName(a);\n const normalizedB = normalizeName(b);\n\n if (!normalizedA || !normalizedB) return false;\n if (normalizedA === normalizedB) return true;\n\n const compactA = normalizedA.replace(/\\s+/g, '');\n const compactB = normalizedB.replace(/\\s+/g, '');\n\n if (compactA === compactB) return true;\n\n const wordsA = nameWords(a);\n const wordsB = nameWords(b);\n\n if (!wordsA.length || !wordsB.length) return false;\n\n const usedB = new Set();\n let matches = 0;\n\n for (const wordA of wordsA) {\n const matchIndex = wordsB.findIndex((wordB, index) => {\n return !usedB.has(index) && tokenMatches(wordA, wordB);\n });\n\n if (matchIndex >= 0) {\n usedB.add(matchIndex);\n matches += 1;\n }\n }\n\n const smallerLength = Math.min(wordsA.length, wordsB.length);\n const ratio = matches / smallerLength;\n\n if (smallerLength <= 2) {\n return matches === smallerLength && matches >= 2;\n }\n\n return matches >= 2 && ratio >= 0.6;\n}\n\nfunction accountDistance(a, b) {\n return editDistance(normalizeAccount(a), normalizeAccount(b));\n}\n\nfunction accountRelationship(payrollAccount, bankAccount) {\n const payroll = normalizeAccount(payrollAccount);\n const bank = normalizeAccount(bankAccount);\n\n if (!payroll || !bank) {\n return { matches: false, type: 'none' };\n }\n\n if (payroll === bank) {\n return { matches: true, type: 'exact' };\n }\n\n const bankHasPayrollSuffix =\n bank.endsWith(payroll) &&\n bank.length > payroll.length &&\n bank.length - payroll.length <= 6;\n\n const payrollHasBankSuffix =\n payroll.endsWith(bank) &&\n payroll.length > bank.length &&\n payroll.length - bank.length <= 6;\n\n if (bankHasPayrollSuffix || payrollHasBankSuffix) {\n return { matches: true, type: 'reference_prefix' };\n }\n\n return { matches: false, type: 'none' };\n}\n\nfunction formatMoney(value) {\n return Math.abs(roundMoney(value)).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n}\n\nfunction bankNames(bank) {\n return Array.from(new Set([\n ...(Array.isArray(bank.bank_name_files) ? bank.bank_name_files : []),\n ...(Array.isArray(bank.bank_account_holders) ? bank.bank_account_holders : []),\n bank.bank_name_file || '',\n bank.bank_account_holder || '',\n ].filter(Boolean)));\n}\n\nfunction bankMatchesName(bank, payrollName) {\n return bankNames(bank).some((name) => samePersonName(payrollName, name));\n}\n\nfunction bestBankDisplayName(bank) {\n return (\n bank.bank_name_file ||\n bank.bank_account_holder ||\n bankNames(bank)[0] ||\n ''\n );\n}\n\nfunction bambooAliases(employee) {\n return Array.from(new Set([\n ...(Array.isArray(employee.aliases) ? employee.aliases : []),\n employee.full_name || '',\n [employee.first_name, employee.middle_name, employee.last_name]\n .filter(Boolean)\n .join(' '),\n [employee.preferred_name, employee.last_name]\n .filter(Boolean)\n .join(' '),\n ].map((value) => String(value || '').trim()).filter(Boolean)));\n}\n\nfunction bambooEmployeeNumber(employee) {\n return normalizeAccount(\n employee.employee_number ||\n employee.employeeNumber ||\n ''\n );\n}\n\nfunction isTrinidadTobagoBambooEmployee(employee) {\n const country = normalizeName(\n employee.country || ''\n );\n const location = normalizeName(\n employee.location || ''\n );\n\n return (\n country === 'tt' ||\n country === 'tto' ||\n country.includes('trinidad') ||\n country.includes('tobago') ||\n location === 'tt' ||\n location === 'tto' ||\n location.includes('trinidad') ||\n location.includes('tobago')\n );\n}\n\nfunction isBambooValidationEligible(employee) {\n // La versión corregida del normalizador declara este campo.\n if (employee.validation_eligible === true) {\n return true;\n }\n\n if (employee.validation_eligible === false) {\n return false;\n }\n\n // Compatibilidad defensiva si este nodo recibe datos de una ejecución\n // anterior: los perfiles de TT siguen siendo válidos. Un perfil de otro\n // país solo puede utilizarse cuando está Active y vigente en el período.\n if (isTrinidadTobagoBambooEmployee(employee)) {\n return true;\n }\n\n return (\n employee.overlaps_period === true &&\n normalizeName(employee.status) === 'active'\n );\n}\n\nfunction nameSimilarityScore(a, b) {\n const normalizedA = normalizeName(a);\n const normalizedB = normalizeName(b);\n\n if (!normalizedA || !normalizedB) return 0;\n if (normalizedA === normalizedB) return 1;\n\n const compactA = normalizedA.replace(/\\s+/g, '');\n const compactB = normalizedB.replace(/\\s+/g, '');\n\n if (compactA === compactB) return 1;\n\n const wordsA = nameWords(normalizedA);\n const wordsB = nameWords(normalizedB);\n\n if (!wordsA.length || !wordsB.length) return 0;\n\n const usedB = new Set();\n const usedA = new Set();\n let exactMatches = 0;\n let fuzzyMatches = 0;\n\n // Primero se reservan las coincidencias exactas para no perder\n // evidencia fuerte por el orden de las palabras.\n for (let indexA = 0; indexA < wordsA.length; indexA++) {\n const indexB = wordsB.findIndex(\n (wordB, currentIndexB) =>\n !usedB.has(currentIndexB) &&\n wordsA[indexA] === wordB\n );\n\n if (indexB >= 0) {\n usedA.add(indexA);\n usedB.add(indexB);\n exactMatches += 1;\n }\n }\n\n // Después se toleran errores ortográficos pequeños únicamente\n // para complementar una coincidencia que ya tiene evidencia exacta.\n for (let indexA = 0; indexA < wordsA.length; indexA++) {\n if (usedA.has(indexA)) continue;\n\n const indexB = wordsB.findIndex(\n (wordB, currentIndexB) =>\n !usedB.has(currentIndexB) &&\n bambooTokenMatches(wordsA[indexA], wordB)\n );\n\n if (indexB >= 0) {\n usedA.add(indexA);\n usedB.add(indexB);\n fuzzyMatches += 1;\n }\n }\n\n const matches = exactMatches + fuzzyMatches;\n\n if (matches < 2) return 0;\n\n // Dos palabras solo son suficientes cuando ambas coinciden exactamente.\n // Esto evita falsos positivos como un apellido correcto acompañado por\n // un nombre distinto que solo se parece parcialmente.\n if (matches === 2 && exactMatches < 2) return 0;\n\n // En nombres largos se exige al menos dos piezas exactas y se permite\n // que una tercera palabra tenga una diferencia ortográfica pequeña.\n if (matches >= 3 && exactMatches < 2) return 0;\n\n const ratioToShorter =\n matches / Math.min(wordsA.length, wordsB.length);\n const ratioToLonger =\n matches / Math.max(wordsA.length, wordsB.length);\n\n return (\n ratioToShorter * 0.7 +\n ratioToLonger * 0.3\n );\n}\n\nfunction bankRowKey(row) {\n return [\n row.source_file || '',\n row.row_number || '',\n ].join('|');\n}\n\nfunction bankRowNames(row) {\n const rowKey = bankRowKey(row);\n\n const linkedPayrollNames =\n typeof linkedPayrollNamesByBankRow !== 'undefined'\n ? linkedPayrollNamesByBankRow.get(rowKey) || []\n : [];\n\n return Array.from(new Set([\n row.bank_name_file || '',\n row.bank_account_holder || '',\n ...linkedPayrollNames,\n ].map((value) => String(value || '').trim()).filter(Boolean)));\n}\n\nfunction bankRowEmployeeNumbers(row) {\n const rowKey = bankRowKey(row);\n\n const linkedNumbers =\n typeof linkedPayrollNumbersByBankRow !== 'undefined'\n ? linkedPayrollNumbersByBankRow.get(rowKey) || []\n : [];\n\n return Array.from(new Set(\n linkedNumbers\n .map(normalizeAccount)\n .filter((value) => value.length >= 6)\n ));\n}\n\nfunction bankRowReferenceText(row) {\n return [\n row.reference || '',\n row.concept || '',\n row.bank_name_file || '',\n row.bank_account_holder || '',\n ...bankRowEmployeeNumbers(row),\n ].join(' ');\n}\n\nfunction isClearlyNonEmployeePayment(row) {\n const normalized = normalizeName([\n row.concept || '',\n row.bank_name_file || '',\n row.bank_account_holder || '',\n ].join(' '));\n\n return [\n 'pension alimenticia',\n 'embargo judicial',\n 'retencion judicial',\n ].some((token) =>\n normalized.includes(normalizeName(token))\n );\n}\n\nfunction buildBambooSearchIndex(employees) {\n const records = [];\n const exactAliasMap = new Map();\n const tokenIndexSets = new Map();\n const employeeNumberMap = new Map();\n\n for (let index = 0; index < employees.length; index++) {\n const employee = employees[index];\n const aliases = bambooAliases(employee)\n .map((alias) => ({\n raw: alias,\n normalized: normalizeName(alias),\n }))\n .filter((alias) => alias.normalized);\n\n const uniqueAliases = [];\n const seenAliases = new Set();\n\n for (const alias of aliases) {\n if (seenAliases.has(alias.normalized)) continue;\n seenAliases.add(alias.normalized);\n uniqueAliases.push({\n ...alias,\n words: nameWords(alias.normalized),\n });\n\n const exact = exactAliasMap.get(alias.normalized) || [];\n exact.push(index);\n exactAliasMap.set(alias.normalized, exact);\n\n const uniqueTokens = Array.from(new Set(\n nameWords(alias.normalized)\n .filter((token) => token.length >= 3)\n ));\n\n for (const token of uniqueTokens) {\n const set = tokenIndexSets.get(token) || new Set();\n set.add(index);\n tokenIndexSets.set(token, set);\n }\n }\n\n const employeeNumber = bambooEmployeeNumber(employee);\n\n if (employeeNumber.length >= 6) {\n const matches = employeeNumberMap.get(employeeNumber) || [];\n matches.push(index);\n employeeNumberMap.set(employeeNumber, matches);\n }\n\n records.push({\n employee,\n aliases: uniqueAliases,\n employeeNumber,\n });\n }\n\n const tokenIndex = new Map();\n for (const [token, set] of tokenIndexSets.entries()) {\n tokenIndex.set(token, Array.from(set));\n }\n\n return {\n records,\n exactAliasMap,\n tokenIndex,\n employeeNumberMap,\n };\n}\n\nconst bambooMatchCache = new Map();\n\nfunction findBambooMatch(bankRow) {\n const names = bankRowNames(bankRow);\n const normalizedNames = Array.from(new Set(\n names.map(normalizeName).filter(Boolean)\n ));\n const directEmployeeNumbers = bankRowEmployeeNumbers(bankRow);\n const referenceNumberTokens = Array.from(new Set(\n (\n String(bankRowReferenceText(bankRow) || '')\n .match(/\\d{6,}/g) || []\n )\n .map(normalizeAccount)\n .filter((value) => value.length >= 6)\n ));\n\n const cacheKey = [\n ...directEmployeeNumbers.sort(),\n ...referenceNumberTokens.sort(),\n ...normalizedNames.sort(),\n ].join('|');\n\n if (bambooMatchCache.has(cacheKey)) {\n return bambooMatchCache.get(cacheKey);\n }\n\n const numberCandidateIndexes = new Set();\n\n for (const employeeNumber of directEmployeeNumbers) {\n for (\n const index of\n bambooSearch.employeeNumberMap.get(employeeNumber) || []\n ) {\n numberCandidateIndexes.add(index);\n }\n }\n\n if (!numberCandidateIndexes.size && referenceNumberTokens.length) {\n for (const referenceNumber of referenceNumberTokens) {\n for (\n const index of\n bambooSearch.employeeNumberMap.get(referenceNumber) || []\n ) {\n numberCandidateIndexes.add(index);\n }\n }\n }\n\n if (numberCandidateIndexes.size === 1) {\n const index = numberCandidateIndexes.values().next().value;\n const record = bambooSearch.records[index];\n\n // Un Employee Number enlazado desde la nómina es confiable.\n // Si proviene solamente de la referencia bancaria, también se exige\n // que el nombre corresponda para evitar falsos positivos por números\n // accidentales dentro del Addenda.\n const referenceNameScore = Math.max(\n 0,\n ...names.flatMap((currentBankName) =>\n record.aliases.map((alias) =>\n nameSimilarityScore(\n currentBankName,\n alias.normalized\n )\n )\n )\n );\n\n if (\n directEmployeeNumbers.length ||\n referenceNameScore >= 0.84\n ) {\n const result = {\n found: true,\n matched_by: directEmployeeNumbers.length\n ? 'employee_number_payroll'\n : 'employee_number_reference_and_name',\n confidence: directEmployeeNumbers.length\n ? 1\n : referenceNameScore,\n employee: record.employee,\n };\n bambooMatchCache.set(cacheKey, result);\n return result;\n }\n\n // La coincidencia numérica aislada se descarta y se continúa\n // con la validación por nombre.\n numberCandidateIndexes.clear();\n }\n\n\n\n /*\n * Consulta primero la resolución calculada una sola vez en el\n * normalizador. Esto evita repetir búsquedas aproximadas por cada fila\n * bancaria y mantiene el task runner estable incluso con miles de\n * empleados en BambooHR.\n */\n const precomputedNameMatches =\n data.bamboo?.resolved_name_matches ||\n {};\n\n const precomputedNameEntries =\n names.map((entry) => {\n const raw =\n typeof entry === 'string'\n ? entry\n : entry?.raw || '';\n\n return {\n raw,\n normalized:\n typeof entry === 'string'\n ? normalizeName(entry)\n : (\n entry?.normalized ||\n normalizeName(raw)\n ),\n token_count:\n typeof entry === 'string'\n ? nameWords(entry).length\n : (\n entry?.tokenCount ||\n nameWords(raw).length\n ),\n };\n }).filter((entry) =>\n entry.normalized\n );\n\n const precomputedFoundByEmployee =\n new Map();\n\n function resolutionEmployeeKey(\n employee\n ) {\n return (\n String(\n employee?.bamboo_id ||\n ''\n ).trim() ||\n normalizeAccount(\n employee?.employee_number ||\n employee?.employeeNumber ||\n ''\n ) ||\n normalizeName(\n employee?.full_name ||\n employee?.displayName ||\n ''\n )\n );\n }\n\n for (\n const nameEntry of\n precomputedNameEntries\n ) {\n const decision =\n precomputedNameMatches[\n nameEntry.normalized\n ];\n\n if (\n !decision ||\n decision.found !== true\n ) {\n continue;\n }\n\n let employee =\n Number.isInteger(\n decision.employee_index\n )\n ? bambooEmployees[\n decision.employee_index\n ]\n : null;\n\n const expectedKey =\n String(\n decision.employee_key ||\n ''\n ).trim();\n\n if (\n !employee ||\n (\n expectedKey &&\n resolutionEmployeeKey(\n employee\n ) !== expectedKey\n )\n ) {\n employee =\n bambooEmployees.find(\n (candidate) =>\n resolutionEmployeeKey(\n candidate\n ) === expectedKey\n ) || null;\n }\n\n if (!employee) continue;\n\n const employeeKey =\n resolutionEmployeeKey(employee);\n\n const candidate = {\n employee,\n employee_key:\n employeeKey,\n confidence:\n Number(\n decision.confidence || 0\n ),\n matched_by:\n decision.matched_by ||\n 'precomputed_name',\n bank_name:\n nameEntry.raw,\n bamboo_alias:\n decision.bamboo_alias ||\n employee.full_name ||\n '',\n informativeness:\n nameEntry.token_count,\n };\n\n const existing =\n precomputedFoundByEmployee\n .get(employeeKey);\n\n if (\n !existing ||\n candidate.confidence >\n existing.confidence ||\n (\n candidate.confidence ===\n existing.confidence &&\n candidate.informativeness >\n existing.informativeness\n )\n ) {\n precomputedFoundByEmployee.set(\n employeeKey,\n candidate\n );\n }\n }\n\n const precomputedRanked =\n Array.from(\n precomputedFoundByEmployee\n .values()\n ).sort((left, right) => {\n if (\n right.confidence !==\n left.confidence\n ) {\n return (\n right.confidence -\n left.confidence\n );\n }\n\n return (\n right.informativeness -\n left.informativeness\n );\n });\n\n if (precomputedRanked.length === 1) {\n const best =\n precomputedRanked[0];\n\n const result = {\n found: true,\n matched_by:\n best.matched_by,\n confidence:\n best.confidence,\n employee:\n best.employee,\n bank_name:\n best.bank_name,\n bamboo_alias:\n best.bamboo_alias,\n };\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n }\n\n if (\n precomputedRanked.length > 1\n ) {\n const best =\n precomputedRanked[0];\n\n const second =\n precomputedRanked[1];\n\n if (\n best.confidence -\n second.confidence >= 0.08\n ) {\n const result = {\n found: true,\n matched_by:\n best.matched_by,\n confidence:\n best.confidence,\n employee:\n best.employee,\n bank_name:\n best.bank_name,\n bamboo_alias:\n best.bamboo_alias,\n };\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n }\n\n const result = {\n found: false,\n matched_by: null,\n confidence:\n best.confidence,\n employee: null,\n ambiguous: true,\n reason:\n 'conflicting_precomputed_name_matches',\n best_candidate: {\n employee:\n best.employee,\n score:\n best.confidence,\n bank_name:\n best.bank_name,\n bamboo_alias:\n best.bamboo_alias,\n },\n };\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n }\n\n\n // Se prioriza el nombre más informativo de la fila. Esto evita que un\n // nombre corto y ambiguo bloquee un nombre completo que identifica a una\n // sola persona, por ejemplo \"Carlos De Leon\" frente a\n // \"Carlos Alexander De Leon Chajon\".\n const informativeNames = names\n .map((raw) => ({\n raw,\n tokens: nameWords(raw).length,\n }))\n .filter((entry) =>\n entry.tokens >= 3\n )\n .sort((left, right) =>\n right.tokens - left.tokens\n );\n\n for (const informativeName of informativeNames) {\n let bestInformative = null;\n let secondInformative = null;\n\n for (\n let index = 0;\n index < bambooSearch.records.length;\n index++\n ) {\n const record =\n bambooSearch.records[index];\n\n let score = 0;\n let bestAlias = '';\n\n for (const alias of record.aliases) {\n const currentScore =\n nameSimilarityScore(\n informativeName.raw,\n alias.normalized\n );\n\n if (currentScore > score) {\n score = currentScore;\n bestAlias = alias.raw;\n }\n }\n\n if (score <= 0) continue;\n\n const candidate = {\n index,\n record,\n score,\n bamboo_alias: bestAlias,\n };\n\n if (\n !bestInformative ||\n candidate.score >\n bestInformative.score\n ) {\n secondInformative =\n bestInformative;\n bestInformative =\n candidate;\n } else if (\n !secondInformative ||\n candidate.score >\n secondInformative.score\n ) {\n secondInformative =\n candidate;\n }\n }\n\n const informativeMargin =\n bestInformative\n ? bestInformative.score -\n (secondInformative?.score || 0)\n : 0;\n\n if (\n bestInformative &&\n bestInformative.score >= 0.90 &&\n informativeMargin >= 0.05\n ) {\n const result = {\n found: true,\n matched_by:\n bestInformative.score === 1\n ? 'exact_informative_name'\n : 'strong_informative_name',\n confidence:\n bestInformative.score,\n employee:\n bestInformative.record.employee,\n bank_name:\n informativeName.raw,\n bamboo_alias:\n bestInformative.bamboo_alias,\n };\n\n bambooMatchCache.set(\n cacheKey,\n result\n );\n\n return result;\n }\n }\n\n const exactCandidateIndexes = new Set();\n\n for (const name of normalizedNames) {\n for (\n const index of\n bambooSearch.exactAliasMap.get(name) || []\n ) {\n exactCandidateIndexes.add(index);\n }\n }\n\n if (exactCandidateIndexes.size === 1) {\n const index = exactCandidateIndexes.values().next().value;\n const result = {\n found: true,\n matched_by: 'exact_name',\n confidence: 1,\n employee: bambooSearch.records[index].employee,\n bank_name: names[0] || '',\n bamboo_alias:\n bambooSearch.records[index].aliases[0]?.raw || '',\n };\n bambooMatchCache.set(cacheKey, result);\n return result;\n }\n\n const candidateVotes = new Map();\n\n for (const name of normalizedNames) {\n const tokens = Array.from(new Set(\n nameWords(name)\n .filter((token) => token.length >= 3)\n ));\n\n for (const token of tokens) {\n const indexes = bambooSearch.tokenIndex.get(token) || [];\n\n // Evita que nombres demasiado comunes generen cientos de comparaciones.\n if (indexes.length > 180) continue;\n\n for (const index of indexes) {\n candidateVotes.set(\n index,\n (candidateVotes.get(index) || 0) + 1\n );\n }\n }\n }\n\n // Cuando una letra fue agregada, omitida o reemplazada, puede no existir\n // ningún token exacto compartido. En ese caso se buscan tokens cercanos\n // solamente entre palabras de longitud comparable.\n if (!candidateVotes.size) {\n for (const name of normalizedNames) {\n const queryTokens = Array.from(new Set(\n nameWords(name)\n .filter((token) => token.length >= 3)\n ));\n\n for (const queryToken of queryTokens) {\n for (\n const [indexedToken, indexes] of\n bambooSearch.tokenIndex.entries()\n ) {\n if (\n Math.abs(\n queryToken.length - indexedToken.length\n ) > 2\n ) {\n continue;\n }\n\n if (\n queryToken[0] !== indexedToken[0] &&\n queryToken.at(-1) !== indexedToken.at(-1)\n ) {\n continue;\n }\n\n if (\n !bambooTokenMatches(\n queryToken,\n indexedToken\n )\n ) {\n continue;\n }\n\n if (indexes.length > 180) continue;\n\n for (const index of indexes) {\n candidateVotes.set(\n index,\n (candidateVotes.get(index) || 0) + 0.75\n );\n }\n }\n }\n }\n }\n\n const candidateIndexes = Array.from(candidateVotes.entries())\n .sort((a, b) => b[1] - a[1])\n .slice(0, 180)\n .map(([index]) => index);\n\n let best = null;\n let second = null;\n\n for (const index of candidateIndexes) {\n const record = bambooSearch.records[index];\n let bestScoreForEmployee = 0;\n let bestBankName = '';\n let bestAlias = '';\n\n for (const currentBankName of names) {\n for (const alias of record.aliases) {\n const score = nameSimilarityScore(\n currentBankName,\n alias.normalized\n );\n\n if (score > bestScoreForEmployee) {\n bestScoreForEmployee = score;\n bestBankName = currentBankName;\n bestAlias = alias.raw;\n }\n }\n }\n\n if (bestScoreForEmployee <= 0) continue;\n\n const candidate = {\n employee: record.employee,\n score: bestScoreForEmployee,\n bank_name: bestBankName,\n bamboo_alias: bestAlias,\n };\n\n if (!best || candidate.score > best.score) {\n second = best;\n best = candidate;\n } else if (!second || candidate.score > second.score) {\n second = candidate;\n }\n }\n\n let result;\n\n if (\n best &&\n best.score >= 0.78 &&\n (!second || best.score - second.score >= 0.05)\n ) {\n result = {\n found: true,\n matched_by:\n normalizeName(best.bank_name) ===\n normalizeName(best.bamboo_alias)\n ? 'exact_name'\n : 'strong_name',\n confidence: best.score,\n employee: best.employee,\n bank_name: best.bank_name,\n bamboo_alias: best.bamboo_alias,\n };\n } else {\n result = {\n found: false,\n matched_by: null,\n confidence: best?.score || 0,\n employee: null,\n ambiguous: Boolean(\n best &&\n second &&\n best.score >= 0.7 &&\n best.score - second.score < 0.05\n ),\n best_candidate: best || null,\n };\n }\n\n bambooMatchCache.set(cacheKey, result);\n return result;\n}\n\nfunction supplementKey(supplement) {\n return [\n supplement.source_sheet || '',\n supplement.row_number || '',\n supplement.supplement_id || '',\n supplement.account || '',\n supplement.payroll_amount || 0,\n ].join('|');\n}\n\nconst payrollAccounts = (data.payroll?.grouped_by_account || [])\n .map((row) => ({\n ...row,\n group_key:\n row.group_key ||\n `${normalizeAccount(row.account)}:${row.currency || 'TTD'}`,\n account: normalizeAccount(row.account),\n employee_name: row.employee_name || row.employee || '',\n employee_number: row.employee_number || row.employeeNumber || '',\n currency: row.currency || 'TTD',\n payroll_amount: roundMoney(row.payroll_amount || row.payrollAmount),\n source_rows: Array.isArray(row.source_rows) ? [...row.source_rows] : [],\n source_sheets: Array.isArray(row.source_sheets)\n ? [...row.source_sheets]\n : [],\n }))\n .filter((row) => row.account && row.payroll_amount > 0);\n\nconst payrollNoAccountRows = (data.payroll?.no_account_rows || [])\n .map((row) => ({\n ...row,\n account: '',\n employee_name: row.employee_name || row.employee || '',\n employee_number: row.employee_number || row.employeeNumber || '',\n currency: row.currency || 'TTD',\n payroll_amount: roundMoney(row.payroll_amount || row.payrollAmount),\n }))\n .filter((row) => row.payroll_amount > 0);\n\nconst bankAccounts = (data.bank?.grouped_by_account || [])\n .map((row) => ({\n ...row,\n group_key:\n row.group_key ||\n `ACCOUNT:${normalizeAccount(row.account)}:${row.currency || 'TTD'}`,\n account: normalizeAccount(row.account),\n account_is_valid: Boolean(row.account_is_valid),\n currency: row.currency || 'TTD',\n amount: roundMoney(row.amount || row.bank_amount || row.bankAmount),\n source_rows: Array.isArray(row.source_rows) ? [...row.source_rows] : [],\n }))\n .filter((row) => row.amount > 0);\n\nconst rawBambooValidationEmployees =\n Array.isArray(data.bamboo?.employees)\n ? data.bamboo.employees\n : [];\n\nconst bambooEmployees =\n rawBambooValidationEmployees.filter(\n isBambooValidationEligible\n );\n\nconst excludedBambooValidationEmployees =\n rawBambooValidationEmployees\n .filter(\n (employee) =>\n !isBambooValidationEligible(employee)\n )\n .map((employee) => ({\n bamboo_id:\n employee.bamboo_id || '',\n employee_number:\n employee.employee_number || '',\n full_name:\n employee.full_name || '',\n country:\n employee.country || '',\n location:\n employee.location || '',\n status:\n employee.status || '',\n overlaps_period:\n Boolean(employee.overlaps_period),\n validation_scope:\n employee.validation_scope || '',\n }));\n\nconst bambooValidationAvailable =\n data.bamboo?.fetch_complete === true &&\n data.bamboo?.validation_available === true &&\n bambooEmployees.length > 0;\n\nconst bambooValidationWarning =\n bambooValidationAvailable\n ? null\n : (\n data.errors?.find((error) =>\n String(error || '').toLowerCase().includes('bamboohr')\n ) ||\n 'La validación Banco sin Bamboo no estuvo disponible porque la descarga de empleados de BambooHR quedó incompleta.'\n );\n\nconst bambooSearch = buildBambooSearchIndex(\n bambooEmployees\n);\n\nconst bankDetailRows = Array.isArray(data.bank?.rows)\n ? data.bank.rows\n : [];\n\nconst potentialSupplements = (\n data.payroll?.potential_supplements ||\n data.debug_payroll?.potential_supplements ||\n data.debug_payroll?.attached_supplements ||\n []\n)\n .map((row) => ({\n ...row,\n account: normalizeAccount(row.account),\n currency: row.currency || 'TTD',\n payroll_amount: roundMoney(row.payroll_amount || row.payrollAmount),\n }))\n .filter((row) => {\n const id = normalizeName(row.supplement_id || '');\n\n return (\n row.account &&\n row.payroll_amount >= 10 &&\n !id.includes('back up')\n );\n });\n\nconst supplementsByAccountCurrency = new Map();\n\nfor (const supplement of potentialSupplements) {\n const key = `${supplement.account}:${supplement.currency}`;\n const current = supplementsByAccountCurrency.get(key) || [];\n\n current.push(supplement);\n supplementsByAccountCurrency.set(key, current);\n}\n\nfunction chooseConditionalSupplements(payroll, bank) {\n const baseAmount = roundMoney(payroll.payroll_amount);\n const bankAmount = roundMoney(bank.amount);\n const candidates =\n supplementsByAccountCurrency.get(\n `${payroll.account}:${payroll.currency}`\n ) || [];\n\n if (\n !candidates.length ||\n bankAmount <= baseAmount + 0.02\n ) {\n return {\n selected: [],\n effectiveAmount: baseAmount,\n baseAmount,\n improvement: 0,\n };\n }\n\n const baseDifference = Math.abs(baseAmount - bankAmount);\n let bestSelected = [];\n let bestAmount = baseAmount;\n let bestDifference = baseDifference;\n\n if (candidates.length <= 12) {\n const combinations = 1 << candidates.length;\n\n for (let mask = 1; mask < combinations; mask++) {\n const selected = [];\n let selectedTotal = 0;\n\n for (let index = 0; index < candidates.length; index++) {\n if ((mask & (1 << index)) !== 0) {\n selected.push(candidates[index]);\n selectedTotal = roundMoney(\n selectedTotal + candidates[index].payroll_amount\n );\n }\n }\n\n const candidateAmount = roundMoney(baseAmount + selectedTotal);\n const candidateDifference = Math.abs(\n candidateAmount - bankAmount\n );\n\n if (candidateDifference < bestDifference) {\n bestSelected = selected;\n bestAmount = candidateAmount;\n bestDifference = candidateDifference;\n }\n }\n } else {\n const sorted = [...candidates].sort(\n (a, b) => b.payroll_amount - a.payroll_amount\n );\n\n let runningAmount = baseAmount;\n const selected = [];\n\n for (const candidate of sorted) {\n const nextAmount = roundMoney(\n runningAmount + candidate.payroll_amount\n );\n\n if (\n Math.abs(nextAmount - bankAmount) <\n Math.abs(runningAmount - bankAmount)\n ) {\n selected.push(candidate);\n runningAmount = nextAmount;\n }\n }\n\n bestSelected = selected;\n bestAmount = runningAmount;\n bestDifference = Math.abs(bestAmount - bankAmount);\n }\n\n const improvement = roundMoney(\n baseDifference - bestDifference\n );\n\n // Evita sumar valores accidentales o inmateriales, como un \"Asignado\" de Q1.\n if (!bestSelected.length || improvement < 5) {\n return {\n selected: [],\n effectiveAmount: baseAmount,\n baseAmount,\n improvement: 0,\n };\n }\n\n return {\n selected: bestSelected,\n effectiveAmount: roundMoney(bestAmount),\n baseAmount,\n improvement,\n };\n}\n\nfunction getDirectCandidates(payroll, matchedBankKeys) {\n return bankAccounts\n .filter((bank) => {\n if (matchedBankKeys.has(bank.group_key)) return false;\n if (bank.currency !== payroll.currency) return false;\n\n const relationship = accountRelationship(\n payroll.account,\n bank.account\n );\n\n if (!relationship.matches) return false;\n\n // Un sufijo de referencia solamente es válido cuando el nombre también\n // corresponde a la misma persona.\n if (\n relationship.type === 'reference_prefix' &&\n !bankMatchesName(bank, payroll.employee_name)\n ) {\n return false;\n }\n\n return true;\n })\n .map((bank) => {\n const relationship = accountRelationship(\n payroll.account,\n bank.account\n );\n const supplementDecision =\n chooseConditionalSupplements(payroll, bank);\n\n return {\n bank,\n relationship,\n supplementDecision,\n nameMatches: bankMatchesName(bank, payroll.employee_name),\n };\n })\n .sort((a, b) => {\n const exactDifference =\n Number(b.relationship.type === 'exact') -\n Number(a.relationship.type === 'exact');\n\n if (exactDifference !== 0) return exactDifference;\n\n const nameDifference =\n Number(b.nameMatches) - Number(a.nameMatches);\n\n if (nameDifference !== 0) return nameDifference;\n\n return (\n Math.abs(\n a.supplementDecision.effectiveAmount - a.bank.amount\n ) -\n Math.abs(\n b.supplementDecision.effectiveAmount - b.bank.amount\n )\n );\n });\n}\n\nfunction buildSources(payroll, selectedSupplements) {\n const supplementRows = selectedSupplements.map((row) => ({\n source_sheet: row.source_sheet,\n row_number: row.row_number,\n amount: row.payroll_amount,\n supplement_original_name:\n row.supplement_original_name || row.employee_name || '',\n supplement_id: row.supplement_id || '',\n applied_conditionally: true,\n }));\n\n const sourceRows = [\n ...(payroll.source_rows || []),\n ...supplementRows,\n ];\n\n const sourceSheets = Array.from(new Set([\n ...(payroll.source_sheets || []),\n ...selectedSupplements\n .map((row) => row.source_sheet)\n .filter(Boolean),\n ]));\n\n return { sourceRows, sourceSheets };\n}\n\nconst matchedPayrollKeys = new Set();\nconst matchedBankKeys = new Set();\nconst matchedNoAccountIndexes = new Set();\nconst appliedSupplementKeys = new Set();\nconst appliedSupplements = [];\nconst finalExactReconciliations = [];\nconst rows = [];\n\nfunction registerSupplements(selected) {\n for (const supplement of selected || []) {\n const key = supplementKey(supplement);\n\n if (!appliedSupplementKeys.has(key)) {\n appliedSupplementKeys.add(key);\n appliedSupplements.push(supplement);\n }\n }\n}\n\n// 1) Cuenta exacta o referencia con prefijo, y monto conciliado.\nfor (const payroll of payrollAccounts) {\n const candidates = getDirectCandidates(\n payroll,\n matchedBankKeys\n ).filter((candidate) => {\n return moneyEquals(\n candidate.supplementDecision.effectiveAmount,\n candidate.bank.amount\n );\n });\n\n if (candidates.length !== 1) continue;\n\n const candidate = candidates[0];\n const bank = candidate.bank;\n const decision = candidate.supplementDecision;\n const sources = buildSources(payroll, decision.selected);\n\n matchedPayrollKeys.add(payroll.group_key);\n matchedBankKeys.add(bank.group_key);\n registerSupplements(decision.selected);\n\n rows.push({\n id: `match_${payroll.group_key}_${bank.group_key}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: payroll.currency,\n payrollAmount: decision.effectiveAmount,\n payroll_amount: decision.effectiveAmount,\n payrollBaseAmount: decision.baseAmount,\n payroll_base_amount: decision.baseAmount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: 0,\n status: 'Coincidencia',\n category: 'coincidencia',\n subcategory:\n candidate.relationship.type === 'reference_prefix'\n ? 'referencia_bancaria_con_prefijo'\n : decision.selected.length\n ? 'cuenta_monto_y_suplemento_condicional'\n : 'cuenta_y_monto_coinciden',\n observation:\n candidate.relationship.type === 'reference_prefix'\n ? 'Conciliado por nombre, monto y referencia bancaria con prefijo.'\n : decision.selected.length\n ? 'Conciliado correctamente. Se aplicó un suplemento porque el banco mostró un pago adicional.'\n : 'Conciliado correctamente.',\n applied_supplements: decision.selected,\n source_sheets: sources.sourceSheets,\n source_rows: sources.sourceRows,\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 2) Cuenta diferente, pero nombre y monto coinciden.\n// Se ejecuta antes de crear diferencias directas para resolver casos como\n// Ashly/Ashley Ramos: la cuenta de la nómina apunta a otra transacción,\n// pero existe otra cuenta bancaria con el mismo nombre y monto correcto.\nfor (const payroll of payrollAccounts) {\n if (matchedPayrollKeys.has(payroll.group_key)) continue;\n\n const candidates = bankAccounts\n .filter((bank) => {\n if (matchedBankKeys.has(bank.group_key)) return false;\n if (bank.currency !== payroll.currency) return false;\n if (!bankMatchesName(bank, payroll.employee_name)) return false;\n\n const decision = chooseConditionalSupplements(\n payroll,\n bank\n );\n\n return moneyEquals(\n decision.effectiveAmount,\n bank.amount\n );\n })\n .map((bank) => ({\n bank,\n supplementDecision: chooseConditionalSupplements(\n payroll,\n bank\n ),\n }));\n\n if (candidates.length !== 1) continue;\n\n const candidate = candidates[0];\n const bank = candidate.bank;\n const decision = candidate.supplementDecision;\n const relationship = accountRelationship(\n payroll.account,\n bank.account\n );\n\n // Las referencias con prefijo ya debieron resolverse en el paso 1.\n if (relationship.type === 'reference_prefix') continue;\n\n const sources = buildSources(payroll, decision.selected);\n\n matchedPayrollKeys.add(payroll.group_key);\n matchedBankKeys.add(bank.group_key);\n registerSupplements(decision.selected);\n\n rows.push({\n id: `possible_wrong_account_${payroll.group_key}_${bank.group_key}`,\n employee: payroll.employee_name || bestBankDisplayName(bank),\n employee_name:\n payroll.employee_name || bestBankDisplayName(bank),\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: payroll.currency,\n payrollAmount: decision.effectiveAmount,\n payroll_amount: decision.effectiveAmount,\n payrollBaseAmount: decision.baseAmount,\n payroll_base_amount: decision.baseAmount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: 0,\n status: 'Riesgo',\n category: 'posible_cuenta_mal_digitada',\n subcategory:\n 'nombre_y_monto_coinciden_cuenta_diferente',\n observation:\n `El nombre y el monto coinciden, pero la cuenta de nómina ` +\n `(${payroll.account || 'sin cuenta'}) es diferente a la cuenta ` +\n `del banco (${bank.account || 'sin cuenta válida'}).`,\n applied_supplements: decision.selected,\n source_sheets: sources.sourceSheets,\n source_rows: sources.sourceRows,\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 3) Nómina sin cuenta válida: conciliar por nombre y monto.\nfor (\n let index = 0;\n index < payrollNoAccountRows.length;\n index++\n) {\n const payroll = payrollNoAccountRows[index];\n\n const candidates = bankAccounts.filter((bank) => {\n if (matchedBankKeys.has(bank.group_key)) return false;\n if (bank.currency !== payroll.currency) return false;\n if (!moneyEquals(bank.amount, payroll.payroll_amount)) {\n return false;\n }\n\n return bankMatchesName(bank, payroll.employee_name);\n });\n\n if (candidates.length !== 1) continue;\n\n const bank = candidates[0];\n\n matchedNoAccountIndexes.add(index);\n matchedBankKeys.add(bank.group_key);\n\n rows.push({\n id: `possible_missing_account_${index}_${bank.group_key}`,\n employee:\n payroll.employee_name || bestBankDisplayName(bank),\n employee_name:\n payroll.employee_name || bestBankDisplayName(bank),\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: bank.account,\n payrollAccount: '',\n payroll_account: '',\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: payroll.currency,\n payrollAmount: payroll.payroll_amount,\n payroll_amount: payroll.payroll_amount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: 0,\n status: 'Riesgo',\n category: 'posible_cuenta_mal_digitada',\n subcategory:\n 'cuenta_faltante_en_nomina_nombre_y_monto_coinciden',\n observation:\n `El nombre y el monto coinciden, pero la nómina no tiene una cuenta bancaria válida registrada. El banco utilizó la cuenta ${bank.account}.`,\n source_sheet: payroll.source_sheet,\n row_number: payroll.row_number,\n source_rows: [\n {\n source_sheet: payroll.source_sheet,\n row_number: payroll.row_number,\n account: '',\n amount: payroll.payroll_amount,\n employee_name: payroll.employee_name,\n },\n ],\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 4) Diferencias reales en una cuenta exacta o equivalente.\nfor (const payroll of payrollAccounts) {\n if (matchedPayrollKeys.has(payroll.group_key)) continue;\n\n const candidates = getDirectCandidates(\n payroll,\n matchedBankKeys\n );\n\n if (!candidates.length) continue;\n\n const candidate = candidates[0];\n const bank = candidate.bank;\n const decision = candidate.supplementDecision;\n const sources = buildSources(payroll, decision.selected);\n const difference = moneyDiff(\n decision.effectiveAmount,\n bank.amount\n );\n\n matchedPayrollKeys.add(payroll.group_key);\n matchedBankKeys.add(bank.group_key);\n registerSupplements(decision.selected);\n\n rows.push({\n id: `difference_${payroll.group_key}_${bank.group_key}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: payroll.currency,\n payrollAmount: decision.effectiveAmount,\n payroll_amount: decision.effectiveAmount,\n payrollBaseAmount: decision.baseAmount,\n payroll_base_amount: decision.baseAmount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference,\n status: 'Riesgo',\n category: 'discrepancia',\n subcategory: 'diferencia_monto',\n observation:\n `Diferencia de ${payroll.currency} ` +\n `${formatMoney(difference)}.`,\n applied_supplements: decision.selected,\n source_sheets: sources.sourceSheets,\n source_rows: sources.sourceRows,\n bank_source_rows: bank.source_rows,\n });\n}\n\n\n// 4.5) Reconciliación final exacta de pares residuales.\n//\n// Este paso corrige casos en los que nómina y banco contienen:\n// - la misma cuenta normalizada;\n// - el mismo empleado;\n// - el mismo monto;\n// pero no fueron enlazados en los pasos anteriores por diferencias técnicas\n// de agrupación, moneda inferida o metadatos del CSV.\n//\n// Es deliberadamente conservador: exige una única contraparte bancaria.\nfor (const payroll of payrollAccounts) {\n if (matchedPayrollKeys.has(payroll.group_key)) continue;\n\n const candidates = bankAccounts\n .filter((bank) => {\n if (matchedBankKeys.has(bank.group_key)) return false;\n\n const payrollAccount = normalizeAccount(payroll.account);\n const bankAccount = normalizeAccount(bank.account);\n\n if (!payrollAccount || payrollAccount !== bankAccount) {\n return false;\n }\n\n if (!bankMatchesName(bank, payroll.employee_name)) {\n return false;\n }\n\n const decision = chooseConditionalSupplements(payroll, bank);\n\n return moneyEquals(\n decision.effectiveAmount,\n bank.amount\n );\n })\n .map((bank) => ({\n bank,\n supplementDecision: chooseConditionalSupplements(\n payroll,\n bank\n ),\n }));\n\n if (candidates.length !== 1) continue;\n\n const candidate = candidates[0];\n const bank = candidate.bank;\n const decision = candidate.supplementDecision;\n const sources = buildSources(\n payroll,\n decision.selected\n );\n\n matchedPayrollKeys.add(payroll.group_key);\n matchedBankKeys.add(bank.group_key);\n registerSupplements(decision.selected);\n\n finalExactReconciliations.push({\n employee_name: payroll.employee_name,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payroll_currency: payroll.currency,\n bank_currency: bank.currency,\n payroll_amount: decision.effectiveAmount,\n bank_amount: bank.amount,\n payroll_group_key: payroll.group_key,\n bank_group_key: bank.group_key,\n });\n\n rows.push({\n id: `final_exact_match_${payroll.group_key}_${bank.group_key}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: bank.currency || payroll.currency,\n payrollAmount: decision.effectiveAmount,\n payroll_amount: decision.effectiveAmount,\n payrollBaseAmount: decision.baseAmount,\n payroll_base_amount: decision.baseAmount,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: 0,\n status: 'Coincidencia',\n category: 'coincidencia',\n subcategory: 'reconciliacion_final_cuenta_nombre_monto',\n observation:\n 'Conciliado por cuenta, nombre y monto en la validación final.',\n applied_supplements: decision.selected,\n source_sheets: sources.sourceSheets,\n source_rows: sources.sourceRows,\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 5) Nómina con cuenta sin pago bancario.\nfor (const payroll of payrollAccounts) {\n if (matchedPayrollKeys.has(payroll.group_key)) continue;\n\n rows.push({\n id: `payroll_without_bank_${payroll.group_key}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: payroll.account,\n payrollAccount: payroll.account,\n payroll_account: payroll.account,\n bankAccount: '',\n bank_account: '',\n currency: payroll.currency,\n payrollAmount: payroll.payroll_amount,\n payroll_amount: payroll.payroll_amount,\n payrollBaseAmount: payroll.payroll_amount,\n payroll_base_amount: payroll.payroll_amount,\n bankAmount: 0,\n bank_amount: 0,\n difference: payroll.payroll_amount,\n status: 'Riesgo',\n category: 'discrepancia',\n subcategory: 'nomina_con_cuenta_sin_pago_banco',\n observation:\n 'Está en nómina, pero no aparece pagado en el banco.',\n applied_supplements: [],\n source_sheets: payroll.source_sheets,\n source_rows: payroll.source_rows,\n });\n}\n\n// 6) Banco sin nómina.\nfor (const bank of bankAccounts) {\n if (matchedBankKeys.has(bank.group_key)) continue;\n\n rows.push({\n id: `bank_without_payroll_${bank.group_key}`,\n employee:\n bestBankDisplayName(bank) || 'Pago bancario sin nómina',\n employee_name:\n bestBankDisplayName(bank) || 'Pago bancario sin nómina',\n employeeNumber: '',\n employee_number: '',\n account: bank.account,\n payrollAccount: '',\n payroll_account: '',\n bankAccount: bank.account,\n bank_account: bank.account,\n currency: bank.currency,\n payrollAmount: 0,\n payroll_amount: 0,\n bankAmount: bank.amount,\n bank_amount: bank.amount,\n difference: roundMoney(0 - bank.amount),\n status: 'Pendiente revisión',\n category: 'banco_sin_nomina',\n subcategory: 'pago_banco_sin_fila_nomina',\n observation:\n 'Recibió un pago en el banco, pero no aparece en la nómina cargada.',\n bank_source_rows: bank.source_rows,\n });\n}\n\n// 7) Nómina sin cuenta que no pudo conciliarse.\nfor (\n let index = 0;\n index < payrollNoAccountRows.length;\n index++\n) {\n if (matchedNoAccountIndexes.has(index)) continue;\n\n const payroll = payrollNoAccountRows[index];\n\n rows.push({\n id:\n `payroll_without_account_` +\n `${payroll.source_sheet}_${payroll.row_number}`,\n employee: payroll.employee_name,\n employee_name: payroll.employee_name,\n employeeNumber: payroll.employee_number,\n employee_number: payroll.employee_number,\n account: '',\n payrollAccount: '',\n payroll_account: '',\n bankAccount: '',\n bank_account: '',\n currency: payroll.currency,\n payrollAmount: payroll.payroll_amount,\n payroll_amount: payroll.payroll_amount,\n bankAmount: 0,\n bank_amount: 0,\n difference: payroll.payroll_amount,\n status: 'Pendiente revisión',\n category: 'nomina_sin_cuenta',\n subcategory: 'nomina_sin_cuenta_bancaria',\n observation:\n 'Tiene monto en nómina, pero no tiene una cuenta bancaria válida para cruzar contra el banco.',\n source_sheet: payroll.source_sheet,\n row_number: payroll.row_number,\n });\n}\n\n// 8) Consolidar el mismo empleado cuando aparece con dos cuentas de nómina.\nconst originalRows = [...rows];\nconst usedRowIds = new Set();\nconst consolidatedRows = [];\n\nfor (const differenceRow of originalRows) {\n if (\n differenceRow.category !== 'discrepancia' ||\n differenceRow.subcategory !== 'diferencia_monto' ||\n usedRowIds.has(differenceRow.id)\n ) {\n continue;\n }\n\n const extraPayrollRow = originalRows.find((candidate) => {\n if (\n candidate.id === differenceRow.id ||\n usedRowIds.has(candidate.id) ||\n candidate.subcategory !==\n 'nomina_con_cuenta_sin_pago_banco' ||\n candidate.currency !== differenceRow.currency\n ) {\n return false;\n }\n\n const samePerson = samePersonName(\n differenceRow.employee_name || differenceRow.employee,\n candidate.employee_name || candidate.employee\n );\n\n const similarAccounts =\n accountDistance(\n differenceRow.account,\n candidate.account\n ) <= 2;\n\n const combinedPayroll = roundMoney(\n differenceRow.payroll_amount +\n candidate.payroll_amount\n );\n\n const totalMatches = moneyEquals(\n combinedPayroll,\n differenceRow.bank_amount\n );\n\n return samePerson && similarAccounts && totalMatches;\n });\n\n if (!extraPayrollRow) continue;\n\n usedRowIds.add(differenceRow.id);\n usedRowIds.add(extraPayrollRow.id);\n\n const totalPayroll = roundMoney(\n differenceRow.payroll_amount +\n extraPayrollRow.payroll_amount\n );\n\n const accounts = Array.from(new Set([\n differenceRow.account,\n extraPayrollRow.account,\n ].filter(Boolean)));\n\n consolidatedRows.push({\n id:\n `split_account_` +\n `${differenceRow.account}_${extraPayrollRow.account}`,\n employee: differenceRow.employee_name,\n employee_name: differenceRow.employee_name,\n employeeNumber:\n differenceRow.employee_number ||\n extraPayrollRow.employee_number ||\n '',\n employee_number:\n differenceRow.employee_number ||\n extraPayrollRow.employee_number ||\n '',\n account:\n differenceRow.bank_account ||\n differenceRow.account,\n payrollAccount: accounts.join(' / '),\n payroll_account: accounts.join(' / '),\n bankAccount: differenceRow.bank_account,\n bank_account: differenceRow.bank_account,\n currency: differenceRow.currency,\n payrollAmount: totalPayroll,\n payroll_amount: totalPayroll,\n bankAmount: differenceRow.bank_amount,\n bank_amount: differenceRow.bank_amount,\n difference: moneyDiff(\n totalPayroll,\n differenceRow.bank_amount\n ),\n status: 'Riesgo',\n category: 'posible_cuenta_mal_digitada',\n subcategory:\n 'mismo_empleado_con_cuentas_distintas_en_nomina',\n observation:\n `El total de nómina coincide con el banco, pero el empleado ` +\n `aparece con cuentas distintas en la nómina: ` +\n `${accounts.join(' y ')}. La cuenta utilizada por el banco ` +\n `fue ${differenceRow.bank_account}.`,\n applied_supplements:\n differenceRow.applied_supplements || [],\n source_sheets: Array.from(new Set([\n ...(differenceRow.source_sheets || []),\n ...(extraPayrollRow.source_sheets || []),\n ])),\n source_rows: [\n ...(differenceRow.source_rows || []),\n ...(extraPayrollRow.source_rows || []),\n ],\n bank_source_rows:\n differenceRow.bank_source_rows || [],\n });\n}\n\nconst coreRows = [\n ...originalRows.filter(\n (row) => !usedRowIds.has(row.id)\n ),\n ...consolidatedRows,\n];\n\nconst coreCoincidencias = coreRows.filter(\n (row) => row.category === 'coincidencia'\n).length;\n\nconst coreDiscrepancias = coreRows.filter(\n (row) => row.category === 'discrepancia'\n).length;\n\nconst coreBancoSinNomina = coreRows.filter(\n (row) => row.category === 'banco_sin_nomina'\n).length;\n\nconst coreNominaSinCuenta = coreRows.filter(\n (row) => row.category === 'nomina_sin_cuenta'\n).length;\n\nconst corePosiblesCuentas = coreRows.filter(\n (row) => row.category === 'posible_cuenta_mal_digitada'\n).length;\n\nconst linkedPayrollNamesByBankRow = new Map();\nconst linkedPayrollNumbersByBankRow = new Map();\n\nfor (const reconciliationRow of coreRows) {\n const linkedName =\n reconciliationRow.employee_name ||\n reconciliationRow.employee ||\n '';\n const linkedEmployeeNumber = normalizeAccount(\n reconciliationRow.employee_number ||\n reconciliationRow.employeeNumber ||\n ''\n );\n\n for (\n const bankSourceRow of\n reconciliationRow.bank_source_rows || []\n ) {\n const rowKey = bankRowKey(bankSourceRow);\n\n const names =\n linkedPayrollNamesByBankRow.get(rowKey) || [];\n const numbers =\n linkedPayrollNumbersByBankRow.get(rowKey) || [];\n\n if (linkedName) names.push(linkedName);\n if (linkedEmployeeNumber.length >= 6) {\n numbers.push(linkedEmployeeNumber);\n }\n\n linkedPayrollNamesByBankRow.set(\n rowKey,\n Array.from(new Set(names))\n );\n linkedPayrollNumbersByBankRow.set(\n rowKey,\n Array.from(new Set(numbers))\n );\n }\n}\n\nconst bambooMatchDetails = [];\nconst bambooExcludedPayments = [];\nconst bankWithoutBambooMap = new Map();\n\nif (bambooValidationAvailable) {\nfor (const bankRow of bankDetailRows) {\n if (isClearlyNonEmployeePayment(bankRow)) {\n bambooExcludedPayments.push({\n source_file: bankRow.source_file,\n row_number: bankRow.row_number,\n reason: 'pago_no_empleado_identificado',\n bank_name_file: bankRow.bank_name_file,\n bank_account_holder:\n bankRow.bank_account_holder,\n amount: bankRow.amount,\n currency: bankRow.currency,\n });\n continue;\n }\n\n const match = findBambooMatch(bankRow);\n\n if (match.found) {\n bambooMatchDetails.push({\n source_file: bankRow.source_file,\n row_number: bankRow.row_number,\n account: bankRow.account,\n amount: bankRow.amount,\n currency: bankRow.currency,\n bank_name_file: bankRow.bank_name_file,\n bank_account_holder:\n bankRow.bank_account_holder,\n matched_by: match.matched_by,\n confidence: roundMoney(match.confidence),\n bamboo_employee_number:\n match.employee?.employee_number || '',\n bamboo_employee_name:\n match.employee?.full_name || '',\n bamboo_status:\n match.employee?.status || '',\n bamboo_country:\n match.employee?.country || '',\n bamboo_location:\n match.employee?.location || '',\n bamboo_validation_scope:\n match.employee?.validation_scope || '',\n bamboo_overlaps_period:\n Boolean(match.employee?.overlaps_period),\n });\n continue;\n }\n\n const displayName =\n bankRow.bank_name_file ||\n bankRow.bank_account_holder ||\n 'Pago bancario sin empleado identificado';\n\n const groupingKey = [\n normalizeAccount(bankRow.account),\n normalizeName(displayName),\n bankRow.currency || 'TTD',\n ].join('|');\n\n const current =\n bankWithoutBambooMap.get(groupingKey) || {\n id: `bank_without_bamboo_${groupingKey}`,\n employee: displayName,\n employee_name: displayName,\n bank_name_file:\n bankRow.bank_name_file || '',\n bank_account_holder:\n bankRow.bank_account_holder || '',\n account: normalizeAccount(bankRow.account),\n bankAccount: normalizeAccount(bankRow.account),\n bank_account: normalizeAccount(bankRow.account),\n currency: bankRow.currency || 'TTD',\n bankAmount: 0,\n bank_amount: 0,\n shipment_numbers: new Set(),\n references: new Set(),\n source_files: new Set(),\n source_rows: [],\n status: 'Pendiente revisión',\n category: 'banco_sin_bamboo',\n subcategory:\n 'pago_bancario_sin_empleado_bamboohr_tt',\n observation:\n 'Se encontró un pago en el banco, pero no se encontró una coincidencia confiable con un empleado de Trinidad y Tobago en BambooHR.',\n best_bamboo_candidate:\n match.best_candidate\n ? {\n employee_number:\n match.best_candidate.employee\n ?.employee_number || '',\n employee_name:\n match.best_candidate.employee\n ?.full_name || '',\n score: roundMoney(\n match.best_candidate.score\n ),\n }\n : null,\n ambiguous_bamboo_match:\n Boolean(match.ambiguous),\n };\n\n current.bankAmount = roundMoney(\n current.bankAmount +\n Number(bankRow.amount || 0)\n );\n current.bank_amount = current.bankAmount;\n\n if (bankRow.shipment_number) {\n current.shipment_numbers.add(\n bankRow.shipment_number\n );\n }\n\n if (bankRow.reference) {\n current.references.add(bankRow.reference);\n }\n\n if (bankRow.source_file) {\n current.source_files.add(\n bankRow.source_file\n );\n }\n\n current.source_rows.push(bankRow);\n bankWithoutBambooMap.set(\n groupingKey,\n current\n );\n}\n}\n\nconst bankWithoutBamboo = Array.from(\n bankWithoutBambooMap.values()\n).map((row) => ({\n ...row,\n shipment_numbers: Array.from(\n row.shipment_numbers\n ),\n references: Array.from(row.references),\n source_files: Array.from(row.source_files),\n difference: roundMoney(\n 0 - row.bank_amount\n ),\n}));\n\nconst nameDifferenceMap = new Map();\n\nfor (const reconciliationRow of coreRows) {\n const payrollName = String(\n reconciliationRow.employee_name ||\n reconciliationRow.employee ||\n ''\n ).trim();\n\n if (!payrollName) continue;\n\n for (\n const bankSourceRow of\n reconciliationRow.bank_source_rows || []\n ) {\n const bankName = String(\n bankSourceRow.bank_name_file ||\n bankSourceRow.participant_name ||\n bankSourceRow.bank_account_holder ||\n ''\n ).trim();\n\n if (\n !bankName ||\n samePersonName(payrollName, bankName)\n ) {\n continue;\n }\n\n const account = normalizeAccount(\n bankSourceRow.account ||\n reconciliationRow.bank_account ||\n reconciliationRow.bankAccount ||\n reconciliationRow.account ||\n ''\n );\n\n const key = [\n normalizeName(payrollName),\n normalizeName(bankName),\n account,\n bankSourceRow.source_file || '',\n bankSourceRow.row_number || '',\n ].join('|');\n\n if (nameDifferenceMap.has(key)) {\n continue;\n }\n\n nameDifferenceMap.set(key, {\n id: `bank_name_difference_${key}`,\n employee: payrollName,\n employee_name: payrollName,\n payroll_name: payrollName,\n bank_name: bankName,\n employeeNumber:\n reconciliationRow.employee_number ||\n reconciliationRow.employeeNumber ||\n '',\n employee_number:\n reconciliationRow.employee_number ||\n reconciliationRow.employeeNumber ||\n '',\n account,\n payrollAccount:\n reconciliationRow.payroll_account ||\n reconciliationRow.payrollAccount ||\n '',\n payroll_account:\n reconciliationRow.payroll_account ||\n reconciliationRow.payrollAccount ||\n '',\n bankAccount: account,\n bank_account: account,\n currency:\n bankSourceRow.currency ||\n reconciliationRow.currency ||\n 'TTD',\n payrollAmount:\n reconciliationRow.payroll_amount ||\n reconciliationRow.payrollAmount ||\n 0,\n payroll_amount:\n reconciliationRow.payroll_amount ||\n reconciliationRow.payrollAmount ||\n 0,\n bankAmount:\n bankSourceRow.amount || 0,\n bank_amount:\n bankSourceRow.amount || 0,\n difference: 0,\n status: 'Pendiente revisión',\n category: 'diferencia_nombre_banco',\n subcategory:\n 'nombre_nomina_vs_participante_banco',\n observation:\n `El nombre registrado en la nómina (${payrollName}) ` +\n `es diferente al nombre enviado al banco (${bankName}).`,\n bank_name_file: payrollName,\n bank_account_holder: bankName,\n source_file:\n bankSourceRow.source_file || '',\n financial_institution_id:\n bankSourceRow.financial_institution_id || '',\n reference:\n bankSourceRow.reference || '',\n row_number:\n bankSourceRow.row_number || '',\n });\n }\n}\n\nconst nameDifferenceRows = Array.from(\n nameDifferenceMap.values()\n);\n\nfunction priority(row) {\n const category = String(\n row.category || ''\n ).toLowerCase();\n\n if (category === 'posible_cuenta_mal_digitada') return 1;\n if (category === 'discrepancia') return 2;\n if (category === 'banco_sin_nomina') return 3;\n if (category === 'nomina_sin_cuenta') return 4;\n if (category === 'diferencia_nombre_banco') return 5;\n if (category === 'coincidencia') return 99;\n\n return 50;\n}\n\nconst rowsFinales = [\n ...coreRows,\n ...nameDifferenceRows,\n].sort((a, b) => {\n const priorityDifference =\n priority(a) - priority(b);\n\n if (priorityDifference !== 0) {\n return priorityDifference;\n }\n\n return String(\n a.employee_name || ''\n ).localeCompare(\n String(b.employee_name || ''),\n 'es'\n );\n});\n\nconst appliedSupplementsTotal = roundMoney(\n appliedSupplements.reduce(\n (sum, row) => sum + row.payroll_amount,\n 0\n )\n);\n\nconst totalNominaBase = roundMoney(\n data.payroll?.total_amount || 0\n);\n\nconst totalNomina = roundMoney(\n totalNominaBase + appliedSupplementsTotal\n);\n\nconst totalBanco = roundMoney(\n data.bank?.total_amount || 0\n);\n\nconst diferenciasNombreBanco =\n nameDifferenceRows.length;\n\nconst pendientes =\n coreDiscrepancias +\n coreBancoSinNomina +\n coreNominaSinCuenta +\n corePosiblesCuentas +\n bankWithoutBamboo.length +\n diferenciasNombreBanco;\n\nconst unusedPotentialSupplements =\n potentialSupplements.filter((row) => {\n return !appliedSupplementKeys.has(\n supplementKey(row)\n );\n });\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'cruce_nomina_tt_banco',\n errors: [],\n metadata: data.metadata || {},\n summary: {\n coincidencias: coreCoincidencias,\n // La tarjeta de la app agrupa todos los casos de discrepancia/riesgo.\n // Se conserva el detalle puro en discrepanciasMontoPago.\n discrepancias:\n coreDiscrepancias + corePosiblesCuentas,\n discrepanciasMontoPago:\n coreDiscrepancias,\n bancoSinNomina: coreBancoSinNomina,\n bancoSinBamboo: bankWithoutBamboo.length,\n nominaSinCuenta: coreNominaSinCuenta,\n diferenciasNombreBanco,\n posiblesCuentasMalDigitadas:\n corePosiblesCuentas,\n totalResultados:\n coreCoincidencias +\n coreDiscrepancias +\n coreBancoSinNomina +\n coreNominaSinCuenta +\n corePosiblesCuentas +\n bankWithoutBamboo.length +\n diferenciasNombreBanco,\n pendientes,\n filasNominaValidas:\n data.payroll?.valid_rows_count || 0,\n filasNominaSinCuenta:\n data.payroll?.no_account_rows_count || 0,\n suplementosPotenciales:\n potentialSupplements.length,\n suplementosNominaAplicados:\n appliedSupplements.length,\n suplementosNominaNoAplicados:\n unusedPotentialSupplements.length,\n suplementosNominaAdjuntados:\n appliedSupplements.length,\n suplementosNominaNoAdjuntados:\n data.payroll?.unattached_supplements_count || 0,\n reconciliacionesExactasFinales:\n finalExactReconciliations.length,\n cuentasNominaAgrupadas:\n payrollAccounts.length,\n transaccionesBanco:\n data.bank?.rows_count || 0,\n cuentasBancoAgrupadas:\n bankAccounts.length,\n empleadosBambooTT:\n Number(\n data.bamboo?.trinidad_tobago_count ||\n bambooEmployees.length\n ),\n empleadosBambooEnPeriodo:\n Number(\n data.bamboo?.active_in_period_count || 0\n ),\n bambooPaginasDescargadas:\n Number(\n data.bamboo?.pages_fetched || 0\n ),\n bambooEmpleadosEsperados:\n Number(\n data.bamboo?.expected_total || 0\n ),\n bambooDescargaCompleta:\n Boolean(\n data.bamboo?.fetch_complete\n ),\n bambooValidacionDisponible:\n bambooValidationAvailable,\n totalNominaBase,\n totalSuplementosAplicados:\n appliedSupplementsTotal,\n totalNomina,\n totalBanco,\n diferenciaTotal:\n moneyDiff(totalNomina, totalBanco),\n },\n rows: rowsFinales,\n bankWithoutBamboo,\n nameDifferences: nameDifferenceRows,\n bambooSummary: data.bamboo || {},\n reportUrl: null,\n debug: {\n sheet_summaries:\n data.payroll?.sheet_summaries || [],\n potential_supplements:\n potentialSupplements,\n applied_supplements:\n appliedSupplements,\n final_exact_reconciliations:\n finalExactReconciliations,\n bamboo_search:\n {\n employees_received:\n rawBambooValidationEmployees.length,\n employees_indexed:\n bambooSearch.records.length,\n employees_excluded:\n excludedBambooValidationEmployees.length,\n excluded_employees:\n excludedBambooValidationEmployees,\n exact_aliases:\n bambooSearch.exactAliasMap.size,\n indexed_tokens:\n bambooSearch.tokenIndex.size,\n cache_entries:\n bambooMatchCache.size,\n },\n bamboo_matches:\n bambooMatchDetails,\n bamboo_excluded_payments:\n bambooExcludedPayments,\n bamboo_validation_available:\n bambooValidationAvailable,\n bamboo_validation_warning:\n bambooValidationWarning,\n banco_sin_bamboo:\n bankWithoutBamboo,\n unused_potential_supplements:\n unusedPotentialSupplements,\n unattached_supplements:\n data.debug_payroll?.unattached_supplements || [],\n payroll_preview:\n payrollAccounts.slice(0, 10),\n bank_preview:\n bankAccounts.slice(0, 10),\n payroll_no_account_preview:\n payrollNoAccountRows.slice(0, 10),\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 6224, - 7296 - ], - "id": "ea4d4d89-a2f9-4c3f-9c53-803706173b27", - "name": "Cruzar Nómina vs Banco" - }, - { - "parameters": { - "jsCode": "const data = $input.first().json || {};\n\nfunction normalizeText(value) {\n return String(value ?? '')\n .replace(/\\uFEFF/g, '')\n .replace(/\\u00A0/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction roundMoney(value) {\n return Math.round(\n (Number(value) || 0) * 100\n ) / 100;\n}\n\nfunction firstValue(value) {\n if (Array.isArray(value)) {\n return value\n .map(normalizeText)\n .filter(Boolean)\n .join(' / ');\n }\n\n return normalizeText(value);\n}\n\nfunction formatPeriodEnd(value) {\n const raw = normalizeText(value);\n\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(raw)) {\n return raw;\n }\n\n const [year, month, day] = raw.split('-');\n\n const monthNames = {\n '01': 'ene',\n '02': 'feb',\n '03': 'mar',\n '04': 'abr',\n '05': 'may',\n '06': 'jun',\n '07': 'jul',\n '08': 'ago',\n '09': 'sep',\n '10': 'oct',\n '11': 'nov',\n '12': 'dic',\n };\n\n return `${day}-${monthNames[month] || month}-${year}`;\n}\n\nfunction mainReportSense(row, difference) {\n const subcategory = normalizeText(\n row.subcategory\n ).toLowerCase();\n\n const payrollAmount = Number(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = Number(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n if (\n subcategory ===\n 'nomina_con_cuenta_sin_pago_banco' ||\n (bankAmount === 0 && payrollAmount > 0)\n ) {\n return 'No aparece pagado en banco';\n }\n\n if (difference > 0) {\n return 'Se pagó de menos';\n }\n\n if (difference < 0) {\n return 'Se pagó de más';\n }\n\n return 'Revisar';\n}\n\nfunction accountValues(value) {\n const values = Array.isArray(value)\n ? value\n : String(value ?? '')\n .split(/\\s*(?:\\/|;|,|\\by\\b)\\s*/i);\n\n return values\n .map((item) =>\n String(item ?? '')\n .replace(/\\u00A0/g, '')\n .replace(/\\.0$/g, '')\n .replace(/\\D/g, '')\n .trim()\n )\n .filter(\n (account) =>\n account.length >= 6 &&\n !/^0+$/.test(account)\n );\n}\n\nfunction payrollAccountsForWrongAccount(row) {\n const candidates = [\n row.payroll_account,\n row.payrollAccount,\n ...(Array.isArray(row.source_rows)\n ? row.source_rows.flatMap(\n (sourceRow) => [\n sourceRow.account,\n sourceRow.payroll_account,\n sourceRow.payrollAccount,\n ]\n )\n : []),\n ];\n\n return Array.from(\n new Set(\n candidates.flatMap(accountValues)\n )\n );\n}\n\nfunction bankAccountForWrongAccount(row) {\n return firstValue(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n );\n}\n\nfunction moneyLabel(value) {\n return Math.abs(\n roundMoney(value)\n ).toLocaleString('en-US', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n}\n\nfunction wrongAccountTotalStatus(row) {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n payrollAmount - bankAmount\n );\n\n if (Math.abs(difference) <= 0.02) {\n return (\n 'El total de nómina coincide con ' +\n 'el total pagado por el banco.'\n );\n }\n\n if (difference > 0) {\n return (\n 'El total de nómina supera el total ' +\n `del banco por TT$${moneyLabel(difference)}.`\n );\n }\n\n return (\n 'El total pagado por el banco supera ' +\n `el total de nómina por TT$${moneyLabel(difference)}.`\n );\n}\n\nfunction wrongAccountFinding(row) {\n const existing = normalizeText(\n row.observation || ''\n );\n\n if (existing) return existing;\n\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n return (\n 'El empleado presenta una posible ' +\n 'inconsistencia entre la cuenta registrada ' +\n `en nómina (${payrollAccounts.join(' y ') || 'sin cuenta identificada'}) ` +\n `y la cuenta utilizada por el banco (${bankAccount || 'sin cuenta identificada'}).`\n );\n}\n\nconst metadata = data.metadata || {};\nconst summary = data.summary || {};\n\nconst rows = Array.isArray(data.rows)\n ? data.rows\n : [];\n\nconst bankWithoutBamboo =\n Array.isArray(data.bankWithoutBamboo)\n ? data.bankWithoutBamboo\n : [];\n\nconst periodLabel =\n metadata.period_label ||\n `${metadata.year || ''}-${metadata.month || ''}-${metadata.period_type || ''}`;\n\nconst periodEndLabel = formatPeriodEnd(\n metadata.period_end || ''\n);\n\nconst spreadsheetTitle =\n `Cruce de Cuentas GLM TT - ${periodLabel}`;\n\nconst sheetIds = {\n nominaVsBanco: 201,\n bancoSinNomina: 202,\n bancoSinBamboo: 203,\n diferenciasNombreBanco: 204,\n cuentaMalDigitada: 205,\n resumen: 206,\n};\n\nconst cuentaMalDigitadaCases = rows.filter(\n (row) =>\n row.category ===\n 'posible_cuenta_mal_digitada'\n);\n\nconst hasCuentaMalDigitada =\n cuentaMalDigitadaCases.length > 0;\n\nconst sheetTitles = {\n nominaVsBanco:\n '01 Nómina vs Banco',\n bancoSinNomina:\n '02 Banco sin Nómina',\n bancoSinBamboo:\n '03 Banco sin Bamboo',\n diferenciasNombreBanco:\n '04 Diferencias de Nombre',\n cuentaMalDigitada:\n '05 Cuenta Mal Digitada',\n resumen: hasCuentaMalDigitada\n ? '06 Resumen'\n : '05 Resumen',\n};\n\nconst mainRows = rows\n .filter(\n (row) =>\n row.category === 'discrepancia'\n )\n .map((row, index) => {\n const payrollAmount = roundMoney(\n row.payroll_amount ??\n row.payrollAmount ??\n 0\n );\n\n const bankAmount = roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n );\n\n const difference = roundMoney(\n row.difference ??\n (payrollAmount - bankAmount)\n );\n\n return [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.payroll_account ||\n row.payrollAccount ||\n row.account ||\n ''\n ),\n payrollAmount,\n bankAmount,\n difference,\n mainReportSense(\n row,\n difference\n ),\n normalizeText(\n row.status || 'Riesgo'\n ).toUpperCase(),\n '',\n ];\n });\n\nconst bancoSinNominaRows = rows\n .filter(\n (row) =>\n row.category ===\n 'banco_sin_nomina'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst bancoSinBambooRows =\n bankWithoutBamboo.map(\n (row, index) => [\n index + 1,\n normalizeText(\n row.bank_name_file ||\n row.employee_name ||\n row.employee ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n firstValue(\n row.source_files ||\n row.source_file ||\n ''\n ),\n 'PENDIENTE REVISIÓN',\n '',\n ]\n );\n\nconst diferenciasNombreRows = rows\n .filter(\n (row) =>\n row.category ===\n 'diferencia_nombre_banco'\n )\n .map((row, index) => [\n index + 1,\n normalizeText(\n row.payroll_name ||\n row.employee_name ||\n row.employee ||\n row.bank_name_file ||\n ''\n ),\n normalizeText(\n row.bank_name ||\n row.bank_account_holder ||\n ''\n ),\n normalizeText(\n row.bank_account ||\n row.bankAccount ||\n row.account ||\n ''\n ),\n roundMoney(\n row.bank_amount ??\n row.bankAmount ??\n 0\n ),\n normalizeText(\n row.status ||\n 'Pendiente revisión'\n ).toUpperCase(),\n normalizeText(\n row.observation || ''\n ),\n '',\n ]);\n\nconst cuentaMalDigitadaRows = [];\n\ncuentaMalDigitadaCases.forEach(\n (row, index) => {\n const payrollAccounts =\n payrollAccountsForWrongAccount(row);\n\n const bankAccount =\n bankAccountForWrongAccount(row);\n\n const fields = [\n [\n 'Empleado',\n normalizeText(\n row.employee_name ||\n row.employee ||\n ''\n ),\n ],\n [\n 'Cuentas registradas en las hojas de nómina',\n payrollAccounts.join(' y ') ||\n 'No se identificó una cuenta válida en la nómina.',\n ],\n [\n 'Cuenta utilizada por el banco',\n bankAccount ||\n 'No se identificó una cuenta válida en el banco.',\n ],\n [\n 'Estado del total',\n wrongAccountTotalStatus(row),\n ],\n [\n 'Hallazgo',\n wrongAccountFinding(row),\n ],\n [\n 'Clasificación',\n 'Posible cuenta mal digitada — revisar y unificar la cuenta registrada en nómina.',\n ],\n ];\n\n fields.forEach(\n (field, fieldIndex) => {\n cuentaMalDigitadaRows.push([\n fieldIndex === 0\n ? index + 1\n : '',\n field[0],\n field[1],\n '',\n ]);\n }\n );\n }\n);\n\nconst resumenRows = [\n ['Período', periodLabel],\n [\n 'Coincidencias',\n Number(summary.coincidencias || 0),\n ],\n [\n 'Discrepancias de monto o pago',\n Number(\n summary.discrepanciasMontoPago ??\n summary.discrepancias ??\n 0\n ),\n ],\n [\n 'Banco sin nómina',\n Number(summary.bancoSinNomina || 0),\n ],\n [\n 'Banco sin Bamboo',\n Number(summary.bancoSinBamboo || 0),\n ],\n [\n 'Nómina sin cuenta no conciliada',\n Number(summary.nominaSinCuenta || 0),\n ],\n [\n 'Diferencias de nombre',\n Number(\n summary.diferenciasNombreBanco || 0\n ),\n ],\n [\n 'Posibles cuentas mal digitadas',\n Number(\n summary.posiblesCuentasMalDigitadas || 0\n ),\n ],\n [\n 'Pendientes del cruce principal',\n Number(summary.pendientes || 0),\n ],\n [\n 'Empleados BambooHR Trinidad y Tobago',\n Number(summary.empleadosBambooTT || 0),\n ],\n [\n 'Empleados BambooHR en el período',\n Number(\n summary.empleadosBambooEnPeriodo || 0\n ),\n ],\n [\n 'Filas válidas de nómina',\n Number(\n summary.filasNominaValidas || 0\n ),\n ],\n [\n 'Filas de nómina sin cuenta detectadas',\n Number(\n summary.filasNominaSinCuenta || 0\n ),\n ],\n [\n 'Transacciones bancarias',\n Number(\n summary.transaccionesBanco || 0\n ),\n ],\n [\n 'Total nómina',\n roundMoney(summary.totalNomina || 0),\n ],\n [\n 'Total banco',\n roundMoney(summary.totalBanco || 0),\n ],\n [\n 'Diferencia total',\n roundMoney(\n summary.diferenciaTotal || 0\n ),\n ],\n];\n\nfunction reportValues(\n title,\n subtitle,\n header,\n body\n) {\n return [\n [\n title,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n [\n subtitle,\n ...Array(\n Math.max(header.length - 1, 0)\n ).fill(''),\n ],\n Array(header.length).fill(''),\n header,\n ...body,\n ];\n}\n\nconst nominaVsBancoValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de Monto Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Empleado',\n 'Cuenta',\n 'Monto en Nómina (TT$)',\n 'Monto en Banco (TT$)',\n 'Diferencia (TT$)',\n 'Sentido',\n 'Estado',\n 'Resolución',\n ],\n mainRows\n );\n\nconst bancoSinNominaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos bancarios sin registro en la nómina · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n bancoSinNominaRows\n );\n\nconst bancoSinBambooValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Pagos en banco sin empleado identificado en BambooHR · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Archivo',\n 'Estado',\n 'Resolución',\n ],\n bancoSinBambooRows\n );\n\nconst diferenciasNombreValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Diferencias de nombre entre nómina y banco · Trinidad y Tobago · ${periodEndLabel}`,\n [\n '#',\n 'Nombre en nómina',\n 'Nombre en banco',\n 'Cuenta',\n 'Monto en banco (TT$)',\n 'Estado',\n 'Observación',\n 'Resolución',\n ],\n diferenciasNombreRows\n );\n\nconst cuentaMalDigitadaValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Cuenta Mal Digitada en Nómina · Trinidad y Tobago · ${periodEndLabel}`,\n ['#', 'Campo', 'Detalle', 'Resolución'],\n cuentaMalDigitadaRows\n );\n\nconst resumenValues =\n reportValues(\n 'GOMEZLEE MARKETING',\n `Resumen del cruce Nómina vs. Banco · Trinidad y Tobago · ${periodEndLabel}`,\n ['Indicador', 'Valor'],\n resumenRows\n );\n\nconst valueData = [\n {\n range:\n `'${sheetTitles.nominaVsBanco}'!A1:I`,\n values: nominaVsBancoValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinNomina}'!A1:H`,\n values: bancoSinNominaValues,\n },\n {\n range:\n `'${sheetTitles.bancoSinBamboo}'!A1:G`,\n values: bancoSinBambooValues,\n },\n {\n range:\n `'${sheetTitles.diferenciasNombreBanco}'!A1:H`,\n values: diferenciasNombreValues,\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n range:\n `'${sheetTitles.cuentaMalDigitada}'!A1:D`,\n values:\n cuentaMalDigitadaValues,\n },\n ]\n : []),\n {\n range:\n `'${sheetTitles.resumen}'!A1:B`,\n values: resumenValues,\n },\n];\n\nconst brandColor = {\n red: 0.29,\n green: 0.49,\n blue: 0.58,\n};\n\nconst whiteColor = {\n red: 1,\n green: 1,\n blue: 1,\n};\n\nconst borderColor = {\n red: 0.82,\n green: 0.86,\n blue: 0.88,\n};\n\nfunction mergeRow(\n sheetId,\n rowIndex,\n columnCount\n) {\n return {\n mergeCells: {\n range: {\n sheetId,\n startRowIndex: rowIndex,\n endRowIndex: rowIndex + 1,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n mergeType: 'MERGE_ALL',\n },\n };\n}\n\nfunction formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n userEnteredFormat\n) {\n const formatFields =\n Object.keys(userEnteredFormat || {});\n\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat,\n },\n fields:\n `userEnteredFormat(${formatFields.join(',')})`,\n },\n };\n}\n\nfunction titleFormat(\n sheetId,\n rowIndex,\n columnCount,\n options = {}\n) {\n return formatRange(\n sheetId,\n rowIndex,\n rowIndex + 1,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: options.bold ?? true,\n italic:\n options.italic ?? false,\n fontSize:\n options.fontSize ?? 12,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'LEFT',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction headerFormat(\n sheetId,\n columnCount\n) {\n return formatRange(\n sheetId,\n 3,\n 4,\n 0,\n columnCount,\n {\n backgroundColor: brandColor,\n textFormat: {\n bold: true,\n foregroundColor:\n whiteColor,\n },\n horizontalAlignment: 'CENTER',\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction freezeRows(\n sheetId,\n count\n) {\n return {\n updateSheetProperties: {\n properties: {\n sheetId,\n gridProperties: {\n frozenRowCount: count,\n },\n },\n fields:\n 'gridProperties.frozenRowCount',\n },\n };\n}\n\nfunction setFilter(\n sheetId,\n columnCount,\n endRowIndex\n) {\n return {\n setBasicFilter: {\n filter: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex:\n columnCount,\n },\n },\n },\n };\n}\n\nfunction setColumnWidth(\n sheetId,\n index,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'COLUMNS',\n startIndex: index,\n endIndex: index + 1,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction setRowHeight(\n sheetId,\n startIndex,\n endIndex,\n pixelSize\n) {\n return {\n updateDimensionProperties: {\n range: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n properties: {\n pixelSize,\n },\n fields: 'pixelSize',\n },\n };\n}\n\nfunction borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n) {\n const border = {\n style: 'SOLID',\n color: borderColor,\n };\n\n return [\n formatRange(\n sheetId,\n 3,\n endRowIndex,\n 0,\n columnCount,\n {\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n {\n updateBorders: {\n range: {\n sheetId,\n startRowIndex: 3,\n endRowIndex,\n startColumnIndex: 0,\n endColumnIndex: columnCount,\n },\n top: border,\n bottom: border,\n left: border,\n right: border,\n innerHorizontal: border,\n innerVertical: border,\n },\n },\n ];\n}\n\nfunction moneyFormat(\n sheetId,\n startColumnIndex,\n endColumnIndex,\n startRowIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n {\n numberFormat: {\n type: 'NUMBER',\n pattern:\n '\"TT$\"#,##0.00',\n },\n horizontalAlignment:\n 'RIGHT',\n verticalAlignment:\n 'MIDDLE',\n }\n );\n}\n\nfunction statusFormat(\n sheetId,\n columnIndex,\n endRowIndex\n) {\n return formatRange(\n sheetId,\n 4,\n endRowIndex,\n columnIndex,\n columnIndex + 1,\n {\n backgroundColor: {\n red: 1,\n green: 0.92,\n blue: 0.92,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.82,\n green: 0.08,\n blue: 0.08,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n );\n}\n\nfunction conditionalDifference(\n sheetId,\n endRowIndex,\n formula,\n backgroundColor,\n textColor\n) {\n return {\n addConditionalFormatRule: {\n rule: {\n ranges: [\n {\n sheetId,\n startRowIndex: 4,\n endRowIndex,\n startColumnIndex: 5,\n endColumnIndex: 6,\n },\n ],\n booleanRule: {\n condition: {\n type: 'CUSTOM_FORMULA',\n values: [\n {\n userEnteredValue:\n formula,\n },\n ],\n },\n format: {\n backgroundColor,\n textFormat: {\n bold: true,\n foregroundColor:\n textColor,\n },\n },\n },\n },\n index: 0,\n },\n };\n}\n\nfunction styleReport(config) {\n const {\n sheetId,\n columnCount,\n bodyRowsCount,\n widths,\n moneyColumns = [],\n statusColumn = null,\n } = config;\n\n const endRowIndex = Math.max(\n 4 + bodyRowsCount,\n 4\n );\n\n const requests = [\n mergeRow(\n sheetId,\n 0,\n columnCount\n ),\n mergeRow(\n sheetId,\n 1,\n columnCount\n ),\n titleFormat(\n sheetId,\n 0,\n columnCount,\n {\n fontSize: 12,\n bold: true,\n }\n ),\n titleFormat(\n sheetId,\n 1,\n columnCount,\n {\n fontSize: 10,\n bold: false,\n italic: true,\n }\n ),\n headerFormat(\n sheetId,\n columnCount\n ),\n freezeRows(sheetId, 4),\n setFilter(\n sheetId,\n columnCount,\n endRowIndex\n ),\n ...borderFormat(\n sheetId,\n columnCount,\n endRowIndex\n ),\n setRowHeight(\n sheetId,\n 0,\n 1,\n 30\n ),\n setRowHeight(\n sheetId,\n 1,\n 2,\n 28\n ),\n setRowHeight(\n sheetId,\n 3,\n 4,\n 42\n ),\n ...widths.map(\n (width, index) =>\n setColumnWidth(\n sheetId,\n index,\n width\n )\n ),\n ];\n\n if (bodyRowsCount > 0) {\n requests.push(\n setRowHeight(\n sheetId,\n 4,\n endRowIndex,\n 30\n )\n );\n\n for (\n const [startColumn, endColumn] of\n moneyColumns\n ) {\n requests.push(\n moneyFormat(\n sheetId,\n startColumn,\n endColumn,\n 4,\n endRowIndex\n )\n );\n }\n\n if (\n Number.isInteger(\n statusColumn\n )\n ) {\n requests.push(\n statusFormat(\n sheetId,\n statusColumn,\n endRowIndex\n )\n );\n }\n }\n\n return requests;\n}\n\n\nfunction wrapRangeRequest(\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex\n) {\n return {\n repeatCell: {\n range: {\n sheetId,\n startRowIndex,\n endRowIndex,\n startColumnIndex,\n endColumnIndex,\n },\n cell: {\n userEnteredFormat: {\n verticalAlignment: 'MIDDLE',\n wrapStrategy: 'WRAP',\n },\n },\n fields:\n 'userEnteredFormat(verticalAlignment,wrapStrategy)',\n },\n };\n}\n\nfunction autoResizeRowsRequest(\n sheetId,\n startIndex,\n endIndex\n) {\n return {\n autoResizeDimensions: {\n dimensions: {\n sheetId,\n dimension: 'ROWS',\n startIndex,\n endIndex,\n },\n },\n };\n}\n\nconst formatRequests = [\n ...styleReport({\n sheetId:\n sheetIds.nominaVsBanco,\n columnCount: 9,\n bodyRowsCount:\n mainRows.length,\n widths: [\n 48,\n 250,\n 145,\n 135,\n 135,\n 135,\n 180,\n 120,\n 260,\n ],\n moneyColumns: [\n [3, 6],\n ],\n statusColumn: 7,\n }),\n\n ...(mainRows.length > 0\n ? [\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5>0',\n {\n red: 1,\n green: 0.97,\n blue: 0.82,\n },\n {\n red: 0.45,\n green: 0.27,\n blue: 0,\n }\n ),\n conditionalDifference(\n sheetIds.nominaVsBanco,\n 4 + mainRows.length,\n '=$F5<0',\n {\n red: 1,\n green: 0.89,\n blue: 0.89,\n },\n {\n red: 0.85,\n green: 0.05,\n blue: 0.05,\n }\n ),\n ]\n : []),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinNomina,\n columnCount: 8,\n bodyRowsCount:\n bancoSinNominaRows.length,\n widths: [\n 48,\n 230,\n 145,\n 135,\n 230,\n 140,\n 360,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.bancoSinBamboo,\n columnCount: 7,\n bodyRowsCount:\n bancoSinBambooRows.length,\n widths: [\n 48,\n 250,\n 145,\n 140,\n 250,\n 150,\n 260,\n ],\n moneyColumns: [[3, 4]],\n statusColumn: 5,\n }),\n\n ...styleReport({\n sheetId:\n sheetIds.diferenciasNombreBanco,\n columnCount: 8,\n bodyRowsCount:\n diferenciasNombreRows.length,\n widths: [\n 48,\n 240,\n 240,\n 145,\n 140,\n 150,\n 420,\n 260,\n ],\n moneyColumns: [[4, 5]],\n statusColumn: 5,\n }),\n];\n\nif (hasCuentaMalDigitada) {\n const endRowIndex =\n 4 +\n cuentaMalDigitadaRows.length;\n\n formatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.cuentaMalDigitada,\n columnCount: 4,\n bodyRowsCount:\n cuentaMalDigitadaRows.length,\n widths: [\n 48,\n 300,\n 520,\n 260,\n ],\n statusColumn: null,\n })\n );\n\n cuentaMalDigitadaCases.forEach(\n (_, caseIndex) => {\n const startRowIndex =\n 4 + caseIndex * 6;\n\n const endCaseRowIndex =\n startRowIndex + 6;\n\n formatRequests.push(\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 0,\n endColumnIndex: 1,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n {\n mergeCells: {\n range: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endRowIndex:\n endCaseRowIndex,\n startColumnIndex: 3,\n endColumnIndex: 4,\n },\n mergeType:\n 'MERGE_ALL',\n },\n },\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 0,\n 1,\n {\n backgroundColor: {\n red: 0.91,\n green: 0.95,\n blue: 0.99,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'CENTER',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 1,\n 2,\n {\n backgroundColor: {\n red: 0.93,\n green: 0.97,\n blue: 0.90,\n },\n textFormat: {\n bold: true,\n foregroundColor: {\n red: 0.20,\n green: 0.36,\n blue: 0.45,\n },\n },\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n formatRange(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 2,\n 3,\n {\n horizontalAlignment:\n 'LEFT',\n verticalAlignment:\n 'MIDDLE',\n wrapStrategy: 'WRAP',\n }\n ),\n setRowHeight(\n sheetIds.cuentaMalDigitada,\n startRowIndex,\n endCaseRowIndex,\n 52\n )\n );\n }\n );\n}\n\nconst resumenEndRowIndex =\n 4 + resumenRows.length;\n\nformatRequests.push(\n ...styleReport({\n sheetId:\n sheetIds.resumen,\n columnCount: 2,\n bodyRowsCount:\n resumenRows.length,\n widths: [340, 180],\n statusColumn: null,\n }),\n moneyFormat(\n sheetIds.resumen,\n 1,\n 2,\n resumenEndRowIndex - 3,\n resumenEndRowIndex\n )\n);\n\n\nconst mainReadabilityEndRow =\n 4 + mainRows.length;\n\nconst bancoSinNominaReadabilityEndRow =\n 4 + bancoSinNominaRows.length;\n\nconst bancoSinBambooReadabilityEndRow =\n 4 + bancoSinBambooRows.length;\n\nconst diferenciasNombreReadabilityEndRow =\n 4 + diferenciasNombreRows.length;\n\nconst resumenReadabilityEndRow =\n 4 + resumenRows.length;\n\n/*\n * Ajuste final de legibilidad.\n *\n * Los anchos definitivos se aplican antes del autoajuste vertical. De esta\n * forma Google Sheets calcula la altura real de cada fila después de envolver\n * el texto, evitando contenido cortado en nombres, observaciones y resúmenes.\n */\nformatRequests.push(\n // 01 Nómina vs Banco\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 1,\n 300\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 2,\n 150\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 3,\n 145\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 4,\n 145\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 5,\n 145\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 6,\n 220\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 7,\n 140\n ),\n setColumnWidth(\n sheetIds.nominaVsBanco,\n 8,\n 320\n ),\n ...(mainRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.nominaVsBanco,\n 4,\n mainReadabilityEndRow,\n 0,\n 9\n ),\n autoResizeRowsRequest(\n sheetIds.nominaVsBanco,\n 4,\n mainReadabilityEndRow\n ),\n ]\n : []),\n\n // 02 Banco sin Nómina\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 1,\n 320\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 2,\n 160\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 3,\n 145\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 4,\n 260\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 5,\n 170\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 6,\n 560\n ),\n setColumnWidth(\n sheetIds.bancoSinNomina,\n 7,\n 320\n ),\n ...(bancoSinNominaRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.bancoSinNomina,\n 4,\n bancoSinNominaReadabilityEndRow,\n 0,\n 8\n ),\n autoResizeRowsRequest(\n sheetIds.bancoSinNomina,\n 4,\n bancoSinNominaReadabilityEndRow\n ),\n ]\n : []),\n\n // 03 Banco sin Bamboo\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 1,\n 320\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 2,\n 160\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 3,\n 145\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 4,\n 260\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 5,\n 170\n ),\n setColumnWidth(\n sheetIds.bancoSinBamboo,\n 6,\n 320\n ),\n ...(bancoSinBambooRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.bancoSinBamboo,\n 4,\n bancoSinBambooReadabilityEndRow,\n 0,\n 7\n ),\n autoResizeRowsRequest(\n sheetIds.bancoSinBamboo,\n 4,\n bancoSinBambooReadabilityEndRow\n ),\n ]\n : []),\n\n // 04 Diferencias de Nombre\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 1,\n 320\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 2,\n 320\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 3,\n 160\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 4,\n 145\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 5,\n 170\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 6,\n 600\n ),\n setColumnWidth(\n sheetIds.diferenciasNombreBanco,\n 7,\n 320\n ),\n ...(diferenciasNombreRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.diferenciasNombreBanco,\n 4,\n diferenciasNombreReadabilityEndRow,\n 0,\n 8\n ),\n autoResizeRowsRequest(\n sheetIds.diferenciasNombreBanco,\n 4,\n diferenciasNombreReadabilityEndRow\n ),\n ]\n : []),\n\n // 05 Cuenta Mal Digitada\n ...(hasCuentaMalDigitada\n ? [\n setColumnWidth(\n sheetIds.cuentaMalDigitada,\n 1,\n 300\n ),\n setColumnWidth(\n sheetIds.cuentaMalDigitada,\n 2,\n 600\n ),\n setColumnWidth(\n sheetIds.cuentaMalDigitada,\n 3,\n 320\n ),\n ]\n : []),\n\n // Resumen\n setColumnWidth(\n sheetIds.resumen,\n 0,\n 380\n ),\n setColumnWidth(\n sheetIds.resumen,\n 1,\n 320\n ),\n ...(resumenRows.length > 0\n ? [\n wrapRangeRequest(\n sheetIds.resumen,\n 4,\n resumenReadabilityEndRow,\n 0,\n 2\n ),\n autoResizeRowsRequest(\n sheetIds.resumen,\n 4,\n resumenReadabilityEndRow\n ),\n ]\n : [])\n);\n\n\nreturn [\n {\n json: {\n ok: true,\n stage:\n 'preparar_google_sheet_tt',\n metadata,\n summary,\n spreadsheetTitle,\n sheetIds,\n sheetTitles,\n createSpreadsheetBody: {\n properties: {\n title: spreadsheetTitle,\n },\n sheets: [\n {\n properties: {\n sheetId:\n sheetIds.nominaVsBanco,\n title:\n sheetTitles.nominaVsBanco,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinNomina,\n title:\n sheetTitles.bancoSinNomina,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.bancoSinBamboo,\n title:\n sheetTitles.bancoSinBamboo,\n },\n },\n {\n properties: {\n sheetId:\n sheetIds.diferenciasNombreBanco,\n title:\n sheetTitles.diferenciasNombreBanco,\n },\n },\n ...(hasCuentaMalDigitada\n ? [\n {\n properties: {\n sheetId:\n sheetIds.cuentaMalDigitada,\n title:\n sheetTitles.cuentaMalDigitada,\n },\n },\n ]\n : []),\n {\n properties: {\n sheetId:\n sheetIds.resumen,\n title:\n sheetTitles.resumen,\n },\n },\n ],\n },\n valueBatchBody: {\n valueInputOption:\n 'RAW',\n data: valueData,\n },\n formatBatchBody: {\n requests: formatRequests,\n },\n originalResponse: data,\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 7264, - 7296 - ], - "id": "0269ecb0-63b1-43f6-9f34-d18d97a104b4", - "name": "Preparar Google Sheet" - }, - { - "parameters": { - "method": "POST", - "url": "https://sheets.googleapis.com/v4/spreadsheets", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "googleOAuth2Api", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{\n(() => {\n const prepared =\n $('Preparar Google Sheet').first().json || {};\n\n const createBody =\n prepared.createSpreadsheetBody || {};\n\n if (\n !Array.isArray(createBody.sheets) ||\n createBody.sheets.length === 0\n ) {\n throw new Error(\n 'Preparar Google Sheet no devolvió las hojas que deben crearse.'\n );\n }\n\n return {\n properties: {\n ...(createBody.properties || {}),\n timeZone: 'America/Port_of_Spain',\n },\n\n sheets: createBody.sheets.map((sheet) => ({\n properties: {\n ...(sheet.properties || {}),\n\n gridProperties: {\n ...((sheet.properties || {}).gridProperties || {}),\n frozenRowCount: 1,\n },\n },\n })),\n };\n})()\n}}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 7520, - 7296 - ], - "id": "1ef3b281-554e-41dd-b95f-28fa3a1e70be", - "name": "Crear Google Sheet", - "credentials": { - "httpBasicAuth": { - "id": "nIxZ7elcHvuzsRKW", - "name": "Neo4j" - }, - "googleOAuth2Api": { - "id": "eHseMeH39kRcXgOF", - "name": "Google account 2" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "={{ 'https://sheets.googleapis.com/v4/spreadsheets/' + $('Crear Google Sheet').first().json.spreadsheetId + '/values:batchUpdate' }}", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "googleOAuth2Api", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $('Preparar Google Sheet').first().json.valueBatchBody }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 7776, - 7296 - ], - "id": "cf220e3d-0954-4b08-aa48-7d90afbb099d", - "name": "Escribir Google Sheet", - "credentials": { - "googleOAuth2Api": { - "id": "dQ1MJSJSWcoWYcb8", - "name": "Google account - Isaac Producción" - } - } - }, - { - "parameters": { - "method": "POST", - "url": "={{ 'https://sheets.googleapis.com/v4/spreadsheets/' + $('Crear Google Sheet').first().json.spreadsheetId + ':batchUpdate' }}", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "googleOAuth2Api", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $('Preparar Google Sheet').first().json.formatBatchBody }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 8032, - 7296 - ], - "id": "1e76a3ba-00f2-4455-ac64-e1f377e3d309", - "name": "Formatear Google Sheet", - "credentials": { - "googleOAuth2Api": { - "id": "dQ1MJSJSWcoWYcb8", - "name": "Google account - Isaac Producción" - } - } - }, - { - "parameters": { - "jsCode": "const createdSheet = $('Crear Google Sheet').first().json || {};\nconst spreadsheetId = createdSheet.spreadsheetId;\n\nif (!spreadsheetId) {\n throw new Error('No se recibió spreadsheetId desde Crear Google Sheet.');\n}\n\nconst allowedEmails = [\n 'iaracena@gomezleemarketing.com',\n 'ymadera@gomezleemarketing.com',\n 'mgomez@gomezleemarketing.com',\n 'jgomez@gomezleemarketing.com',\n];\n\nreturn allowedEmails.map((email) => ({\n json: {\n spreadsheetId,\n email,\n permissionBody: {\n type: 'user',\n role: 'writer',\n emailAddress: email,\n },\n },\n}));" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 8304, - 7296 - ], - "id": "bba45803-df53-41eb-9f04-988fc9456a3c", - "name": "Preparar permisos Google Sheet" - }, - { - "parameters": { - "method": "POST", - "url": "={{ 'https://www.googleapis.com/drive/v3/files/' + $json.spreadsheetId + '/permissions?sendNotificationEmail=false' }}", - "authentication": "predefinedCredentialType", - "nodeCredentialType": "googleOAuth2Api", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $json.permissionBody }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 8560, - 7296 - ], - "id": "00d29e58-0e73-4b96-8848-f3012b30189b", - "name": "Compartir Google Sheet", - "credentials": { - "googleOAuth2Api": { - "id": "dQ1MJSJSWcoWYcb8", - "name": "Google account - Isaac Producción" - } - } - }, - { - "parameters": { - "jsCode": "const cruce =\n $('Cruzar Nómina vs Banco').first().json || {};\n\nconst createdSheet =\n $('Crear Google Sheet').first().json || {};\n\nconst metadata = cruce.metadata || {};\nconst summary = cruce.summary || {};\nconst debug = cruce.debug || {};\n\nconst spreadsheetId =\n createdSheet.spreadsheetId ||\n cruce.spreadsheetId ||\n '';\n\nconst reportUrl =\n createdSheet.spreadsheetUrl ||\n createdSheet.spreadsheet_url ||\n (\n spreadsheetId\n ? `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit`\n : null\n );\n\nfunction toNumber(value) {\n const parsed = Number(value);\n return Number.isFinite(parsed)\n ? parsed\n : 0;\n}\n\nfunction buildPeriodKey(periodMetadata) {\n const country =\n periodMetadata.country || 'TT';\n\n const year =\n periodMetadata.year || '';\n\n const month = String(\n periodMetadata.month || ''\n ).padStart(2, '0');\n\n const periodType =\n periodMetadata.period_type ||\n 'periodo';\n\n return (\n `${country}-${year}-${month}-${periodType}`\n );\n}\n\nconst discrepancias =\n toNumber(summary.discrepancias);\n\nconst discrepanciasMontoPago =\n toNumber(\n summary.discrepanciasMontoPago ??\n Math.max(\n 0,\n discrepancias -\n toNumber(\n summary.posiblesCuentasMalDigitadas\n )\n )\n );\n\nconst bancoSinNomina =\n toNumber(summary.bancoSinNomina);\n\nconst nominaSinCuenta =\n toNumber(summary.nominaSinCuenta);\n\nconst diferenciasNombreBanco =\n toNumber(\n summary.diferenciasNombreBanco\n );\n\nconst bancoSinBamboo =\n toNumber(summary.bancoSinBamboo);\n\nconst posiblesCuentasMalDigitadas =\n toNumber(\n summary.posiblesCuentasMalDigitadas\n );\n\nconst pendientes =\n toNumber(summary.pendientes) ||\n (\n discrepanciasMontoPago +\n bancoSinNomina +\n nominaSinCuenta +\n posiblesCuentasMalDigitadas +\n bancoSinBamboo +\n diferenciasNombreBanco\n );\n\nconst requiereRevision =\n pendientes > 0 ||\n bancoSinBamboo > 0;\n\nconst estado = requiereRevision\n ? 'pendiente_revision'\n : 'resuelto';\n\nconst payload = {\n source_app:\n metadata.source_app ||\n 'cruce-cuentas-glm-trinidad-tobago',\n\n country: 'TT',\n country_name:\n 'Trinidad y Tobago',\n\n year: toNumber(metadata.year),\n month: toNumber(metadata.month),\n period_type:\n metadata.period_type || '',\n period_label:\n metadata.period_label || '',\n period_start:\n metadata.period_start || null,\n period_end:\n metadata.period_end || null,\n period_key:\n buildPeriodKey({\n ...metadata,\n country: 'TT',\n }),\n\n payroll_file_name:\n metadata.payroll_file_name || '',\n\n bank_file_names:\n metadata.bank_file_names || [],\n\n coincidencias:\n toNumber(summary.coincidencias),\n\n discrepancias,\n\n banco_sin_bamboo:\n bancoSinBamboo,\n\n detalle_banco_sin_bamboo:\n Array.isArray(\n cruce.bankWithoutBamboo\n )\n ? cruce.bankWithoutBamboo\n : [],\n\n banco_sin_nomina:\n bancoSinNomina,\n\n nomina_sin_cuenta:\n nominaSinCuenta,\n\n nomina_sin_bamboo: 0,\n bamboo_sin_nomina: 0,\n\n filas_nomina_validas:\n toNumber(\n summary.filasNominaValidas\n ),\n\n cuentas_nomina_agrupadas:\n toNumber(\n summary.cuentasNominaAgrupadas\n ),\n\n transacciones_banco:\n toNumber(\n summary.transaccionesBanco\n ),\n\n cuentas_banco_agrupadas:\n toNumber(\n summary.cuentasBancoAgrupadas\n ),\n\n total_nomina:\n toNumber(summary.totalNomina),\n\n total_banco:\n toNumber(summary.totalBanco),\n\n diferencia_total:\n toNumber(\n summary.diferenciaTotal\n ),\n\n report_url: reportUrl,\n spreadsheet_id:\n spreadsheetId,\n estado,\n\n ejecutado_por_nombre:\n metadata.requested_by_name ||\n 'Usuario GLM',\n\n ejecutado_por_email:\n metadata.requested_by_email ||\n '',\n\n metadata: {\n ...metadata,\n country: 'TT',\n country_name:\n 'Trinidad y Tobago',\n diferencias_nombre_banco:\n diferenciasNombreBanco,\n banco_sin_bamboo:\n bancoSinBamboo,\n posibles_cuentas_mal_digitadas:\n toNumber(\n summary\n .posiblesCuentasMalDigitadas\n ),\n pendientes_cruce_principal:\n pendientes,\n requiere_revision:\n requiereRevision,\n },\n\n summary,\n\n debug: {\n sheet_summaries:\n debug.sheet_summaries || [],\n bank_name_differences:\n cruce.nameDifferences || [],\n bamboo_matches:\n debug.bamboo_matches || [],\n bamboo_excluded_payments:\n debug.bamboo_excluded_payments || [],\n banco_sin_bamboo:\n cruce.bankWithoutBamboo || [],\n },\n};\n\nreturn [\n {\n json: {\n ...cruce,\n\n // Se conserva la tabla histórica actual para\n // que la app pueda consultar todos los países\n // mediante el campo country y luego usar RPC.\n supabaseTable:\n 'cruces_cuentas_gt_reportes',\n\n supabasePayload: payload,\n reportUrl,\n spreadsheetId,\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 9904, - 7296 - ], - "id": "09cfaf1e-f278-4f34-b3f2-09ccbc37c10d", - "name": "Preparar histórico Supabase" - }, - { - "parameters": { - "method": "POST", - "url": "https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes", - "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" - }, - { - "name": "Prefer", - "value": "return=representation" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $json.supabasePayload }}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 10160, - 7296 - ], - "id": "a828f0bf-44ef-452c-8aae-f089b34aace5", - "name": "Insertar histórico Supabase", - "onError": "continueRegularOutput" - }, - { - "parameters": { - "jsCode": "const prepared = $('Preparar Google Sheet').first().json || {};\nconst createdSheet = $('Crear Google Sheet').first().json || {};\n\nconst original =\n prepared.originalResponse ||\n prepared.original_response ||\n prepared.response ||\n {};\n\nconst spreadsheetId = createdSheet.spreadsheetId || '';\nconst reportUrl =\n createdSheet.spreadsheetUrl ||\n (spreadsheetId ? `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit` : null);\n\nreturn [\n {\n json: {\n ok: original.ok ?? true,\n message: reportUrl\n ? 'Cruce procesado correctamente. Google Sheet generado.'\n : 'Cruce procesado correctamente, pero no se recibió URL del Google Sheet.',\n stage: reportUrl ? 'cruce_completado_con_reporte' : 'cruce_completado_sin_reporte',\n errors: original.errors || [],\n metadata: original.metadata || {},\n summary: original.summary || {},\n rows: original.rows || [],\n bankWithoutBamboo:\n original.bankWithoutBamboo || [],\n bambooSummary:\n original.bambooSummary || {},\n reportUrl,\n googleSheet: {\n spreadsheetId,\n spreadsheetUrl: reportUrl,\n },\n debug: {\n rows_returned: Array.isArray(original.rows) ? original.rows.length : 0,\n coincidencias: original.summary?.coincidencias ?? 0,\n discrepancias: original.summary?.discrepancias ?? 0,\n discrepanciasMontoPago:\n original.summary?.discrepanciasMontoPago ?? 0,\n posiblesCuentasMalDigitadas:\n original.summary?.posiblesCuentasMalDigitadas ?? 0,\n totalResultados:\n original.summary?.totalResultados ?? 0,\n bancoSinBamboo:\n original.summary?.bancoSinBamboo ?? 0,\n bancoSinBambooRows:\n Array.isArray(original.bankWithoutBamboo)\n ? original.bankWithoutBamboo.length\n : 0,\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 10432, - 7296 - ], - "id": "4321d98f-7d24-49d5-a03f-b19eb407f11e", - "name": "Preparar respuesta final" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{\n(() => {\n const data = $json || {};\n\n const original =\n data.originalResponse ||\n data.original_response ||\n data.response ||\n data.cruceResponse ||\n data.cruce_response ||\n data;\n\n const summary = original.summary || data.summary || {};\n const rows = original.rows || data.rows || [];\n const bankWithoutBamboo =\n original.bankWithoutBamboo ||\n data.bankWithoutBamboo ||\n [];\n const bambooSummary =\n original.bambooSummary ||\n data.bambooSummary ||\n {};\n\n const reportUrl =\n data.reportUrl ||\n data.report_url ||\n data.googleSheetUrl ||\n data.google_sheet_url ||\n data.spreadsheetUrl ||\n data.spreadsheet_url ||\n original.reportUrl ||\n original.report_url ||\n null;\n\n return {\n ok: original.ok ?? data.ok ?? true,\n message: reportUrl\n ? 'Cruce procesado correctamente. Google Sheet generado.'\n : 'Cruce procesado correctamente.',\n stage: reportUrl ? 'cruce_completado_con_reporte' : 'cruce_completado',\n errors: original.errors || data.errors || [],\n metadata: original.metadata || data.metadata || {},\n summary,\n rows,\n bankWithoutBamboo,\n bambooSummary,\n reportUrl,\n debug: {\n source_stage: data.stage || null,\n rows_returned:\n Array.isArray(rows) ? rows.length : 0,\n banco_sin_bamboo_rows:\n Array.isArray(bankWithoutBamboo)\n ? bankWithoutBamboo.length\n : 0,\n report_url_found: Boolean(reportUrl),\n },\n };\n})()\n}}", - "options": { - "responseCode": 200, - "responseHeaders": { - "entries": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - } - } - }, - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 10688, - 7296 - ], - "id": "5f85312f-d57b-462b-943a-a7339f81e69d", - "name": "Respond to Webhook" - }, - { - "parameters": { - "content": "# 📥 RECEPCIÓN Y LECTURA DE ARCHIVOS — TT\n\nRecibe desde el Portal de Verificación de Nóminas los archivos y parámetros necesarios para procesar Trinidad y Tobago.\n\nFuentes utilizadas:\n\n- Directorio de empleados de BambooHR.\n- Archivo CSV del banco.\n- Libro de nómina con múltiples hojas o unidades.\n\nEste bloque:\n\n1. Recibe la solicitud enviada por la aplicación.\n2. Normaliza los parámetros del período.\n3. Consulta los empleados disponibles en BambooHR.\n4. Estandariza los datos del directorio.\n5. Convierte el CSV bancario en registros procesables.\n6. Extrae individualmente las hojas incluidas en el archivo de nómina.\n\nLas hojas extraídas pueden corresponder a diferentes clientes, marcas o unidades operativas.\n\nReglas:\n\n- No iniciar el cruce sin los archivos obligatorios.\n- Mantener separados los datos de banco, nómina y BambooHR.\n- Conservar el período recibido desde la aplicación.\n- No asumir que todas las hojas contienen la misma estructura.\n- Preparar una salida consistente para la etapa de consolidación.", - "height": 2016, - "width": 1424, - "color": 7 - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - 2576, - 6624 - ], - "id": "d209ed03-9a6c-4377-aeb3-a25361644e5f", - "name": "Sticky Note" - }, - { - "parameters": { - "content": "# 🔍 CONSOLIDACIÓN Y CRUCE — TRINIDAD Y TOBAGO\n\nConsolida todas las hojas de nómina y compara los empleados y valores contra el archivo bancario y BambooHR.\n\n## Consolidación de nómina\n\nLas hojas extraídas se unen progresivamente hasta formar una única nómina del período.\n\nDespués de combinarlas:\n\n- Se normalizan nombres.\n- Se limpian espacios y caracteres.\n- Se estandarizan correos e identificadores.\n- Se homogenizan los campos monetarios.\n- Se conserva la hoja o unidad de origen cuando sea necesario.\n\n## Cruce de fuentes\n\nEl flujo incorpora progresivamente:\n\n1. Nómina consolidada.\n2. Registros del banco.\n3. Información del empleado en BambooHR.\n\nEl cruce permite identificar casos como:\n\n- Empleados con diferencias entre nómina y banco.\n- Personas presentes únicamente en nómina.\n- Personas presentes únicamente en el banco.\n- Empleados que no pueden relacionarse con BambooHR.\n- Posibles diferencias de nombre, correo, cuenta o monto.\n\nReglas:\n\n- Evitar duplicar empleados al combinar hojas.\n- No depender únicamente del nombre cuando exista otro identificador.\n- Mantener disponibles los valores originales para validación.\n- Diferenciar una ausencia real de un problema de coincidencia.\n- Preparar los resultados en el formato requerido por el reporte final.", - "height": 1984, - "width": 1888, - "color": "#321764" - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - 4528, - 6560 - ], - "id": "17197e7f-e073-464e-b8c8-ff0856c584d7", - "name": "Sticky Note1" - }, - { - "parameters": { - "content": "# 📊 GENERACIÓN DEL REPORTE EN GOOGLE SHEETS\n\nCrea el reporte final de verificación de nómina de Trinidad y Tobago.\n\nProceso:\n\n1. Organiza los resultados obtenidos durante el cruce.\n2. Define las hojas, encabezados y filas del reporte.\n3. Crea un nuevo archivo de Google Sheets.\n4. Escribe toda la información procesada.\n5. Aplica formato visual.\n6. Configura los permisos de acceso.\n7. Comparte el reporte con las personas autorizadas.\n\nEl reporte puede incluir:\n\n- Resultados del cruce.\n- Diferencias detectadas.\n- Registros sin correspondencia.\n- Información de BambooHR.\n- Resumen del período.\n- Datos necesarios para revisión y seguimiento.\n\nFormato aplicado:\n\n- Encabezados destacados.\n- Columnas ajustadas.\n- Valores monetarios con formato correcto.\n- Fechas normalizadas.\n- Filtros y congelación de encabezados cuando corresponda.\n\nReglas:\n\n- No compartir el archivo antes de terminar la escritura.\n- No devolver un enlace hasta confirmar que el Sheet existe.\n- Compartir solamente con los usuarios autorizados.\n- Mantener Google Sheets como entregable y no como fuente original de los datos.", - "height": 720, - "width": 2064, - "color": "#556822" - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - 6752, - 6944 - ], - "id": "656ae2c7-4c04-4346-90de-1e4db21a637d", - "name": "Sticky Note2" - }, - { - "parameters": { - "content": "# 🗂️ HISTÓRICO Y RESPUESTA FINAL\n\nRegistra la ejecución en Supabase y devuelve el resultado al Portal de Verificación de Nóminas.\n\n## Registro histórico\n\nDespués de generar el reporte, se prepara un registro con información como:\n\n- País: Trinidad y Tobago.\n- Año y mes procesados.\n- Tipo de período.\n- Fecha de ejecución.\n- Usuario que inició el proceso.\n- Cantidad de registros analizados.\n- Cantidad de hallazgos.\n- Enlace del Google Sheet.\n- Estado inicial del reporte.\n- Identificador de la ejecución.\n\nSupabase funciona como fuente oficial para los históricos mostrados posteriormente en el portal.\n\n## Respuesta a la aplicación\n\nEl flujo construye una respuesta final con:\n\n- Indicador de éxito.\n- Enlace al reporte.\n- Resumen de resultados.\n- Identificador del histórico.\n- Estado del proceso.\n- Mensaje apto para mostrar en la interfaz.\n\nReglas:\n\n- Registrar el histórico solamente después de crear el reporte.\n- No declarar éxito si el Sheet o el histórico fallaron.\n- No devolver credenciales ni datos internos.\n- Mantener una estructura estable para la aplicación.\n- Cerrar siempre la solicitud mediante Respond to Webhook.", - "height": 768, - "width": 2032, - "color": "#774B22" - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - 8944, - 6944 - ], - "id": "ac5189a6-802b-4f5e-a946-2aa574408974", - "name": "Sticky Note3" - } - ], - "pinData": {}, - "connections": { - "Webhook": { - "main": [ - [ - { - "node": "Preparar entrada app", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar entrada app": { - "main": [ - [ - { - "node": "Parsear CSV banco TT", - "type": "main", - "index": 0 - }, - { - "node": "Extract - BICE", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Goldey Samuel", - "type": "main", - "index": 0 - }, - { - "node": "Extract - P&G", - "type": "main", - "index": 0 - }, - { - "node": "Extract - Whirlpool", - "type": "main", - "index": 0 - }, - { - "node": "Extract - KAD", - "type": "main", - "index": 0 - }, - { - "node": "Extract - GLM People", - "type": "main", - "index": 0 - }, - { - "node": "Extract - GLM", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - BICE": { - "main": [ - [ - { - "node": "Merge Hojas TT 01-02", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - Goldey Samuel": { - "main": [ - [ - { - "node": "Merge Hojas TT 01-02", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge Hojas TT 01-02": { - "main": [ - [ - { - "node": "Merge Hojas TT 03", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - P&G": { - "main": [ - [ - { - "node": "Merge Hojas TT 03", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge Hojas TT 03": { - "main": [ - [ - { - "node": "Merge Hojas TT 04", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - Whirlpool": { - "main": [ - [ - { - "node": "Merge Hojas TT 04", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge Hojas TT 04": { - "main": [ - [ - { - "node": "Merge Hojas TT 05", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - KAD": { - "main": [ - [ - { - "node": "Merge Hojas TT 05", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge Hojas TT 05": { - "main": [ - [ - { - "node": "Merge Hojas TT 06", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - GLM People": { - "main": [ - [ - { - "node": "Merge Hojas TT 06", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge Hojas TT 06": { - "main": [ - [ - { - "node": "Merge Hojas TT 07", - "type": "main", - "index": 0 - } - ] - ] - }, - "Extract - GLM": { - "main": [ - [ - { - "node": "Merge Hojas TT 07", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge Hojas TT 07": { - "main": [ - [ - { - "node": "Normalizar Nómina TT", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parsear CSV banco TT": { - "main": [ - [ - { - "node": "Merge Banco + Nómina TT", - "type": "main", - "index": 0 - } - ] - ] - }, - "Normalizar Nómina TT": { - "main": [ - [ - { - "node": "Merge Banco + Nómina TT", - "type": "main", - "index": 1 - } - ] - ] - }, - "HTTP - Empleados BambooHR TT": { - "main": [ - [ - { - "node": "Normalizar BambooHR TT", - "type": "main", - "index": 0 - } - ] - ] - }, - "Merge Banco + Nómina TT": { - "main": [ - [ - { - "node": "HTTP - Empleados BambooHR TT", - "type": "main", - "index": 0 - }, - { - "node": "Merge - Agregar BambooHR TT", - "type": "main", - "index": 0 - } - ] - ] - }, - "Normalizar BambooHR TT": { - "main": [ - [ - { - "node": "Merge - Agregar BambooHR TT", - "type": "main", - "index": 1 - } - ] - ] - }, - "Merge - Agregar BambooHR TT": { - "main": [ - [ - { - "node": "Cruzar Nómina vs Banco", - "type": "main", - "index": 0 - } - ] - ] - }, - "Cruzar Nómina vs Banco": { - "main": [ - [ - { - "node": "Preparar Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar Google Sheet": { - "main": [ - [ - { - "node": "Crear Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Crear Google Sheet": { - "main": [ - [ - { - "node": "Escribir Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Escribir Google Sheet": { - "main": [ - [ - { - "node": "Formatear Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Formatear Google Sheet": { - "main": [ - [ - { - "node": "Preparar permisos Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar permisos Google Sheet": { - "main": [ - [ - { - "node": "Compartir Google Sheet", - "type": "main", - "index": 0 - } - ] - ] - }, - "Compartir Google Sheet": { - "main": [ - [ - { - "node": "Preparar histórico Supabase", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar histórico Supabase": { - "main": [ - [ - { - "node": "Insertar histórico Supabase", - "type": "main", - "index": 0 - } - ] - ] - }, - "Insertar histórico Supabase": { - "main": [ - [ - { - "node": "Preparar respuesta final", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar respuesta final": { - "main": [ - [ - { - "node": "Respond to Webhook", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1", - "binaryMode": "separate", - "availableInMCP": true, - "timeSavedMode": "fixed", - "errorWorkflow": "puF4LUczoSz3hcek", - "timezone": "America/Santo_Domingo", - "callerPolicy": "workflowsFromSameOwner" - }, - "versionId": "2be11ec0-4659-47ca-9a58-38fd0f9eb4d9", - "meta": { - "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" - }, - "id": "5AujMxduslftVg9z", - "tags": [] -} \ No newline at end of file diff --git a/Flujo de n8n: Portal de Verificación de Nómina GT - Envío de Reporte.json b/Flujo de n8n: Portal de Verificación de Nómina GT - Envío de Reporte.json deleted file mode 100644 index bef74ad..0000000 --- a/Flujo de n8n: Portal de Verificación de Nómina GT - Envío de Reporte.json +++ /dev/null @@ -1,354 +0,0 @@ -{ - "name": "Portal de Verificación de Nómina GT - Envío de Reporte", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "cruce-cuentas-gt-marcar-resuelto", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2.1, - "position": [ - -560, - 32 - ], - "id": "681b06d3-4b12-441e-b5b2-61b283b8f418", - "name": "Webhook Marcar Resuelto", - "webhookId": "5b7d6bd6-caa6-47e6-b9b8-982f6e7e0a0f" - }, - { - "parameters": { - "jsCode": "const body = $input.first().json.body || $input.first().json || {};\n\nfunction clean(value) {\n return String(value ?? '').replace(/\\s+/g, ' ').trim();\n}\n\nconst reportId = clean(body.reportId || body.id);\nconst comentarioResolucion = clean(body.comentarioResolucion || body.comentario || '');\nconst resueltoPorNombre = clean(body.resueltoPorNombre || body.userName || 'Usuario GLM');\nconst resueltoPorEmail = clean(body.resueltoPorEmail || body.userEmail || '');\n\nconst errors = [];\n\nif (!reportId) {\n errors.push('No se recibió el ID del reporte.');\n}\n\nif (!comentarioResolucion) {\n errors.push('Debe indicar un comentario de resolución.');\n}\n\nif (comentarioResolucion.length < 10) {\n errors.push('El comentario de resolución debe ser más descriptivo.');\n}\n\nif (errors.length > 0) {\n return [\n {\n json: {\n ok: false,\n stage: 'validacion_resolucion',\n errors,\n reportId,\n },\n },\n ];\n}\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'resolucion_validada',\n reportId,\n comentarioResolucion,\n resueltoPorNombre,\n resueltoPorEmail,\n resolvedAt: new Date().toISOString(),\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - -352, - 32 - ], - "id": "7df13555-97eb-4c62-be7d-13b12a631cd9", - "name": "Validar resolución" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "c89884c6-de96-4b08-bd20-54644bb95bc7", - "leftValue": "={{ $json.ok }}", - "rightValue": "", - "operator": { - "type": "boolean", - "operation": "true", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - -144, - 32 - ], - "id": "fdaa242f-9e63-4c95-82cd-27f645805f66", - "name": "¿Solicitud válida?" - }, - { - "parameters": { - "method": "PATCH", - "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes?id=eq.' + $json.reportId }}", - "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" - }, - { - "name": "Prefer", - "value": "return=representation" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{\n {\n estado: 'resuelto',\n resuelto_por_nombre: $json.resueltoPorNombre,\n resuelto_por_email: $json.resueltoPorEmail,\n resuelto_en: $json.resolvedAt,\n comentario_resolucion: $json.comentarioResolucion,\n updated_at: $json.resolvedAt\n }\n}}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 192, - -80 - ], - "id": "31b2de6a-89f6-4d69-9712-59e5ee3957e8", - "name": "Actualizar reporte Supabase" - }, - { - "parameters": { - "jsCode": "const input = $input.first().json || {};\nconst report = Array.isArray(input) ? input[0] : input;\nconst original = $('Validar resolución').first().json || {};\n\nif (!report || !report.id) {\n throw new Error('No se encontró el reporte para preparar la notificación.');\n}\n\nfunction escapeHtml(value) {\n return String(value ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction numberValue(value) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nconst periodo = report.period_label || 'Período no especificado';\nconst reportUrl = report.report_url || '';\n\nconst discrepancias = numberValue(report.discrepancias);\nconst bancoSinBamboo = numberValue(report.banco_sin_bamboo);\nconst nominaSinBamboo = numberValue(report.nomina_sin_bamboo);\nconst bambooSinNomina = numberValue(report.bamboo_sin_nomina);\n\nconst resolvedAt = original.resolvedAt || new Date().toISOString();\n\nconst resolvedDateLabel = new Date(resolvedAt).toLocaleString('es-GT', {\n timeZone: 'America/Guatemala',\n dateStyle: 'long',\n timeStyle: 'short',\n});\n\nconst resolvedByName =\n original.resueltoPorNombre || 'Usuario GLM';\n\nconst resolvedByEmail =\n original.resueltoPorEmail || '';\n\nconst resolutionComment =\n original.comentarioResolucion || 'Sin comentario registrado.';\n\nconst logoUrl =\n 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\n\nconst subject =\n `Reporte resuelto - Cruce de Cuentas Guatemala - ${periodo}`;\n\nconst textBody = `\nHola Yanelly,\n\nSe marcó como resuelto un reporte de Cruce de Cuentas Guatemala.\n\nPeríodo:\n${periodo}\n\nResuelto por:\n${resolvedByName}${resolvedByEmail ? ` (${resolvedByEmail})` : ''}\n\nFecha/hora:\n${resolvedDateLabel}\n\nResumen:\n- Discrepancias: ${discrepancias}\n- Banco sin Bamboo: ${bancoSinBamboo}\n- Nómina sin Bamboo: ${nominaSinBamboo}\n- Bamboo sin Nómina: ${bambooSinNomina}\n\nComentario:\n${resolutionComment}\n\nGoogle Sheet:\n${reportUrl || 'No disponible'}\n\nSaludos,\nPortal Cruce de Cuentas GLM\n`.trim();\n\nconst htmlBody = `\n\n\n\n \n \n ${escapeHtml(subject)}\n\n\n\n\n \n \n \n\n \n\n \n \n \n \n \n\n \n  \n \n\n \n \n
\n Cruce de Cuentas Guatemala\n
\n\n

\n Reporte marcado como resuelto\n

\n \n \n\n \n \n

\n Hola Yanelly,\n

\n\n

\n El siguiente reporte fue revisado y marcado como\n resuelto.\n

\n \n \n\n \n \n \n \n \n Período\n \n\n \n ${escapeHtml(periodo)}\n \n \n\n \n \n Resuelto por\n \n\n \n ${escapeHtml(resolvedByName)}\n ${\n resolvedByEmail\n ? `
${escapeHtml(resolvedByEmail)}`\n : ''\n }\n \n \n\n \n \n Fecha y hora\n \n\n \n ${escapeHtml(resolvedDateLabel)}\n \n \n \n \n \n\n \n \n
\n Resumen del reporte\n
\n \n \n\n \n \n \n \n \n
\n ${discrepancias}\n
\n\n
\n Discrepancias\n
\n \n\n \n
\n ${bancoSinBamboo}\n
\n\n
\n Banco sin Bamboo\n
\n \n\n \n
\n ${nominaSinBamboo}\n
\n\n
\n Nómina sin Bamboo\n
\n \n\n \n
\n ${bambooSinNomina}\n
\n\n
\n Bamboo sin Nómina\n
\n \n \n \n \n \n\n \n \n
\n Comentario de resolución\n
\n \n \n\n \n \n
\n ${escapeHtml(resolutionComment).replace(/\\n/g, '
')}\n
\n \n \n\n ${\n reportUrl\n ? `\n \n \n \n Abrir Google Sheet\n \n \n \n `\n : ''\n }\n\n \n \n Mensaje automático del Portal Cruce de Cuentas GLM.\n \n \n\n \n \n \n \n\n\n\n`.trim();\n\nreturn [\n {\n json: {\n to: 'ymadera@gomezleemarketing.com, jgomez@gomezleemarketing.com, mgomez@gomezleemarketing.com',\n cc: 'iaracena@gomezleemarketing.com',\n subject,\n textBody,\n htmlBody,\n report,\n resolution: original,\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 400, - -80 - ], - "id": "fb3f529f-164b-4987-a2af-e99e8544c9b1", - "name": "Preparar correo Yanelly" - }, - { - "parameters": { - "sendTo": "={{$json.to}}", - "subject": "={{$json.subject}}", - "message": "={{ $json.htmlBody }}", - "options": { - "appendAttribution": false, - "ccList": "={{ $json.cc }}" - } - }, - "type": "n8n-nodes-base.gmail", - "typeVersion": 2.2, - "position": [ - 608, - -80 - ], - "id": "86252093-a337-4c18-a733-d7f80a954a1f", - "name": "Enviar correo Yanelly", - "webhookId": "da3b90f2-2e52-4a99-beea-5bc580486f75", - "credentials": { - "gmailOAuth2": { - "id": "UDcO1FLJqA453V2D", - "name": "Gmail account 3" - } - } - }, - { - "parameters": { - "jsCode": "const original = $('Validar resolución').first().json || {};\nconst updatedResponse = $('Actualizar reporte Supabase').first().json || {};\nconst updatedReport = Array.isArray(updatedResponse) ? updatedResponse[0] : updatedResponse;\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'reporte_resuelto',\n message: 'Reporte marcado como resuelto correctamente.',\n report: {\n id: original.reportId,\n estado: 'resuelto',\n estadoLabel: 'RESUELTO',\n resueltoPorNombre: original.resueltoPorNombre,\n resueltoPorEmail: original.resueltoPorEmail,\n resueltoEn: original.resolvedAt,\n comentarioResolucion: original.comentarioResolucion,\n reportUrl: updatedReport.report_url || '',\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 816, - -80 - ], - "id": "43dd0a1d-3829-46a6-bcf2-349ad4f5754a", - "name": "Preparar respuesta resuelto" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{ $json }}", - "options": { - "responseCode": 200, - "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": [ - 1024, - -80 - ], - "id": "4fcda86c-0bd9-4797-a85a-273ce6876ad8", - "name": "Responder resuelto app" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{ $json }}", - "options": { - "responseCode": 400, - "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": [ - 64, - 128 - ], - "id": "15a0d2a5-d5f7-4da0-ba2b-ea08dc77dc23", - "name": "Responder error resolución" - }, - { - "parameters": { - "content": "# ✅ CIERRE DE REPORTE — GUATEMALA\n\nRecibe desde el Portal de Verificación de Nóminas la solicitud para marcar un reporte de Guatemala como resuelto.\n\n## Validación de la solicitud\n\nEl Webhook recibe la información enviada por la aplicación y verifica que existan los datos necesarios para procesar el cierre, como:\n\n- Identificador del reporte.\n- Usuario que realizó la resolución.\n- Nuevo estado solicitado.\n- Comentario o detalle de la resolución.\n- Información necesaria para localizar el registro en Supabase.\n\nLa solicitud solamente continúa cuando los datos son válidos y suficientes.\n\n## Actualización en Supabase\n\nCuando la solicitud es correcta:\n\n1. Localiza el reporte correspondiente.\n2. Actualiza su estado a RESUELTO.\n3. Registra el usuario responsable del cierre.\n4. Guarda la fecha y hora de resolución.\n5. Conserva el comentario o detalle proporcionado.\n\nSupabase es la fuente oficial del estado mostrado en el Portal de Verificación de Nóminas.\n\n## Notificación a Yanelly\n\nDespués de confirmar la actualización:\n\n1. Se prepara un correo con los datos principales del reporte.\n2. Se informa que el caso de Guatemala fue marcado como resuelto.\n3. Se identifica al usuario que realizó el cierre.\n4. Se incluye el comentario de resolución, cuando esté disponible.\n5. Se envía la notificación a Yanelly mediante Gmail.\n\nEl correo solamente debe enviarse después de comprobar que Supabase fue actualizado correctamente.\n\n## Respuesta a la aplicación\n\nCuando el proceso termina correctamente:\n\n- Se construye una respuesta de éxito.\n- Se confirma el nuevo estado del reporte.\n- Se devuelve el resultado a la aplicación.\n- El portal puede actualizar inmediatamente la interfaz.\n\n## Ruta de error\n\nCuando la solicitud no supera la validación:\n\n- No se modifica el reporte en Supabase.\n- No se envía ningún correo.\n- Se devuelve una respuesta de error a la aplicación.\n- Se informa que la solicitud contiene datos incompletos o inválidos.\n\n## Reglas\n\n- No marcar un reporte como resuelto sin un identificador válido.\n- No enviar el correo antes de confirmar la actualización.\n- No declarar éxito si Supabase devolvió un error.\n- Evitar cambios parciales entre la base de datos y la notificación.\n- Ambas rutas deben responder al Webhook para evitar solicitudes abiertas.\n- La ruta inválida debe finalizar sin modificar información.", - "height": 1248, - "width": 2624, - "color": "#832F2F" - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - -1376, - -624 - ], - "id": "b9408dc9-bbab-4585-bf2a-9f88f5fe8eaa", - "name": "Sticky Note" - } - ], - "pinData": {}, - "connections": { - "Webhook Marcar Resuelto": { - "main": [ - [ - { - "node": "Validar resolución", - "type": "main", - "index": 0 - } - ] - ] - }, - "Validar resolución": { - "main": [ - [ - { - "node": "¿Solicitud válida?", - "type": "main", - "index": 0 - } - ] - ] - }, - "¿Solicitud válida?": { - "main": [ - [ - { - "node": "Actualizar reporte Supabase", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Responder error resolución", - "type": "main", - "index": 0 - } - ] - ] - }, - "Actualizar reporte Supabase": { - "main": [ - [ - { - "node": "Preparar correo Yanelly", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar correo Yanelly": { - "main": [ - [ - { - "node": "Enviar correo Yanelly", - "type": "main", - "index": 0 - } - ] - ] - }, - "Enviar correo Yanelly": { - "main": [ - [ - { - "node": "Preparar respuesta resuelto", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar respuesta resuelto": { - "main": [ - [ - { - "node": "Responder resuelto app", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1", - "binaryMode": "separate", - "availableInMCP": true, - "timeSavedMode": "fixed", - "errorWorkflow": "puF4LUczoSz3hcek", - "timezone": "America/Santo_Domingo", - "callerPolicy": "workflowsFromSameOwner" - }, - "versionId": "afe39c11-be81-4a06-8937-36adaf938182", - "meta": { - "templateCredsSetupCompleted": true, - "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" - }, - "id": "KuJTvjNNx9XmoazA", - "tags": [] -} \ No newline at end of file diff --git a/Flujo de n8n: Portal de Verificación de Nómina GT - Históricos.json b/Flujo de n8n: Portal de Verificación de Nómina GT - Históricos.json deleted file mode 100644 index f0612fb..0000000 --- a/Flujo de n8n: Portal de Verificación de Nómina GT - Históricos.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "name": "Portal de Verificación de Nómina GT - Históricos", - "nodes": [ - { - "parameters": { - "path": "cruce-cuentas-gt-historicos", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2.1, - "position": [ - -112, - -16 - ], - "id": "47845bfd-64f5-47ec-89e6-c0faa317e08b", - "name": "Webhook Históricos GT", - "webhookId": "e92a1ebc-80e5-4afe-b38d-48a92c4b7fea" - }, - { - "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 totalPendientes =\n discrepancias +\n bancoSinNomina +\n nominaSinCuenta +\n bancoSinBamboo +\n nominaSinBamboo +\n bambooSinNomina;\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 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_gt_listos',\n count: reports.length,\n reports,\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 304, - -16 - ], - "id": "0fb42965-9a23-42e1-bd82-152452dc54ed", - "name": "Normalizar históricos para app" - }, - { - "parameters": { - "url": "https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes?select=*&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": [ - 96, - -16 - ], - "id": "3b8cd950-a973-433e-82a1-fcd67bc9a483", - "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": [ - 528, - -16 - ], - "id": "31c2d67f-3989-4342-9be3-ed718fc63e79", - "name": "Responder históricos app" - }, - { - "parameters": { - "content": "# 📚 HISTÓRICOS DE VERIFICACIÓN — GUATEMALA\n\nEntrega a la aplicación el historial de reportes procesados para Guatemala.\n\n## Recepción de la solicitud\n\nEl Webhook recibe desde el Portal de Verificación de Nóminas una solicitud GET para cargar los registros históricos.\n\nLa solicitud puede incluir parámetros utilizados por la aplicación, como:\n\n- Año.\n- Mes.\n- Tipo de período.\n- Estado del reporte.\n- Usuario responsable.\n- Identificador del reporte.\n- Parámetros de ordenamiento o filtrado.\n\n## Consulta en Supabase\n\nEl flujo consulta en Supabase los reportes históricos correspondientes a Guatemala.\n\nSupabase funciona como fuente oficial para recuperar información como:\n\n- Identificador del reporte.\n- Año y mes procesados.\n- Tipo de nómina o período.\n- Fecha de ejecución.\n- Estado del reporte.\n- Cantidad de hallazgos.\n- Enlace al reporte generado.\n- Usuario que ejecutó el proceso.\n- Usuario y fecha de resolución.\n- Comentario de resolución, cuando exista.\n\n## Normalización para la aplicación\n\nLos registros recuperados se transforman al formato esperado por el portal.\n\nEste bloque:\n\n- Normaliza nombres de campos.\n- Convierte fechas al formato utilizado por la interfaz.\n- Establece valores predeterminados para campos vacíos.\n- Ordena los reportes según la lógica del portal.\n- Conserva los enlaces necesarios.\n- Prepara una estructura consistente aunque no existan resultados.\n\n## Respuesta al portal\n\nEl workflow devuelve a la aplicación:\n\n- Indicador de éxito.\n- Lista de históricos encontrados.\n- Cantidad total de registros.\n- Datos normalizados para mostrar en la interfaz.\n\nCuando no existen históricos, debe responder correctamente con una lista vacía y no tratar el resultado como un error.\n\n## Reglas\n\n- Consultar únicamente reportes correspondientes a Guatemala.\n- No modificar ningún registro durante esta operación.\n- No devolver campos internos o sensibles que la aplicación no necesita.\n- No inventar valores cuando Supabase devuelve campos vacíos.\n- Mantener una estructura de respuesta estable.\n- Responder siempre al Webhook para evitar solicitudes abiertas.\n- Supabase es la fuente oficial de los históricos mostrados en el portal.", - "height": 1296, - "width": 1504, - "color": 6 - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - -768, - -752 - ], - "id": "0a6676a2-9aff-4a7c-b138-05bef8bb8752", - "name": "Sticky Note" - } - ], - "pinData": {}, - "connections": { - "Webhook Históricos GT": { - "main": [ - [ - { - "node": "Consultar históricos Supabase", - "type": "main", - "index": 0 - } - ] - ] - }, - "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 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1", - "binaryMode": "separate", - "availableInMCP": true, - "timeSavedMode": "fixed", - "errorWorkflow": "puF4LUczoSz3hcek", - "timezone": "America/Santo_Domingo", - "callerPolicy": "workflowsFromSameOwner" - }, - "versionId": "fab93c67-7332-4b99-80fe-648c4cace390", - "meta": { - "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" - }, - "id": "QDKFgvoJKviQbDl3", - "tags": [] -} \ No newline at end of file diff --git a/Flujo de n8n: Portal de Verificación de Nómina TT - Envío de Reporte.json b/Flujo de n8n: Portal de Verificación de Nómina TT - Envío de Reporte.json deleted file mode 100644 index e774686..0000000 --- a/Flujo de n8n: Portal de Verificación de Nómina TT - Envío de Reporte.json +++ /dev/null @@ -1,353 +0,0 @@ -{ - "name": "Portal de Verificación de Nómina TT - Envío de Reporte", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "cruce-cuentas-tt-marcar-resuelto", - "responseMode": "responseNode", - "options": {} - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2.1, - "position": [ - -752, - 112 - ], - "id": "1affa570-6b53-43d0-afab-25aaf5b8da2d", - "name": "Webhook Marcar Resuelto", - "webhookId": "b0f8173c-c81a-4d32-9205-ab0102dec679" - }, - { - "parameters": { - "jsCode": "const body = $input.first().json.body || $input.first().json || {};\n\nfunction clean(value) {\n return String(value ?? '').replace(/\\s+/g, ' ').trim();\n}\n\nconst reportId = clean(body.reportId || body.id);\nconst comentarioResolucion = clean(body.comentarioResolucion || body.comentario || '');\nconst resueltoPorNombre = clean(body.resueltoPorNombre || body.userName || 'Usuario GLM');\nconst resueltoPorEmail = clean(body.resueltoPorEmail || body.userEmail || '');\n\nconst errors = [];\n\nif (!reportId) {\n errors.push('No se recibió el ID del reporte.');\n}\n\nif (!comentarioResolucion) {\n errors.push('Debe indicar un comentario de resolución.');\n}\n\nif (comentarioResolucion.length < 10) {\n errors.push('El comentario de resolución debe ser más descriptivo.');\n}\n\nif (errors.length > 0) {\n return [\n {\n json: {\n ok: false,\n stage: 'validacion_resolucion',\n errors,\n reportId,\n },\n },\n ];\n}\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'resolucion_validada',\n reportId,\n comentarioResolucion,\n resueltoPorNombre,\n resueltoPorEmail,\n resolvedAt: new Date().toISOString(),\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - -544, - 112 - ], - "id": "5e78d132-c731-4d6a-8926-8afa88e5df12", - "name": "Validar resolución" - }, - { - "parameters": { - "conditions": { - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict", - "version": 3 - }, - "conditions": [ - { - "id": "c89884c6-de96-4b08-bd20-54644bb95bc7", - "leftValue": "={{ $json.ok }}", - "rightValue": "", - "operator": { - "type": "boolean", - "operation": "true", - "singleValue": true - } - } - ], - "combinator": "and" - }, - "options": {} - }, - "type": "n8n-nodes-base.if", - "typeVersion": 2.3, - "position": [ - -336, - 112 - ], - "id": "cc34f3e6-40f2-43e3-99f1-0aea4fb502c4", - "name": "¿Solicitud válida?" - }, - { - "parameters": { - "method": "PATCH", - "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cruces_cuentas_gt_reportes?id=eq.' + encodeURIComponent($json.reportId) + '&country=eq.TT' }}", - "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" - }, - { - "name": "Prefer", - "value": "return=representation" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{\n {\n estado: 'resuelto',\n resuelto_por_nombre: $json.resueltoPorNombre,\n resuelto_por_email: $json.resueltoPorEmail,\n resuelto_en: $json.resolvedAt,\n comentario_resolucion: $json.comentarioResolucion,\n updated_at: $json.resolvedAt\n }\n}}", - "options": {} - }, - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.4, - "position": [ - 0, - 0 - ], - "id": "52b2af1a-ac9f-4b47-9950-2f91e32084b0", - "name": "Actualizar reporte Supabase" - }, - { - "parameters": { - "jsCode": "const input = $input.first().json || {};\nconst report = Array.isArray(input) ? input[0] : input;\n\nif (!report || !report.id) {\n throw new Error(\n 'No se encontró un reporte de Trinidad y Tobago con el ID recibido.'\n );\n}\n\nif (String(report.country || '').toUpperCase() !== 'TT') {\n throw new Error(\n 'El reporte localizado no pertenece a Trinidad y Tobago.'\n );\n}\nconst original = $('Validar resolución').first().json || {};\n\n\nfunction escapeHtml(value) {\n return String(value ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction numberValue(value) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nconst periodo = report.period_label || 'Período no especificado';\nconst reportUrl = report.report_url || '';\n\nconst discrepancias = numberValue(report.discrepancias);\nconst bancoSinNomina = numberValue(report.banco_sin_nomina);\nconst bancoSinBamboo = numberValue(report.banco_sin_bamboo);\nconst nominaSinCuenta = numberValue(report.nomina_sin_cuenta);\n\nconst resolvedAt = original.resolvedAt || new Date().toISOString();\n\nconst resolvedDateLabel = new Date(resolvedAt).toLocaleString('es-DO', {\n timeZone: 'America/Port_of_Spain',\n dateStyle: 'long',\n timeStyle: 'short',\n});\n\nconst resolvedByName =\n original.resueltoPorNombre || 'Usuario GLM';\n\nconst resolvedByEmail =\n original.resueltoPorEmail || '';\n\nconst resolutionComment =\n original.comentarioResolucion || 'Sin comentario registrado.';\n\nconst logoUrl =\n 'https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png';\n\nconst subject =\n `Reporte resuelto - Cruce de Cuentas Trinidad y Tobago - ${periodo}`;\n\nconst textBody = `\nHola equipo,\n\nSe marcó como resuelto un reporte de Cruce de Cuentas Trinidad y Tobago.\n\nPeríodo:\n${periodo}\n\nResuelto por:\n${resolvedByName}${resolvedByEmail ? ` (${resolvedByEmail})` : ''}\n\nFecha/hora:\n${resolvedDateLabel}\n\nResumen:\n- Discrepancias: ${discrepancias}\n- Banco sin nómina: ${bancoSinNomina}\n- Banco sin Bamboo: ${bancoSinBamboo}\n- Nómina sin cuenta: ${nominaSinCuenta}\n\nComentario:\n${resolutionComment}\n\nGoogle Sheet:\n${reportUrl || 'No disponible'}\n\nSaludos,\nPortal Cruce de Cuentas GLM\n`.trim();\n\nconst htmlBody = `\n\n\n\n \n \n ${escapeHtml(subject)}\n\n\n\n\n \n \n \n\n \n\n \n \n \n \n \n\n \n  \n \n\n \n \n
\n Cruce de Cuentas Trinidad y Tobago\n
\n\n

\n Reporte marcado como resuelto\n

\n \n \n\n \n \n

\n Hola equipo,\n

\n\n

\n El siguiente reporte fue revisado y marcado como\n resuelto.\n

\n \n \n\n \n \n \n \n \n Período\n \n\n \n ${escapeHtml(periodo)}\n \n \n\n \n \n Resuelto por\n \n\n \n ${escapeHtml(resolvedByName)}\n ${\n resolvedByEmail\n ? `
${escapeHtml(resolvedByEmail)}`\n : ''\n }\n \n \n\n \n \n Fecha y hora\n \n\n \n ${escapeHtml(resolvedDateLabel)}\n \n \n \n \n \n\n \n \n
\n Resumen del reporte\n
\n \n \n\n \n \n \n \n \n
\n ${discrepancias}\n
\n\n
\n Discrepancias\n
\n \n\n \n
\n ${bancoSinBamboo}\n
\n\n
\n Banco sin Bamboo\n
\n \n\n \n
\n ${bancoSinNomina}\n
\n\n
\n Banco sin nómina\n
\n \n\n \n
\n ${nominaSinCuenta}\n
\n\n
\n Nómina sin cuenta\n
\n \n \n \n \n \n\n \n \n
\n Comentario de resolución\n
\n \n \n\n \n \n
\n ${escapeHtml(resolutionComment).replace(/\\n/g, '
')}\n
\n \n \n\n ${\n reportUrl\n ? `\n \n \n \n Abrir Google Sheet\n \n \n \n `\n : ''\n }\n\n \n \n Mensaje automático del Portal Cruce de Cuentas GLM.\n \n \n\n \n \n \n \n\n\n\n`.trim();\n\nreturn [\n {\n json: {\n to: 'jgomez@gomezleemarketing.com, mgomez@gomezleemarketing.com, ymadera@gomezleemarketing.com',\n cc: 'iaracena@gomezleemarketing.com',\n subject,\n textBody,\n htmlBody,\n report,\n resolution: original,\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 208, - 0 - ], - "id": "966b80c6-0935-46fe-a700-d073bbc95b2f", - "name": "Preparar correo equipo TT" - }, - { - "parameters": { - "sendTo": "={{$json.to}}", - "subject": "={{$json.subject}}", - "message": "={{ $json.htmlBody }}", - "options": { - "appendAttribution": false, - "ccList": "={{ $json.cc }}" - } - }, - "type": "n8n-nodes-base.gmail", - "typeVersion": 2.2, - "position": [ - 416, - 0 - ], - "id": "dc52381c-3e76-4991-bc09-a87472d51d64", - "name": "Enviar correo equipo TT", - "webhookId": "da3b90f2-2e52-4a99-beea-5bc580486f75", - "credentials": { - "gmailOAuth2": { - "id": "UDcO1FLJqA453V2D", - "name": "Gmail account 3" - } - } - }, - { - "parameters": { - "jsCode": "const original = $('Validar resolución').first().json || {};\nconst updatedResponse = $('Actualizar reporte Supabase').first().json || {};\nconst updatedReport = Array.isArray(updatedResponse) ? updatedResponse[0] : updatedResponse;\n\nreturn [\n {\n json: {\n ok: true,\n stage: 'reporte_resuelto',\n message: 'Reporte marcado como resuelto correctamente.',\n report: {\n id: original.reportId,\n estado: 'resuelto',\n estadoLabel: 'RESUELTO',\n country: 'TT',\n countryName: 'Trinidad y Tobago',\n resueltoPorNombre: original.resueltoPorNombre,\n resueltoPorEmail: original.resueltoPorEmail,\n resueltoEn: original.resolvedAt,\n comentarioResolucion: original.comentarioResolucion,\n reportUrl: updatedReport.report_url || '',\n },\n },\n },\n];" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 624, - 0 - ], - "id": "d0b12567-ae34-4d7a-8b8c-70e4e855bf35", - "name": "Preparar respuesta resuelto" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{ $json }}", - "options": { - "responseCode": 200, - "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": [ - 832, - 0 - ], - "id": "f6ea0dba-e017-41f8-9cb4-2685a608a58d", - "name": "Responder resuelto app" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{ $json }}", - "options": { - "responseCode": 400, - "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": [ - -128, - 208 - ], - "id": "4fbe8e29-c6b3-4095-8474-4d82a1e8c639", - "name": "Responder error resolución" - }, - { - "parameters": { - "content": "# ✅ CIERRE DE REPORTE — TRINIDAD Y TOBAGO\n\nRecibe desde el Portal de Verificación de Nóminas la solicitud para marcar un reporte de Trinidad y Tobago como resuelto.\n\n## Validación inicial\n\nEl Webhook recibe la información enviada por la aplicación y valida datos como:\n\n- Identificador del reporte.\n- Usuario que realizó el cierre.\n- Estado solicitado.\n- Comentario o detalle de resolución.\n- Datos necesarios para localizar el registro en Supabase.\n\nLa solicitud solamente continúa cuando contiene información válida y suficiente.\n\n## Actualización del reporte\n\nCuando la solicitud es correcta:\n\n1. Localiza el reporte correspondiente en Supabase.\n2. Cambia su estado a RESUELTO.\n3. Registra quién realizó la resolución.\n4. Guarda la fecha y hora del cierre.\n5. Conserva el comentario o información de seguimiento disponible.\n\nSupabase es la fuente oficial del estado del reporte dentro del portal.\n\n## Notificación al equipo TT\n\nDespués de actualizar el registro:\n\n1. Se prepara un correo para el equipo responsable de Trinidad y Tobago.\n2. Se incluyen los datos principales del reporte.\n3. Se informa que el caso fue marcado como resuelto.\n4. Se identifica al usuario que realizó el cierre.\n5. Se envía el correo mediante Gmail.\n\nLa notificación permite que el equipo conozca el cambio sin tener que revisar manualmente el portal.\n\n## Respuesta a la aplicación\n\nCuando el proceso termina correctamente:\n\n- Se construye una respuesta de éxito.\n- Se confirma que el reporte fue actualizado.\n- Se devuelve el nuevo estado a la aplicación.\n- El portal puede actualizar inmediatamente su interfaz.\n\n## Ruta de error\n\nCuando la solicitud es inválida:\n\n- No se modifica Supabase.\n- No se envía el correo al equipo TT.\n- Se devuelve una respuesta de error a la aplicación.\n- Se informa que faltan datos o que la solicitud no pudo procesarse.\n\n## Reglas\n\n- No marcar un reporte como resuelto sin un identificador válido.\n- No enviar la notificación antes de confirmar la actualización en Supabase.\n- No declarar éxito cuando la base de datos no fue actualizada.\n- Toda respuesta del Webhook debe cerrarse correctamente para evitar solicitudes abiertas.\n- La ruta de error debe terminar sin realizar cambios parciales.", - "height": 1264, - "width": 2624, - "color": "#28765F" - }, - "type": "n8n-nodes-base.stickyNote", - "typeVersion": 1, - "position": [ - -1536, - -480 - ], - "id": "e58c6673-d3ac-40e1-9b10-ae20e09730af", - "name": "Sticky Note" - } - ], - "pinData": {}, - "connections": { - "Webhook Marcar Resuelto": { - "main": [ - [ - { - "node": "Validar resolución", - "type": "main", - "index": 0 - } - ] - ] - }, - "Validar resolución": { - "main": [ - [ - { - "node": "¿Solicitud válida?", - "type": "main", - "index": 0 - } - ] - ] - }, - "¿Solicitud válida?": { - "main": [ - [ - { - "node": "Actualizar reporte Supabase", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Responder error resolución", - "type": "main", - "index": 0 - } - ] - ] - }, - "Actualizar reporte Supabase": { - "main": [ - [ - { - "node": "Preparar correo equipo TT", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar correo equipo TT": { - "main": [ - [ - { - "node": "Enviar correo equipo TT", - "type": "main", - "index": 0 - } - ] - ] - }, - "Enviar correo equipo TT": { - "main": [ - [ - { - "node": "Preparar respuesta resuelto", - "type": "main", - "index": 0 - } - ] - ] - }, - "Preparar respuesta resuelto": { - "main": [ - [ - { - "node": "Responder resuelto app", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1", - "binaryMode": "separate", - "availableInMCP": true, - "timeSavedMode": "fixed", - "errorWorkflow": "puF4LUczoSz3hcek", - "timezone": "America/Santo_Domingo", - "callerPolicy": "workflowsFromSameOwner" - }, - "versionId": "59895fdc-0362-4c8f-88d1-3f5e866711e0", - "meta": { - "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" - }, - "id": "aihmNdYFskfvf3XF", - "tags": [] -} \ No newline at end of file diff --git a/GENERAR_DIST_BONO14.bat b/GENERAR_DIST_BONO14.bat new file mode 100644 index 0000000..18dfbf1 --- /dev/null +++ b/GENERAR_DIST_BONO14.bat @@ -0,0 +1,10 @@ +@echo off +echo Generando dist actualizado de Cruce de Cuentas GLM con Bono 14... +call npm run build +if errorlevel 1 ( + echo. + echo Si aparece un error de dependencia nativa de Rollup, ejecuta npm ci y vuelve a intentar. + exit /b 1 +) +echo. +echo Dist generado correctamente. diff --git a/IMPLEMENTACION_BANCO_SIN_BAMBOO.md b/IMPLEMENTACION_BANCO_SIN_BAMBOO.md new file mode 100644 index 0000000..4db74e6 --- /dev/null +++ b/IMPLEMENTACION_BANCO_SIN_BAMBOO.md @@ -0,0 +1,26 @@ +# Banco sin BambooHR — correcciones persistentes + +Esta versión conserva el flujo existente de conciliación y amplía únicamente la gestión de falsos positivos de **Banco sin BambooHR**. + +## Qué cambia + +1. Cada reporte nuevo guarda en Supabase el detalle completo de `Banco sin BambooHR` asociado al ID histórico del reporte. +2. En **Históricos**, los reportes con casos de Banco sin Bamboo pueden reabrirse días después y cada caso puede enviarse a RRHH con **Reportar a RRHH**. +3. La aplicación consulta el estado de correcciones ya notificadas y muestra **Reportado a RRHH** para evitar repetir el mismo envío desde el mismo reporte. +4. El workflow de corrección BambooHR existente sigue siendo el responsable de validar la identidad, registrar aliases/equivalencias y enviar el correo a RRHH. No requiere cambios en esta entrega. +5. Los workflows generadores de Guatemala y Trinidad y Tobago devuelven ahora el `executionId` real del histórico para asociar la corrección al reporte exacto. + +## Robustez después de dejar la pestaña abierta + +Antes de una nueva ejecución, la aplicación revalida la sesión Supabase y reactiva de forma segura la conexión con el origen de n8n. Cada intento recibe además un `client_request_id` y el POST se envía sin caché. No se hace un reintento automático del cruce porque eso podría crear dos Google Sheets, dos históricos o dos correos si el primer POST sí llegó al servidor. + +El `dist` incluido contiene además una capa de compatibilidad para la versión compilada recibida: reactiva la conexión de n8n antes del POST y habilita la gestión histórica de Banco sin Bamboo sin modificar el bundle previo que ya estaba funcionando. + +## Orden de instalación + +1. Ejecutar `SUPABASE_Cruce_Cuentas_BambooHR_Historicos_READY.sql` en Supabase. +2. Reemplazar/importar los workflows actualizados de Guatemala y Trinidad y Tobago. +3. Mantener activo el workflow existente `Cruce de Cuentas - Reportar Corrección BambooHR a RRHH`. +4. Publicar el `dist` incluido o compilar nuevamente el proyecto en el entorno normal de desarrollo. + +Los reportes anteriores a esta migración pueden no tener detalle histórico de Banco sin Bamboo; los reportes nuevos sí lo conservarán automáticamente. diff --git a/IMPLEMENTACION_BONO14_GUATEMALA.md b/IMPLEMENTACION_BONO14_GUATEMALA.md new file mode 100644 index 0000000..c3e8c1b --- /dev/null +++ b/IMPLEMENTACION_BONO14_GUATEMALA.md @@ -0,0 +1,106 @@ +# Bono 14 Guatemala — Implementación final + +Esta versión incorpora **Bono 14** como un reporte anual independiente dentro +del módulo de Guatemala, sin mezclar sus importes con la nómina ordinaria. + +## Disponibilidad en la app + +La sección aparece únicamente con: + +- País: Guatemala +- Mes: Julio +- Tipo de cruce: Quincena 15 + +La ejecución normal de la Quincena 15 permanece independiente. + +## Archivos + +Bono 14 recibe: + +1. Un Excel anual de Bono 14. +2. Uno o varios CSV bancarios. + +El webhook independiente es: + +`bono14gt-bamboo-test` + +Variable frontend: + +`VITE_N8N_GUATEMALA_BONO14_WEBHOOK_URL` + +## Identidad del reporte + +El histórico utiliza: + +`GT-AAAA-07-bono14` + +Ejemplo 2026: + +`GT-2026-07-bono14` + +Si se procesa nuevamente el mismo año, se reutiliza el mismo Google Sheet y el +mismo histórico; las resoluciones que sigan correspondiendo a casos presentes +se conservan. + +## Período anual + +Para Bono 14 del año `AAAA`: + +- inicio: `AAAA-1-07-01` +- fin: `AAAA-06-30` + +Ejemplo 2026: `2025-07-01` a `2026-06-30`. + +## Lectura del Excel + +El workflow detecta dinámicamente las columnas principales de las siete hojas +canónicas y conserva índices de respaldo por compatibilidad. + +También identifica el **total oficial de cada hoja** y cierra la tabla en esa +fila. Esto evita incorporar filas auxiliares o de ajustes colocadas debajo del +total oficial. + +Antes de continuar, el workflow valida que la suma de empleados leída coincida +con el total oficial de cada hoja. Si no coincide, se detiene para evitar +generar un reporte incorrecto. + +Hojas canónicas: + +- P&G +- B14 MOTO +- B14 WP +- PMI +- B14 Nestle +- B14 GLM Q +- B14 GLM $ + +El flujo trabaja con QTZ y USD de forma separada. + +## Google Sheet + +El reporte genera: + +1. `01 Bono 14 vs Banco` +2. `02 Banco sin Bono 14` +3. `03 Banco sin Bamboo` +4. `04 Diferencias nombre banco` +5. `05 Cuenta Mal Digitada` (cuando aplica) +6. `06 Resumen` + +Conserva las mismas condiciones finales de Guatemala: protección de datos, +`Resolución` editable, reutilización del mismo reporte, limpieza de formatos +residuales y orden automático de pestañas. + +## Supabase + +No se crea una segunda tabla de históricos. Se reutiliza +`cruces_cuentas_gt_reportes` y Bono 14 se diferencia mediante: + +- `country = GT` +- `month = 7` +- `period_type = bono14` +- `report_identity = GT-AAAA-07-bono14` + +Ejecutar: + +`SUPABASE_Cruce_Cuentas_Bono14_Guatemala_READY.sql` diff --git a/IMPLEMENTACION_REPORTE_UNICO_PERIODO.md b/IMPLEMENTACION_REPORTE_UNICO_PERIODO.md new file mode 100644 index 0000000..69bd1ff --- /dev/null +++ b/IMPLEMENTACION_REPORTE_UNICO_PERIODO.md @@ -0,0 +1,7 @@ +# Reporte único por período + +Esta versión conserva un único reporte por **país + año + mes + tipo de período/quincena**. + +Cuando n8n detecta que el período ya existe en Supabase, reutiliza el `spreadsheet_id` existente, lee las resoluciones ya completadas, recalcula los datos, limpia y vuelve a escribir el mismo Google Sheet y conserva las resoluciones que todavía corresponden al mismo caso. El histórico se actualiza mediante `report_identity` en lugar de insertar otra fila. + +La aplicación también prioriza `updated_at` al ordenar Históricos para que una regeneración del mismo período vuelva a aparecer arriba sin crear otro registro. diff --git a/README.md b/README.md index bc78d28..163d871 100644 --- a/README.md +++ b/README.md @@ -1,566 +1,409 @@ -# Cruce de Cuentas GLM - Centralizado +# Cruce de Cuentas GLM -> Aplicación centralizada para automatizar y gestionar el cruce de cuentas de nómina contra archivos bancarios, validando empleados con BambooHR, generando reportes en Google Sheets y dejando una arquitectura preparada para incorporar progresivamente los demás países de GomezLee Marketing. +> Portal centralizado para acceder a los módulos de Cruce de Cuentas por país, permitiendo tener un único punto de entrada seguro para los portales regionales de conciliación. --- -## INFORMACIÓN GENERAL +## Información General + +| Campo | Detalle | +| ------------------------ | ------------------------------------ | +| Proyecto | Cruce de Cuentas GLM | +| Área | Nómina | +| Estado | Prototipo funcional / Portal central | +| Developer Principal | Isaac Aracena | +| IT Manager | Luis Matos | +| Product Owner | Máximo Gómez | -| Campo | Detalle | -|---|---| -| Proyecto | Cruce de Cuentas GLM - Centralizado | -| Área | Administración | -| Estado | Guatemala y Trinidad y Tobago completados | -| Developer Principal | Isaac Aracena | -| IT Manager | Luis Matos | --- -## OBJETIVO +## Objetivo ### Problema que resuelve -El proceso de validar que los pagos enviados al banco coincidan correctamente con la nómina requiere comparar archivos con estructuras diferentes, revisar cuentas bancarias, nombres, montos y empleados, y confirmar manualmente si una persona existe o no en BambooHR. - -Este trabajo manual consume tiempo, aumenta el riesgo de errores y hace más difícil identificar rápidamente diferencias reales de pago, cuentas mal digitadas, empleados pagados que no aparecen en la nómina o registros bancarios que no corresponden a empleados válidos. +Actualmente, los procesos de cruce de cuentas por país pueden quedar dispersos en diferentes aplicaciones, enlaces, flujos o repositorios. Esto dificulta el acceso ordenado, el control de permisos y la visibilidad de cuáles países tienen un portal activo, pendiente o en desarrollo. ### Solución implementada -Cruce de Cuentas GLM centraliza este proceso en una aplicación web. +Cruce de Cuentas GLM funciona como un portal central de acceso. Desde esta aplicación, los usuarios autorizados pueden iniciar sesión con Google, visualizar los países disponibles y acceder al portal correspondiente de cada país. -El usuario selecciona el país y período, carga la nómina y los archivos bancarios correspondientes, y el sistema procesa la información automáticamente mediante workflows de n8n. - -El resultado incluye: - -- Comparación de cuentas bancarias. -- Comparación de montos pagados contra nómina. -- Identificación de coincidencias. -- Identificación de discrepancias reales. -- Detección de posibles cuentas bancarias mal digitadas. -- Validación de empleados contra BambooHR. -- Identificación de pagos bancarios que no corresponden a empleados localizados en BambooHR. -- Detección de diferencias de nombres en los archivos bancarios. -- Generación automática de un Google Sheet estructurado. -- Historial de reportes y seguimiento de resolución desde la aplicación. -- Formato automático del reporte para evitar ajustes manuales de columnas y filas. - -La solución comenzó con Guatemala y Trinidad y Tobago y está diseñada para agregar progresivamente los demás países de GomezLee Marketing sin crear aplicaciones independientes. +Este repositorio no ejecuta el cruce de nómina ni procesa archivos bancarios. Su responsabilidad principal es centralizar el acceso y redireccionar a los portales operativos de cada país. ### Usuarios / Beneficiarios -- Equipos administrativos responsables de validar pagos y nóminas. -- Recursos Humanos. -- Personal autorizado para consultar históricos y dar seguimiento a discrepancias. -- IT, para soporte, mantenimiento y expansión del sistema a nuevos países. +* Equipo de Nómina. +* IT. +* Gerencia regional. +* Usuarios autorizados que necesiten acceder a los portales de cruce por país. --- -## ARQUITECTURA +## Estado Actual del Proyecto -### Diagrama de flujo +Este repositorio contiene un **portal frontend centralizado** desarrollado con React, Vite, TypeScript y Supabase Auth. -```text -[Usuario] - | - v -[App Web React / Vite] - | - +----> [Supabase Auth + Control de acceso] - | - v -[Selección de país + período + archivos] - | - v -[Webhook n8n del país] - | - +----> [Parseo de nómina] - | - +----> [Parseo de archivos bancarios] - | - +----> [BambooHR Custom Report - onlyCurrent=false] - | - v -[Normalización + Conciliación + Validaciones] - | - v -[Clasificación de resultados] - | - +----> Coincidencias - +----> Discrepancias - +----> Banco sin Bamboo - +----> Diferencias de Nombre - +----> Posibles cuentas mal digitadas - +----> Resumen - | - v -[Google Sheets] - | - +----> [Histórico / seguimiento en Supabase] - | - v -[Resultado mostrado en la aplicación] +La aplicación permite: + +* Iniciar sesión con Google mediante Supabase Auth. +* Validar correos autorizados. +* Mostrar una pantalla central con países disponibles. +* Visualizar tarjetas por país. +* Identificar el portal de Guatemala como módulo iniciado. +* Mostrar los demás países como módulos pendientes o no iniciados. +* Preparar la navegación hacia los portales independientes de cada país. + +--- + + +## Arquitectura + +### Arquitectura actual + +```txt +Usuario autorizado + ↓ +Google Login + ↓ +Supabase Auth + ↓ +Portal Central Cruce de Cuentas GLM + ↓ +Tarjetas por país + ↓ +Modal informativo / redirección futura ``` -### Stack tecnológico +### Arquitectura objetivo -| Componente | Tecnología | Propósito | -|---|---|---| -| Frontend | React + TypeScript + Vite | Aplicación web centralizada | -| Automatización | n8n | Orquestación de cruces y generación de reportes | -| Base de datos / Auth | Supabase / PostgreSQL | Autenticación, permisos e información persistente | -| RRHH | BambooHR API | Validación de empleados activos e inactivos | -| Reportes | Google Sheets API | Generación y formato de reportes | -| Repositorio | Gitea | Control de versiones | - -### Integraciones externas - -| Sistema | Tipo de integración | Datos que fluyen | -|---|---|---| -| Supabase | Auth + REST / RPC | Sesión, autorización, histórico y seguimiento | -| n8n | Webhook HTTP | Archivos de nómina, archivos bancarios y metadata del período | -| BambooHR | API REST | Empleados, estado, ubicación, fecha de ingreso y Employee Number | -| Google Sheets | Google API | Creación y formato del reporte final | -| Gitea | Git | Código fuente y control de versiones | +```txt +Usuario autorizado + ↓ +Portal Central Cruce de Cuentas GLM + ↓ +Selección de país + ↓ +Redirección al portal del país + ↓ +Portal específico ejecuta su propio flujo + ↓ +n8n / BambooHR / Banco / Nómina / Reportes +``` --- -## PAÍSES IMPLEMENTADOS +## Stack Tecnológico -### Guatemala - COMPLETADO - -El flujo de Guatemala procesa una nómina Excel con múltiples hojas y uno o varios archivos bancarios. - -Estado actual: - -- Cruce de nómina vs banco operativo. -- Validación BambooHR operativa. -- Se consultan empleados activos e inactivos mediante `onlyCurrent=false`. -- Matching de nombres optimizado para evitar bloqueos del task runner de n8n. -- Matching por Employee Number cuando existe evidencia disponible. -- Matching por nombres normalizados, alias y variantes controladas. -- No se fuerzan coincidencias ambiguas. -- Banco sin Bamboo validado con el dataset de regresión de junio 2026. -- Baseline validado del caso de prueba: **34 casos reales de Banco sin Bamboo**. -- Diferencias de nombre bancario incluidas en el reporte. -- Posibles cuentas mal digitadas incluidas. -- Columna de resolución incluida donde corresponde. -- Formato del Google Sheet automatizado. -- Altura de filas y ajuste de texto automático. -- Reporte final validado visualmente. - -### Trinidad y Tobago - COMPLETADO - -El flujo de Trinidad y Tobago procesa la estructura de nómina utilizada por el país y los archivos bancarios ACH correspondientes. - -Estado actual: - -- Cruce de nómina vs banco operativo. -- Validación BambooHR operativa. -- Empleados activos e inactivos incluidos. -- Normalización de nombres adaptada a nombres con apóstrofes, guiones y variantes. -- Matching BambooHR optimizado. -- Banco sin Bamboo validado con el dataset de regresión de junio 2026. -- Baseline validado del caso de prueba: **0 casos reales de Banco sin Bamboo**. -- Casos anteriormente problemáticos fueron reconocidos correctamente por BambooHR. -- Columna de resolución incluida donde corresponde. -- Formato automático del Google Sheet habilitado. -- Reporte final validado visualmente. - -### Próximos países - -La arquitectura no está limitada a GT y TT. - -Los siguientes países se incorporarán progresivamente reutilizando: - -1. El mismo frontend. -2. La misma autenticación. -3. El mismo control centralizado de acceso. -4. La misma estructura de histórico. -5. El mismo modelo de generación de reportes. -6. Un workflow específico por país cuando la estructura de nómina o banco lo requiera. +| Componente | Tecnología | Propósito | +| --------------- | ----------------------------- | -------------------------------------- | +| Frontend | React 19 | Interfaz del portal central | +| Build Tool | Vite | Desarrollo local y build de producción | +| Lenguaje | TypeScript | Tipado del frontend | +| Estilos | Tailwind CSS 4 | Diseño visual | +| Animaciones | Motion | Transiciones y animaciones | +| Iconos | Lucide React | Iconografía | +| Autenticación | Supabase Auth | Login con Google | +| Package Manager | npm | Gestión de dependencias | +| Infraestructura | Servidor GLM | Hosting del portal | --- -## REGLAS DE NEGOCIO +## Integraciones -1. El país seleccionado determina qué workflow de conciliación debe ejecutarse. -2. El período debe contener año, mes y tipo de período antes de ejecutar el cruce. -3. La nómina y los archivos bancarios deben analizarse manteniendo sus datos originales para trazabilidad. -4. Las cuentas bancarias se normalizan antes de compararse. -5. Los montos se comparan con precisión monetaria y tolerancias controladas. -6. Una coincidencia por nombre nunca debe forzarse si existen candidatos ambiguos. -7. BambooHR debe consultar empleados activos e inactivos mediante `onlyCurrent=false`. -8. Employee Number tiene prioridad cuando existe una referencia confiable que permita utilizarlo. -9. Las variaciones de nombre pueden resolverse mediante nombres completos, alias, normalización y reglas de similitud controladas. -10. Los casos ambiguos deben permanecer como pendientes de revisión en lugar de convertirse en falsos positivos. -11. Un registro de Banco sin Bamboo solo debe permanecer en esa categoría cuando no existe evidencia suficiente para asociarlo con una persona de BambooHR. -12. El reporte debe ser legible al generarse; el usuario no debe tener que expandir manualmente filas o columnas para visualizar información. -13. Los reportes históricos deben conservar su estado de resolución. -14. La incorporación de un nuevo país no debe requerir crear otra aplicación independiente. +### Integraciones actuales + +| Sistema | Tipo de integración | Uso | +| -------------- | -------------------- | ---------------------------- | +| Supabase Auth | OAuth con Google | Autenticación de usuarios | +| Google Login | Provider de Supabase | Inicio de sesión corporativo | +| Frontend React | SPA | Portal visual centralizado | + +### Integraciones futuras + +| Sistema | Tipo de integración | Uso | +| ----------------- | ------------------- | -------------------------------------------------------------- | +| Portales por país | Redirección por URL | Enviar al usuario al portal correspondiente | +| Supabase Database | Opcional | Configurar países, URLs, estado y permisos desde base de datos | +| n8n | Indirecta | Cada portal país podrá disparar su propio flujo de cruce | +| BambooHR | Indirecta | Será usado por el flujo del país, no por este portal central | --- -## CONFIGURACIÓN Y SETUP +## Regla Principal del Sistema + +Este portal solo centraliza accesos. + +```txt +Portal Central ≠ Motor de Cruce +Portal Central = Login + Selección de país + Redirección +``` + +La lógica operativa debe mantenerse separada por país para evitar mezclar reglas, formatos de nómina, monedas, bancos, excepciones y reportes. + +--- + +## Reglas de Negocio + +1. Solo usuarios autorizados pueden acceder al portal. + +2. El usuario debe iniciar sesión mediante Google. + +3. La autenticación se gestiona con Supabase Auth. + +4. Los correos autorizados se validan actualmente desde el frontend. + +5. El portal debe mostrar todos los países disponibles. + +6. Guatemala debe mostrarse como el primer módulo iniciado. + +7. Los países no implementados deben mostrarse como pendientes, no iniciados o deshabilitados. + +8. Al seleccionar un país activo, el sistema debe redireccionar al portal correspondiente. + +9. Al seleccionar un país no activo, el sistema debe informar que el módulo aún no está disponible. + +10. Este portal no debe procesar archivos de nómina ni banco. + +11. Este portal no debe ejecutar conciliaciones. + +12. Este portal no debe almacenar archivos sensibles. + +13. Cada portal país debe manejar su propia lógica, flujo de n8n y reglas de cruce. + +14. Las URLs de redirección deben mantenerse documentadas y controladas. + +15. Cualquier cambio de permisos debe ser validado por IT. + +--- + +Cualquier otro correo que inicie sesión con Google debe ser rechazado y cerrado automáticamente. + +> Nota: En una versión futura, esta validación debería moverse a una tabla de permisos en Supabase o a una política controlada por IT, para evitar mantener correos hardcodeados en el frontend. + +--- + +### Estado actual + +El portal usa Supabase Auth para autenticación, pero no requiere una base de datos compleja para operar como portal central. + +### Uso opcional de Supabase Database + +Si se desea evitar URLs hardcodeadas en el frontend, Supabase puede usarse para guardar configuración dinámica de portales. + +Tabla sugerida: + +```sql +create table country_portals ( + id uuid primary key default gen_random_uuid(), + country_name text not null, + country_code text not null unique, + portal_url text, + status text not null default 'pending', + is_visible boolean not null default true, + display_order int not null default 0, + created_at timestamptz default now(), + updated_at timestamptz default now() +); +``` + +Campos sugeridos: + +| Campo | Descripción | +| ------------- | ------------------------------------- | +| country_name | Nombre del país | +| country_code | Código del país | +| portal_url | URL del portal específico | +| status | Estado del portal | +| is_visible | Define si se muestra o no en pantalla | +| display_order | Orden visual | +| created_at | Fecha de creación | +| updated_at | Fecha de actualización | + +--- + +## Configuración y Setup ### Prerrequisitos -- Node.js compatible con el proyecto. -- npm. -- Acceso al repositorio Gitea. -- Acceso al proyecto Supabase. -- Acceso a n8n. -- Credenciales BambooHR configuradas en n8n. -- Credenciales Google configuradas en n8n. -- Acceso al servidor donde se publica el frontend. -- Workflows de los países habilitados en n8n. - -### Variables de entorno - -Las variables reales deben permanecer fuera del repositorio y documentarse mediante `.env.example`. - -Entre las configuraciones necesarias se encuentran: - -| Variable / configuración | Descripción | Dónde se obtiene | -|---|---|---| -| Supabase URL | URL del proyecto Supabase | Supabase | -| Supabase public/anon key | Clave pública utilizada por el frontend | Supabase | -| URLs de webhook n8n | Endpoints para ejecutar cada país | n8n | -| Redirect URLs de autenticación | URLs válidas de login/callback | Supabase Auth | - -> **Nunca commitear credenciales, service-role keys, contraseñas, API keys privadas ni secretos de BambooHR al repositorio.** - -### Control de acceso en Supabase - -El acceso centralizado de la aplicación utiliza la tabla: - -```text -public.cruce_cuentas_usuarios_autorizados -``` - -y las funciones/RPC implementadas para validar acceso: - -```text -cruce_cuentas_mi_acceso() -cruce_cuentas_tiene_acceso_app() -``` - -El frontend consulta Supabase para determinar si el usuario autenticado tiene acceso a la aplicación. +* Node.js v18 o superior. +* npm. +* Acceso al repositorio. +* Proyecto Supabase configurado. +* Supabase Auth con Google habilitado. +* Variables de entorno configuradas. +* URLs de redirección configuradas en Supabase. --- -## INSTALACIÓN / DESARROLLO LOCAL +## Variables de Entorno + +Crear un archivo `.env.local` en la raíz del proyecto basado en `.env.example`. + +```env +VITE_SUPABASE_URL="https://TU-PROYECTO.supabase.co" +VITE_SUPABASE_ANON_KEY="TU_SUPABASE_ANON_KEY" +``` + + +--- + +## Instalación / Ejecución Local + +### Instalar dependencias ```bash -git clone https://git.digitalcompass.agency/Isaac_Aracena/cruce-cuentas-glm-centralizado -cd cruce-cuentas-glm-centralizado - npm install ``` -Crear el archivo de entorno local a partir del ejemplo disponible en el repositorio: - -```bash -cp .env.example .env -``` - -Configurar las variables necesarias y ejecutar: +### Ejecutar en desarrollo ```bash npm run dev ``` -Para generar el build de producción: +La aplicación estará disponible en: + +```txt +http://localhost:3000 +``` + +### Generar build de producción ```bash npm run build ``` -El resultado se genera en: +### Preview del build -```text -/dist +```bash +npm run preview +``` + +### Validar TypeScript + +```bash +npm run lint +``` + +### Limpiar build + +```bash +npm run clean ``` --- -## DEPLOY +## Configuración de Vite -La aplicación se publica bajo: +El proyecto está configurado para desplegarse bajo la ruta: -```text -https://digitalcompass.agency/cruce-cuentas/ -``` - -Antes de publicar una nueva versión: - -```bash -npm install -npm run build -``` - -Luego debe desplegarse el contenido actualizado de `dist` en la ubicación correspondiente del servidor. - -### Importante - -El `base` de Vite debe mantenerse compatible con: - -```text +```txt /cruce-cuentas/ ``` -Después del deploy se debe comprobar: +En `vite.config.ts`: -- Login. -- Recuperación de sesión. -- Acceso autorizado/no autorizado. -- Carga de archivos. -- Ejecución de GT. -- Ejecución de TT. -- Apertura del Google Sheet generado. -- Histórico de reportes. -- Persistencia de resoluciones. - ---- - -## CÓMO FUNCIONA - -### Flujo paso a paso - -1. El usuario inicia sesión. -2. Supabase valida la sesión. -3. La aplicación consulta si el usuario está autorizado. -4. El usuario selecciona el país. -5. Selecciona año, mes y período. -6. Adjunta la nómina. -7. Adjunta los archivos bancarios requeridos. -8. La aplicación envía los datos al webhook de n8n correspondiente al país. -9. n8n extrae y normaliza las diferentes hojas de nómina. -10. n8n procesa los archivos bancarios. -11. BambooHR devuelve la base de empleados utilizando un reporte custom con empleados actuales e históricos. -12. Se ejecutan las reglas de matching y conciliación. -13. Se clasifican las coincidencias, discrepancias y casos de revisión. -14. Se genera el Google Sheet. -15. Se aplican automáticamente anchos, ajuste de texto y alturas de filas. -16. El enlace del reporte vuelve a la aplicación. -17. El usuario puede abrir el Google Sheet y consultar posteriormente el histórico. - -### Triggers - -| Trigger | Frecuencia | Descripción | -|---|---|---| -| Webhook GT | On demand | Ejecutado al procesar un cruce de Guatemala | -| Webhook TT | On demand | Ejecutado al procesar un cruce de Trinidad y Tobago | -| Futuros webhooks | On demand | Se agregarán al incorporar nuevos países | - ---- - -## TESTING - -### Casos de prueba mínimos - -| Caso | Input | Output esperado | Estado | -|---|---|---|---| -| Guatemala - junio 2026 Q30 | Nómina + archivos bancarios reales de prueba | 34 casos reales de Banco sin Bamboo | ✅ Validado | -| Trinidad y Tobago - junio 2026 Q30 | Nómina + archivo bancario real de prueba | 0 casos reales de Banco sin Bamboo | ✅ Validado | -| Empleado inactivo en BambooHR | Pago de persona histórica | Debe poder localizarse con `onlyCurrent=false` | ✅ Validado | -| Variación de nombre | Tildes, segundo nombre, apóstrofes o guiones | Match cuando existe evidencia suficiente | ✅ Validado | -| Nombre ambiguo | Dos candidatos posibles | No forzar match | ✅ Validado | -| Cuenta diferente con nombre y monto correctos | Nómina y banco con cuentas distintas | Clasificar como posible cuenta mal digitada | ✅ Validado | -| Diferencia de monto | Misma persona/cuenta con monto diferente | Crear discrepancia | ✅ Validado | -| Banco sin nómina | Pago bancario sin registro equivalente | Mostrar para revisión | ✅ Validado | -| Formato del reporte | Observaciones/nombres largos | Contenido visible sin expansión manual | ✅ Implementado | -| Rendimiento BambooHR | Miles de empleados | No bloquear el task runner de n8n | ✅ Optimizado | - -### Regresión obligatoria antes de cambios en BambooHR - -Cualquier modificación al matching de BambooHR debe volver a probar como mínimo: - -- Dataset GT de junio 2026. -- Dataset TT de junio 2026. -- Casos de nombres cortos. -- Casos de nombres completos. -- Empleados inactivos. -- Empleados con ubicación inconsistente. -- Nombres con tildes. -- Nombres con apóstrofes o guiones. -- Ambigüedades. -- Tiempo de ejecución del nodo Code. - -No se debe reducir globalmente el nivel de confianza únicamente para hacer desaparecer filas de Banco sin Bamboo. - ---- - -## ERRORES CONOCIDOS Y TROUBLESHOOTING - -| Error | Causa probable | Solución | -|---|---|---| -| `Task execution aborted because runner became unresponsive` | Código de matching recorriendo demasiados empleados/repeticiones | Mantener matching indexado y reutilizar resultados precalculados; no volver a búsquedas O(N×M) sobre toda la base | -| Banco sin Bamboo aumenta repentinamente | Reporte BambooHR incompleto o regresión del matching | Revisar `onlyCurrent=false`, normalizador y dataset de regresión | -| Falso positivo de BambooHR | Umbral demasiado permisivo o nombre ambiguo | Mantener reglas conservadoras y no aceptar candidatos sin evidencia suficiente | -| No abre el reporte | URL del Sheet no llegó correctamente al frontend | Revisar respuesta final del workflow y ejecución de n8n | -| Login vuelve a una ruta incorrecta | Redirect URL de Supabase no configurada | Revisar URLs permitidas para `/cruce-cuentas/` y callbacks | -| Contenido cortado en Google Sheets | Formato final no aplicado | Revisar las requests de wrap, ancho de columnas y auto-resize de filas | -| Workflow rojo en n8n | Error de input, credencial o nodo Code | Revisar `Executions` y el primer nodo que falla | - ---- - -## MONITOREO - -### n8n - -Revisar `Executions` ante cualquier reporte de error. - -- Verde = ejecución terminada correctamente. -- Rojo = identificar el primer nodo fallido. -- En problemas de BambooHR, revisar especialmente: - - HTTP BambooHR. - - Normalizar BambooHR. - - Cruzar Nómina vs Banco. - -### Supabase - -Revisar: - -- Sesiones. -- Usuarios autorizados. -- Histórico. -- Errores de RLS/RPC cuando corresponda. - -### Output esperado - -Una ejecución correcta debe: - -1. Terminar sin errores. -2. Generar un Google Sheet. -3. Devolver la URL del reporte a la aplicación. -4. Mostrar un reporte legible y completamente formateado. -5. Mantener únicamente diferencias reales o casos que requieren revisión humana. - ---- - -## ESTRUCTURA DEL REPOSITORIO - -Estructura principal esperada/actual del frontend: - -```text -/cruce-cuentas-glm-centralizado -|-- README.md -|-- package.json -|-- vite.config.ts -|-- .env.example -|-- /src -| |-- componentes y lógica de la aplicación -| └-- integraciones del frontend -|-- /public -|-- /dist -| └-- build utilizado para producción -└-- ... +```ts +base: "/cruce-cuentas/" ``` -Los workflows de n8n deben mantenerse exportados y versionados de forma controlada durante la evolución del proyecto. +URL de producción esperada: + +```txt +https://digitalcompass.agency/cruce-cuentas/ +``` --- -## CHANGELOG +## Supabase Auth -### 2026-08-08 - Estado actual - -- Guatemala completado funcionalmente. -- Trinidad y Tobago completado funcionalmente. -- Frontend centralizado para múltiples países. -- Autenticación y control de acceso mediante Supabase. -- Histórico y seguimiento integrados. -- Validación BambooHR con empleados activos e inactivos. -- Matching BambooHR optimizado para rendimiento y precisión. -- Baseline GT: 34 casos reales de Banco sin Bamboo. -- Baseline TT: 0 casos reales de Banco sin Bamboo. -- Formato automático de Google Sheets implementado. -- Arquitectura preparada para incorporar países adicionales. +En Supabase Auth se debe configurar Google como proveedor de autenticación. --- -## DECISIONS LOG +## Decisions Log -### DEC-001 - Una sola aplicación para todos los países +### DEC-001 — Crear un portal central en lugar de mezclar todos los cruces -- **Contexto:** El proceso de cruce se repetirá en diferentes países. -- **Opciones consideradas:** Una aplicación por país vs una aplicación centralizada. -- **Decisión:** Mantener una única aplicación y agregar lógica/workflows por país. -- **Razón:** Facilita mantenimiento, acceso, histórico, despliegue y crecimiento. +* Fecha: Junio 2026. +* Contexto: Cada país puede tener reglas, bancos, formatos y flujos distintos. +* Opciones consideradas: Un único sistema para todos los países vs portal central con módulos separados. +* Decisión: Crear un portal central que redireccione a los portales por país. +* Razón: Permite centralizar el acceso sin mezclar la lógica operativa de cada país. -### DEC-002 - Supabase como control centralizado de acceso +### DEC-002 — Usar Supabase Auth para el acceso -- **Contexto:** El acceso no debe depender de listas hardcodeadas en el frontend. -- **Decisión:** Mantener usuarios autorizados en Supabase. -- **Razón:** Permite agregar o retirar acceso sin recompilar la aplicación. +* Fecha: Junio 2026. +* Contexto: El portal necesita login seguro con usuarios autorizados. +* Opciones consideradas: Login manual vs Google SSO vía Supabase. +* Decisión: Usar Supabase Auth con Google. +* Razón: Aprovecha autenticación existente, reduce complejidad y permite controlar acceso. -### DEC-003 - Consultar históricos de BambooHR +### DEC-003 — Guatemala como primer país iniciado -- **Contexto:** Un pago puede corresponder a una persona actualmente inactiva. -- **Decisión:** Utilizar el reporte custom de BambooHR con `onlyCurrent=false`. -- **Razón:** Banco sin Bamboo debe significar realmente que no existe evidencia suficiente en BambooHR, no simplemente que el empleado está inactivo. +* Fecha: Junio 2026. +* Contexto: El primer flujo operativo de cruce se está trabajando para Guatemala. +* Opciones consideradas: Activar todos los países vs iniciar con Guatemala. +* Decisión: Marcar Guatemala como primer módulo iniciado. +* Razón: Permite validar la arquitectura antes de escalar a otros países. -### DEC-004 - Matching BambooHR conservador +### DEC-004 — Separar portal central del motor de cruce -- **Contexto:** Nombres pueden variar entre nómina, banco y BambooHR. -- **Decisión:** Combinar Employee Number, nombres normalizados, aliases y matching indexado, manteniendo reglas estrictas para ambigüedades. -- **Razón:** Reducir falsos negativos sin generar falsos positivos. +* Fecha: Junio 2026. +* Contexto: El portal central no debe cargar lógica pesada ni procesar datos sensibles. +* Opciones consideradas: Procesar cruces desde este portal vs redireccionar a portales especializados. +* Decisión: Este repositorio solo centraliza y redirecciona. +* Razón: Mejora seguridad, mantenimiento y escalabilidad. -### DEC-005 - Precalcular e indexar búsquedas de BambooHR +### DEC-005 — Mantener países pendientes visibles -- **Contexto:** Comparar cada fila contra miles de empleados provocó bloqueos del task runner. -- **Decisión:** Indexar candidatos y reutilizar resoluciones precalculadas. -- **Razón:** Mantener tiempos de ejecución estables. - -### DEC-006 - Formato del reporte completamente automático - -- **Contexto:** Algunas celdas quedaban cortadas y requerían ajustes manuales. -- **Decisión:** Aplicar wrap, anchos definidos y auto-resize de filas durante la creación del Google Sheet. -- **Razón:** El reporte debe quedar listo para uso inmediatamente después de generarse. - -### DEC-007 - Expansión progresiva a los demás países - -- **Fecha:** 2026-08-08 -- **Contexto:** GT y TT son únicamente la primera etapa. -- **Decisión:** Continuar incorporando países dentro de la misma plataforma. -- **Razón:** Mantener una solución GLM centralizada y escalable. +* Fecha: Junio 2026. +* Contexto: Se quiere mostrar la visión regional completa aunque solo algunos módulos estén activos. +* Opciones consideradas: Ocultar países no activos vs mostrarlos como pendientes. +* Decisión: Mostrar países pendientes con aviso de no iniciado. +* Razón: Comunica el roadmap regional sin habilitar funciones incompletas. --- -## CONTACTOS DEL PROYECTO +## Contactos del Proyecto -| Rol | Nombre | Contacto | -|---|---|---| -| IT Manager | Luis Matos | lmatos@gomezleemarketing.com | -| Developer Principal | Isaac Aracena | iaracena@gomezleemarketing.com | +| Rol | Nombre | +| -------------------- | ------------- | +| Product Owner | Máximo Gomez | +| IT Manager | Luis Matos | +| Developer | Isaac Aracena | +| Usuarios autorizados | A completar | --- -## DEFINITION OF DONE +## Definition of Done -### Alcance actual - Guatemala y Trinidad y Tobago - -- [x] Frontend centralizado operativo. -- [x] Login mediante Supabase. -- [x] Acceso centralizado mediante base de datos. -- [x] Workflow Guatemala operativo. -- [x] Workflow Trinidad y Tobago operativo. -- [x] Cruce de cuentas y montos. -- [x] Validación BambooHR. -- [x] Empleados activos e inactivos incluidos. -- [x] Matching optimizado para no bloquear n8n. -- [x] Reporte Google Sheets generado automáticamente. -- [x] Banco sin Bamboo validado contra datasets de regresión. -- [x] Formato automático de filas y columnas. -- [x] Histórico disponible. -- [x] Código frontend versionado en Gitea. -- [x] Pruebas con datos reales de referencia para GT y TT. -- [x] Documentar Board de ejecución definitivo. -- [x] Documentar PRD definitivo en el repositorio. -- [x] Mantener `.env.example` sincronizado con las variables utilizadas. -- [x] Versionar los exports finales de n8n en la estructura definitiva del repositorio. -- [x] Registrar validación/cierre formal del negocio cuando corresponda. +* README completo y actualizado. +* Portal carga correctamente en local. +* Portal carga correctamente en `/cruce-cuentas/`. +* Supabase Auth configurado. +* Google Login funcionando. +* Correos autorizados validados. +* Usuario no autorizado bloqueado. +* Tarjetas de países visibles. +* Guatemala configurado como módulo iniciado. +* Países pendientes muestran aviso correcto. +* Países activos redireccionan a su portal correspondiente. +* Variables de entorno documentadas en `.env.example`. +* Código commiteado y pusheado al repositorio. +* Probado en ambiente real, no solo local. +* Luis Matos validó el acceso. +* Product Owner aprobó el resultado final. --- -**Documento mantenido por el equipo GLM IT.** +--- + +## Bono 14 — Guatemala + +El módulo de Guatemala incluye un cruce anual independiente de **Bono 14** +visible únicamente para **Julio · Quincena 15**. Utiliza un Excel anual y CSV +bancarios propios, genera un Google Sheet/histórico separado y reutiliza la +identidad `GT-AAAA-07-bono14` cuando el mismo año se vuelve a procesar. + +Ver `IMPLEMENTACION_BONO14_GUATEMALA.md`. diff --git a/SUPABASE_Cruce_Cuentas_Alias_BambooHR.sql b/SUPABASE_Cruce_Cuentas_Alias_BambooHR.sql new file mode 100644 index 0000000..4d09a6f --- /dev/null +++ b/SUPABASE_Cruce_Cuentas_Alias_BambooHR.sql @@ -0,0 +1,407 @@ +-- Cruce de Cuentas GLM +-- Equivalencias validadas para falsos positivos de "Banco sin BambooHR" +-- Ejecutar en el SQL Editor del Supabase empresarial. +-- Script idempotente: puede ejecutarse nuevamente sin borrar datos existentes. + +begin; + +create extension if not exists pgcrypto; + +create or replace function public.cruce_cuentas_normalizar_nombre(p_value text) +returns text +language sql +immutable +strict +as $$ + select trim( + regexp_replace( + translate( + lower(coalesce(p_value, '')), + 'áàäâãåéèëêíìïîóòöôõúùüûñçÁÀÄÂÃÅÉÈËÊÍÌÏÎÓÒÖÔÕÚÙÜÛÑÇ', + 'aaaaaaeeeeiiiiooooouuuuncAAAAAAEEEEIIIIOOOOOUUUUNC' + ), + '[^a-z0-9]+', + ' ', + 'g' + ) + ); +$$; + +create table if not exists public.cruce_cuentas_bamboo_aliases ( + id uuid primary key default gen_random_uuid(), + pais text not null check (pais in ('GT', 'TT')), + nombre_origen text not null, + nombre_origen_normalizado text not null, + cuenta_bancaria text not null default '', + bamboo_employee_number text, + bamboo_employee_id text, + bamboo_nombre text not null, + motivo text, + comentario text, + activo boolean not null default true, + validado_por_email text not null, + validado_por_nombre text, + last_reported_at timestamptz not null default now(), + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Compatibilidad si una versión anterior del script creó la tabla sin cuenta. +alter table public.cruce_cuentas_bamboo_aliases + add column if not exists cuenta_bancaria text not null default ''; + +drop index if exists public.ux_cruce_cuentas_bamboo_aliases_pais_nombre; + +create unique index if not exists ux_cruce_cuentas_bamboo_aliases_pais_nombre_cuenta + on public.cruce_cuentas_bamboo_aliases (pais, nombre_origen_normalizado, cuenta_bancaria); + +create index if not exists ix_cruce_cuentas_bamboo_aliases_employee_number + on public.cruce_cuentas_bamboo_aliases (pais, bamboo_employee_number) + where activo = true and bamboo_employee_number is not null; + +create table if not exists public.cruce_cuentas_bamboo_correcciones ( + id uuid primary key default gen_random_uuid(), + pais text not null check (pais in ('GT', 'TT')), + nombre_origen_principal text not null, + nombres_origen text[] not null default array[]::text[], + cuenta_bancaria text, + monto_banco numeric(18,2), + moneda text, + bamboo_employee_number text, + bamboo_employee_id text, + bamboo_nombre text not null, + motivo text not null, + comentario text, + execution_id text, + report_url text, + period_label text, + period_start date, + period_end date, + solicitado_por_email text not null, + solicitado_por_nombre text, + destinatarios_rrhh text[] not null default array[]::text[], + alias_ids uuid[] not null default array[]::uuid[], + estado text not null default 'pendiente_notificacion' + check (estado in ('pendiente_notificacion', 'notificado_rrhh', 'corregido_bamboo', 'cancelado')), + correo_enviado boolean not null default false, + correo_enviado_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists ix_cruce_cuentas_bamboo_correcciones_pais_created + on public.cruce_cuentas_bamboo_correcciones (pais, created_at desc); + +create index if not exists ix_cruce_cuentas_bamboo_correcciones_estado + on public.cruce_cuentas_bamboo_correcciones (estado, created_at desc); + +create or replace function public.cruce_cuentas_bamboo_set_normalized() +returns trigger +language plpgsql +set search_path = public +as $$ +begin + new.pais := upper(trim(new.pais)); + new.nombre_origen := trim(new.nombre_origen); + new.nombre_origen_normalizado := public.cruce_cuentas_normalizar_nombre(new.nombre_origen); + new.cuenta_bancaria := regexp_replace(coalesce(new.cuenta_bancaria, ''), '[^0-9A-Za-z]+', '', 'g'); + new.validado_por_email := lower(trim(new.validado_por_email)); + new.updated_at := now(); + return new; +end; +$$; + +drop trigger if exists trg_cruce_cuentas_bamboo_alias_normalized + on public.cruce_cuentas_bamboo_aliases; + +create trigger trg_cruce_cuentas_bamboo_alias_normalized +before insert or update on public.cruce_cuentas_bamboo_aliases +for each row execute function public.cruce_cuentas_bamboo_set_normalized(); + +create or replace function public.cruce_cuentas_bamboo_touch_updated_at() +returns trigger +language plpgsql +set search_path = public +as $$ +begin + new.updated_at := now(); + return new; +end; +$$; + +drop trigger if exists trg_cruce_cuentas_bamboo_correcciones_updated_at + on public.cruce_cuentas_bamboo_correcciones; + +create trigger trg_cruce_cuentas_bamboo_correcciones_updated_at +before update on public.cruce_cuentas_bamboo_correcciones +for each row execute function public.cruce_cuentas_bamboo_touch_updated_at(); + +-- RPC consumida únicamente por los workflows generadores de reportes. +-- Siempre devuelve un objeto con aliases=[], incluso cuando no existen registros. +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'); +$$; + +alter table public.cruce_cuentas_bamboo_aliases enable row level security; +alter table public.cruce_cuentas_bamboo_correcciones enable row level security; + +-- El navegador nunca escribe ni lee estas tablas directamente. +-- Toda operación pasa por n8n con service_role. +revoke all on table public.cruce_cuentas_bamboo_aliases from anon, authenticated; +revoke all on table public.cruce_cuentas_bamboo_correcciones from anon, authenticated; +grant all on table public.cruce_cuentas_bamboo_aliases to service_role; +grant all on table public.cruce_cuentas_bamboo_correcciones to service_role; + +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; + +-- ============================================================ +-- HISTÓRICOS: conservar y recuperar Banco sin Bamboo por reporte +-- ============================================================ + +-- Los workflows ya envían el detalle de cada caso. Esta columna hace que el +-- detalle quede persistido y pueda abrirse días después desde Históricos. +alter table public.cruces_cuentas_gt_reportes + add column if not exists detalle_banco_sin_bamboo jsonb not null default '[]'::jsonb; + +-- Identificador generado por el navegador para trazabilidad de una ejecución. +-- No se usa como llave única porque no se reintenta automáticamente un POST +-- de conciliación (evita duplicar reportes, correos o Sheets). +alter table public.cruces_cuentas_gt_reportes + add column if not exists client_request_id text; + +update public.cruces_cuentas_gt_reportes +set detalle_banco_sin_bamboo = '[]'::jsonb +where detalle_banco_sin_bamboo is null; + +create index if not exists ix_cruces_cuentas_reportes_client_request + on public.cruces_cuentas_gt_reportes (client_request_id) + where client_request_id is not null; + +create index if not exists ix_cruce_cuentas_bamboo_correcciones_execution + on public.cruce_cuentas_bamboo_correcciones (execution_id, created_at desc) + where execution_id is not null; + +-- Reemplaza la RPC histórica manteniendo exactamente la misma firma y +-- estructura pública, pero excluye detalle_banco_sin_bamboo de la lista para +-- no descargar todos los casos cada vez que se abre Históricos. El detalle se +-- solicita únicamente cuando el usuario pulsa Banco sin Bamboo. +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') 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; + +-- RPC de detalle utilizada por Históricos al abrir un reporte específico. +-- No otorga SELECT directo sobre las tablas de aliases/correcciones. +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') 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; + +-- Validaciones opcionales después de ejecutar: +-- select public.cruce_cuentas_bamboo_aliases_activos('GT'); +-- select public.cruce_cuentas_bamboo_aliases_activos('TT'); +-- select column_name, data_type from information_schema.columns +-- where table_schema='public' and table_name='cruces_cuentas_gt_reportes' +-- and column_name in ('detalle_banco_sin_bamboo', 'client_request_id'); diff --git a/SUPABASE_Cruce_Cuentas_BambooHR_Historicos_READY.sql b/SUPABASE_Cruce_Cuentas_BambooHR_Historicos_READY.sql new file mode 100644 index 0000000..4d09a6f --- /dev/null +++ b/SUPABASE_Cruce_Cuentas_BambooHR_Historicos_READY.sql @@ -0,0 +1,407 @@ +-- Cruce de Cuentas GLM +-- Equivalencias validadas para falsos positivos de "Banco sin BambooHR" +-- Ejecutar en el SQL Editor del Supabase empresarial. +-- Script idempotente: puede ejecutarse nuevamente sin borrar datos existentes. + +begin; + +create extension if not exists pgcrypto; + +create or replace function public.cruce_cuentas_normalizar_nombre(p_value text) +returns text +language sql +immutable +strict +as $$ + select trim( + regexp_replace( + translate( + lower(coalesce(p_value, '')), + 'áàäâãåéèëêíìïîóòöôõúùüûñçÁÀÄÂÃÅÉÈËÊÍÌÏÎÓÒÖÔÕÚÙÜÛÑÇ', + 'aaaaaaeeeeiiiiooooouuuuncAAAAAAEEEEIIIIOOOOOUUUUNC' + ), + '[^a-z0-9]+', + ' ', + 'g' + ) + ); +$$; + +create table if not exists public.cruce_cuentas_bamboo_aliases ( + id uuid primary key default gen_random_uuid(), + pais text not null check (pais in ('GT', 'TT')), + nombre_origen text not null, + nombre_origen_normalizado text not null, + cuenta_bancaria text not null default '', + bamboo_employee_number text, + bamboo_employee_id text, + bamboo_nombre text not null, + motivo text, + comentario text, + activo boolean not null default true, + validado_por_email text not null, + validado_por_nombre text, + last_reported_at timestamptz not null default now(), + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +-- Compatibilidad si una versión anterior del script creó la tabla sin cuenta. +alter table public.cruce_cuentas_bamboo_aliases + add column if not exists cuenta_bancaria text not null default ''; + +drop index if exists public.ux_cruce_cuentas_bamboo_aliases_pais_nombre; + +create unique index if not exists ux_cruce_cuentas_bamboo_aliases_pais_nombre_cuenta + on public.cruce_cuentas_bamboo_aliases (pais, nombre_origen_normalizado, cuenta_bancaria); + +create index if not exists ix_cruce_cuentas_bamboo_aliases_employee_number + on public.cruce_cuentas_bamboo_aliases (pais, bamboo_employee_number) + where activo = true and bamboo_employee_number is not null; + +create table if not exists public.cruce_cuentas_bamboo_correcciones ( + id uuid primary key default gen_random_uuid(), + pais text not null check (pais in ('GT', 'TT')), + nombre_origen_principal text not null, + nombres_origen text[] not null default array[]::text[], + cuenta_bancaria text, + monto_banco numeric(18,2), + moneda text, + bamboo_employee_number text, + bamboo_employee_id text, + bamboo_nombre text not null, + motivo text not null, + comentario text, + execution_id text, + report_url text, + period_label text, + period_start date, + period_end date, + solicitado_por_email text not null, + solicitado_por_nombre text, + destinatarios_rrhh text[] not null default array[]::text[], + alias_ids uuid[] not null default array[]::uuid[], + estado text not null default 'pendiente_notificacion' + check (estado in ('pendiente_notificacion', 'notificado_rrhh', 'corregido_bamboo', 'cancelado')), + correo_enviado boolean not null default false, + correo_enviado_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists ix_cruce_cuentas_bamboo_correcciones_pais_created + on public.cruce_cuentas_bamboo_correcciones (pais, created_at desc); + +create index if not exists ix_cruce_cuentas_bamboo_correcciones_estado + on public.cruce_cuentas_bamboo_correcciones (estado, created_at desc); + +create or replace function public.cruce_cuentas_bamboo_set_normalized() +returns trigger +language plpgsql +set search_path = public +as $$ +begin + new.pais := upper(trim(new.pais)); + new.nombre_origen := trim(new.nombre_origen); + new.nombre_origen_normalizado := public.cruce_cuentas_normalizar_nombre(new.nombre_origen); + new.cuenta_bancaria := regexp_replace(coalesce(new.cuenta_bancaria, ''), '[^0-9A-Za-z]+', '', 'g'); + new.validado_por_email := lower(trim(new.validado_por_email)); + new.updated_at := now(); + return new; +end; +$$; + +drop trigger if exists trg_cruce_cuentas_bamboo_alias_normalized + on public.cruce_cuentas_bamboo_aliases; + +create trigger trg_cruce_cuentas_bamboo_alias_normalized +before insert or update on public.cruce_cuentas_bamboo_aliases +for each row execute function public.cruce_cuentas_bamboo_set_normalized(); + +create or replace function public.cruce_cuentas_bamboo_touch_updated_at() +returns trigger +language plpgsql +set search_path = public +as $$ +begin + new.updated_at := now(); + return new; +end; +$$; + +drop trigger if exists trg_cruce_cuentas_bamboo_correcciones_updated_at + on public.cruce_cuentas_bamboo_correcciones; + +create trigger trg_cruce_cuentas_bamboo_correcciones_updated_at +before update on public.cruce_cuentas_bamboo_correcciones +for each row execute function public.cruce_cuentas_bamboo_touch_updated_at(); + +-- RPC consumida únicamente por los workflows generadores de reportes. +-- Siempre devuelve un objeto con aliases=[], incluso cuando no existen registros. +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'); +$$; + +alter table public.cruce_cuentas_bamboo_aliases enable row level security; +alter table public.cruce_cuentas_bamboo_correcciones enable row level security; + +-- El navegador nunca escribe ni lee estas tablas directamente. +-- Toda operación pasa por n8n con service_role. +revoke all on table public.cruce_cuentas_bamboo_aliases from anon, authenticated; +revoke all on table public.cruce_cuentas_bamboo_correcciones from anon, authenticated; +grant all on table public.cruce_cuentas_bamboo_aliases to service_role; +grant all on table public.cruce_cuentas_bamboo_correcciones to service_role; + +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; + +-- ============================================================ +-- HISTÓRICOS: conservar y recuperar Banco sin Bamboo por reporte +-- ============================================================ + +-- Los workflows ya envían el detalle de cada caso. Esta columna hace que el +-- detalle quede persistido y pueda abrirse días después desde Históricos. +alter table public.cruces_cuentas_gt_reportes + add column if not exists detalle_banco_sin_bamboo jsonb not null default '[]'::jsonb; + +-- Identificador generado por el navegador para trazabilidad de una ejecución. +-- No se usa como llave única porque no se reintenta automáticamente un POST +-- de conciliación (evita duplicar reportes, correos o Sheets). +alter table public.cruces_cuentas_gt_reportes + add column if not exists client_request_id text; + +update public.cruces_cuentas_gt_reportes +set detalle_banco_sin_bamboo = '[]'::jsonb +where detalle_banco_sin_bamboo is null; + +create index if not exists ix_cruces_cuentas_reportes_client_request + on public.cruces_cuentas_gt_reportes (client_request_id) + where client_request_id is not null; + +create index if not exists ix_cruce_cuentas_bamboo_correcciones_execution + on public.cruce_cuentas_bamboo_correcciones (execution_id, created_at desc) + where execution_id is not null; + +-- Reemplaza la RPC histórica manteniendo exactamente la misma firma y +-- estructura pública, pero excluye detalle_banco_sin_bamboo de la lista para +-- no descargar todos los casos cada vez que se abre Históricos. El detalle se +-- solicita únicamente cuando el usuario pulsa Banco sin Bamboo. +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') 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; + +-- RPC de detalle utilizada por Históricos al abrir un reporte específico. +-- No otorga SELECT directo sobre las tablas de aliases/correcciones. +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') 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; + +-- Validaciones opcionales después de ejecutar: +-- select public.cruce_cuentas_bamboo_aliases_activos('GT'); +-- select public.cruce_cuentas_bamboo_aliases_activos('TT'); +-- select column_name, data_type from information_schema.columns +-- where table_schema='public' and table_name='cruces_cuentas_gt_reportes' +-- and column_name in ('detalle_banco_sin_bamboo', 'client_request_id'); diff --git a/SUPABASE_Cruce_Cuentas_Bono14_Guatemala_READY.sql b/SUPABASE_Cruce_Cuentas_Bono14_Guatemala_READY.sql new file mode 100644 index 0000000..9af24ef --- /dev/null +++ b/SUPABASE_Cruce_Cuentas_Bono14_Guatemala_READY.sql @@ -0,0 +1,303 @@ +-- ============================================================ +-- Cruce de Cuentas GLM - Bono 14 Guatemala +-- Script incremental / idempotente +-- ============================================================ +-- Objetivo: +-- Habilitar y verificar que Bono 14 use la MISMA infraestructura +-- de reporte único ya instalada, pero con una identidad independiente: +-- +-- GT--07-bono14 +-- +-- Ejemplo: +-- GT-2026-07-bono14 +-- +-- No crea otra tabla de históricos y no elimina datos existentes. +-- Puede ejecutarse nuevamente sin borrar reportes. +-- ============================================================ + +begin; + +-- ------------------------------------------------------------ +-- 1. Validación de prerrequisitos de la versión "reporte único" +-- ------------------------------------------------------------ +do $$ +declare + v_missing text[] := array[]::text[]; +begin + if to_regclass('public.cruces_cuentas_gt_reportes') is null then + raise exception + 'No existe public.cruces_cuentas_gt_reportes. Instala primero la estructura base de Cruce de Cuentas.'; + end if; + + if not exists ( + select 1 + from information_schema.columns + where table_schema = 'public' + and table_name = 'cruces_cuentas_gt_reportes' + and column_name = 'report_identity' + ) then + v_missing := array_append(v_missing, 'report_identity'); + end if; + + if not exists ( + select 1 + from information_schema.columns + where table_schema = 'public' + and table_name = 'cruces_cuentas_gt_reportes' + and column_name = 'updated_at' + ) then + v_missing := array_append(v_missing, 'updated_at'); + end if; + + if not exists ( + select 1 + from information_schema.columns + where table_schema = 'public' + and table_name = 'cruces_cuentas_gt_reportes' + and column_name = 'country' + ) then + v_missing := array_append(v_missing, 'country'); + end if; + + if not exists ( + select 1 + from information_schema.columns + where table_schema = 'public' + and table_name = 'cruces_cuentas_gt_reportes' + and column_name = 'year' + ) then + v_missing := array_append(v_missing, 'year'); + end if; + + if not exists ( + select 1 + from information_schema.columns + where table_schema = 'public' + and table_name = 'cruces_cuentas_gt_reportes' + and column_name = 'month' + ) then + v_missing := array_append(v_missing, 'month'); + end if; + + if not exists ( + select 1 + from information_schema.columns + where table_schema = 'public' + and table_name = 'cruces_cuentas_gt_reportes' + and column_name = 'period_type' + ) then + v_missing := array_append(v_missing, 'period_type'); + end if; + + if cardinality(v_missing) > 0 then + raise exception + 'Faltan columnas requeridas en public.cruces_cuentas_gt_reportes: %. Ejecuta primero el script de REPORTE ÚNICO POR PERÍODO.', + array_to_string(v_missing, ', '); + end if; +end; +$$; + +-- ------------------------------------------------------------ +-- 2. Constructor canónico de identidad +-- ------------------------------------------------------------ +-- Se mantiene genérico para nómina GT, nómina TT y Bono 14. +create or replace function public.cruce_cuentas_build_report_identity( + p_country text, + p_year integer, + p_month integer, + p_period_type text +) +returns text +language sql +immutable +as $$ + select + upper(trim(coalesce(p_country, 'GT'))) + || '-' || + coalesce(p_year, 0)::text + || '-' || + lpad(coalesce(p_month, 0)::text, 2, '0') + || '-' || + lower(trim(coalesce(p_period_type, 'periodo'))); +$$; + +-- ------------------------------------------------------------ +-- 3. Trigger de identidad / updated_at +-- ------------------------------------------------------------ +-- Reinstalado de forma idempotente para garantizar que un UPSERT de +-- Bono 14 mantenga la misma identidad y actualice updated_at. +create or replace function public.cruce_cuentas_set_report_identity() +returns trigger +language plpgsql +set search_path = public +as $$ +declare + v_canonical text; +begin + v_canonical := + public.cruce_cuentas_build_report_identity( + coalesce(new.country, 'GT'), + new.year, + new.month, + new.period_type + ); + + if tg_op = 'INSERT' then + if nullif(trim(coalesce(new.report_identity, '')), '') is null then + new.report_identity := v_canonical; + end if; + else + -- Los duplicados históricos antiguos conservan su sufijo :legacy:. + if coalesce(old.report_identity, '') like '%:legacy:%' then + new.report_identity := old.report_identity; + else + new.report_identity := v_canonical; + end if; + end if; + + new.updated_at := now(); + return new; +end; +$$; + +drop trigger if exists trg_cruce_cuentas_report_identity + on public.cruces_cuentas_gt_reportes; + +create trigger trg_cruce_cuentas_report_identity +before insert or update on public.cruces_cuentas_gt_reportes +for each row +execute function public.cruce_cuentas_set_report_identity(); + +-- ------------------------------------------------------------ +-- 4. Garantizar unicidad por identidad +-- ------------------------------------------------------------ +-- La versión de reporte único ya debe tener este índice. Se valida antes +-- de recrearlo para no alterar ni eliminar históricos existentes. +do $$ +begin + if not exists ( + select 1 + from pg_indexes + where schemaname = 'public' + and tablename = 'cruces_cuentas_gt_reportes' + and indexname = 'ux_cruces_cuentas_reportes_report_identity' + ) then + if exists ( + select report_identity + from public.cruces_cuentas_gt_reportes + where report_identity is not null + group by report_identity + having count(*) > 1 + ) then + raise exception + 'Existen report_identity duplicados. Ejecuta primero el script de REPORTE ÚNICO POR PERÍODO para aplicar su backfill seguro antes de Bono 14.'; + end if; + + create unique index ux_cruces_cuentas_reportes_report_identity + on public.cruces_cuentas_gt_reportes (report_identity); + end if; +end; +$$; + +-- Índice auxiliar específico para consultas/soporte de Bono 14. +create index if not exists ix_cruces_cuentas_gt_reportes_bono14_updated + on public.cruces_cuentas_gt_reportes (year, updated_at desc) + where upper(coalesce(country, 'GT')) = 'GT' + and lower(coalesce(period_type, '')) = 'bono14'; + +-- ------------------------------------------------------------ +-- 5. RPC que n8n consulta ANTES de crear/reutilizar el Google Sheet +-- ------------------------------------------------------------ +create or replace function public.cruce_cuentas_get_reporte_periodo( + p_country text, + p_year integer, + p_month integer, + p_period_type text +) +returns jsonb +language plpgsql +stable +security definer +set search_path = public +as $$ +declare + v_identity text; + v_report public.cruces_cuentas_gt_reportes%rowtype; +begin + v_identity := + public.cruce_cuentas_build_report_identity( + p_country, + p_year, + p_month, + p_period_type + ); + + select r.* + into v_report + from public.cruces_cuentas_gt_reportes r + where r.report_identity = v_identity + order by + r.updated_at desc nulls last, + r.created_at desc nulls last + limit 1; + + if not found then + return jsonb_build_object( + 'ok', true, + 'found', false, + 'report_identity', v_identity, + 'report', null + ); + end if; + + return jsonb_build_object( + 'ok', true, + 'found', true, + 'report_identity', v_identity, + 'report', jsonb_build_object( + 'id', v_report.id, + 'report_identity', v_report.report_identity, + 'country', v_report.country, + 'year', v_report.year, + 'month', v_report.month, + 'period_type', v_report.period_type, + 'period_label', v_report.period_label, + 'spreadsheet_id', v_report.spreadsheet_id, + 'report_url', v_report.report_url, + 'estado', v_report.estado, + 'created_at', v_report.created_at, + 'updated_at', v_report.updated_at + ) + ); +end; +$$; + +revoke all on function public.cruce_cuentas_get_reporte_periodo(text, integer, integer, text) + from public, anon, authenticated; + +grant execute on function public.cruce_cuentas_get_reporte_periodo(text, integer, integer, text) + to service_role; + +commit; + +-- ============================================================ +-- VALIDACIÓN FINAL (solo lectura) +-- Debe devolver: GT-2026-07-bono14 +-- ============================================================ +select + public.cruce_cuentas_build_report_identity( + 'GT', + 2026, + 7, + 'bono14' + ) as identidad_bono14_esperada; + +-- Si ya existe un Bono 14 2026, esta consulta devuelve found=true. +-- Si todavía no se ha generado, devuelve found=false (lo esperado antes +-- de la primera ejecución). +select public.cruce_cuentas_get_reporte_periodo( + 'GT', + 2026, + 7, + 'bono14' +) as estado_bono14_2026; diff --git a/SUPABASE_RPC_HISTORICOS.sql b/SUPABASE_RPC_HISTORICOS.sql new file mode 100644 index 0000000..1045f86 --- /dev/null +++ b/SUPABASE_RPC_HISTORICOS.sql @@ -0,0 +1,93 @@ +-- RPC paginada para Reportes Históricos del Cruce de Cuentas GLM. +-- Ejecutar una sola vez en el SQL Editor del Supabase empresarial. +-- La función filtra el país en la base de datos antes de paginar. + +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') 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) 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; +grant execute on function public.cruce_cuentas_get_historicos(text, integer, integer, text, text) to authenticated; diff --git a/VALIDACION_BONO14.md b/VALIDACION_BONO14.md new file mode 100644 index 0000000..79f28d7 --- /dev/null +++ b/VALIDACION_BONO14.md @@ -0,0 +1,35 @@ +# Validación técnica — Bono 14 Guatemala + +Validaciones offline realizadas sobre los archivos reales suministrados: + +- TypeScript: `npm run lint` requerido y validado en este paquete. +- Workflow n8n: 52 nodos y 46 conexiones raíz. +- Code nodes: 11 validados con `node --check`. +- Total Bono 14 QTZ leído: Q 1,332,673.42 +- Total Bono 14 USD leído: $ 6,873.75 +- Las siete hojas canónicas coinciden con su fila de total oficial. +- Los CSV 1263–1268 reproducen exactamente los totales QTZ de las hojas correspondientes. +- El CSV 912 US reproduce exactamente el total USD. +- El CSV 1269 se acepta como archivo bancario adicional y NO se excluye silenciosamente; cualquier monto no respaldado por el Excel aparecerá en el cruce. + +## Build de producción + +El código fuente TypeScript está validado. + +El `node_modules` del ZIP original contiene paquetes nativos de Windows para +Rollup. El sandbox Linux no puede generar de forma confiable un `dist` nuevo +con esos binarios, por lo que este ZIP **no incluye el dist anterior**. + +Antes de desplegar en el servidor, ejecutar en el entorno habitual del proyecto: + +```bash +npm run build +``` + +Si el entorno donde se compila es Linux y aparece un error de Rollup por +dependencia nativa, ejecutar primero: + +```bash +npm ci +npm run build +``` diff --git a/dist/assets/index-9ESZyUmu.css b/dist/assets/index-9ESZyUmu.css deleted file mode 100644 index 27c8e78..0000000 --- a/dist/assets/index-9ESZyUmu.css +++ /dev/null @@ -1 +0,0 @@ -@import"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap";/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:"Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-mono:"JetBrains Mono", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-800:oklch(44.4% .177 26.899);--color-orange-50:oklch(98% .016 73.684);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-800:oklch(47% .157 37.304);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-blue-50:oklch(97% .014 254.604);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-800:oklch(42.4% .199 265.638);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-800:oklch(43.2% .232 292.759);--color-slate-700:oklch(37.2% .044 257.287);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-3xl:48rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-md:.375rem;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-gris-fondo:#f5f5f5}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.top-0{top:0}.top-1\/2{top:50%}.top-\[84px\]{top:84px}.top-full{top:100%}.right-0{right:0}.right-6{right:calc(var(--spacing) * 6)}.bottom-0{bottom:0}.bottom-6{bottom:calc(var(--spacing) * 6)}.left-0{left:0}.left-3{left:calc(var(--spacing) * 3)}.left-3\.5{left:calc(var(--spacing) * 3.5)}.z-10{z-index:10}.z-40{z-index:40}.z-45{z-index:45}.z-50{z-index:50}.z-\[80\]{z-index:80}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-7{margin-bottom:calc(var(--spacing) * 7)}.-ml-2{margin-left:calc(var(--spacing) * -2)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1{height:var(--spacing)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-4\.5{height:calc(var(--spacing) * 4.5)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-14{height:calc(var(--spacing) * 14)}.h-\[80px\]{height:80px}.h-\[84px\]{height:84px}.h-screen{height:100vh}.max-h-\[140px\]{max-height:140px}.min-h-\[130px\]{min-height:130px}.min-h-\[150px\]{min-height:150px}.min-h-\[190px\]{min-height:190px}.min-h-screen{min-height:100vh}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-4\.5{width:calc(var(--spacing) * 4.5)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-12{width:calc(var(--spacing) * 12)}.w-64{width:calc(var(--spacing) * 64)}.w-auto{width:auto}.w-full{width:100%}.w-px{width:1px}.max-w-3xl{max-width:var(--container-3xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[180px\]{max-width:180px}.max-w-\[220px\]{max-width:220px}.max-w-\[260px\]{max-width:260px}.max-w-\[300px\]{max-width:300px}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:0}.min-w-8{min-width:calc(var(--spacing) * 8)}.min-w-\[280px\]{min-width:280px}.min-w-\[960px\]{min-width:960px}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.scroll-mt-28{scroll-margin-top:calc(var(--spacing) * 28)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-neutral-200>:not(:last-child)){border-color:var(--color-neutral-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-4{border-bottom-style:var(--tw-border-style);border-bottom-width:4px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-\[\#4F758B\]{border-color:#4f758b}.border-\[\#4F758B\]\/10{border-color:#4f758b1a}.border-\[\#4F758B\]\/20{border-color:#4f758b33}.border-\[\#4F758B\]\/30{border-color:#4f758b4d}.border-\[\#6CC24A\]{border-color:#6cc24a}.border-\[\#6CC24A\]\/30{border-color:#6cc24a4d}.border-\[\#D0D0D0\]{border-color:#d0d0d0}.border-amber-200{border-color:var(--color-amber-200)}.border-blue-200{border-color:var(--color-blue-200)}.border-emerald-200{border-color:var(--color-emerald-200)}.border-emerald-700{border-color:var(--color-emerald-700)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-200\/10{border-color:#e5e5e51a}@supports (color:color-mix(in lab,red,red)){.border-neutral-200\/10{border-color:color-mix(in oklab,var(--color-neutral-200) 10%,transparent)}}.border-neutral-300{border-color:var(--color-neutral-300)}.border-orange-200{border-color:var(--color-orange-200)}.border-red-200{border-color:var(--color-red-200)}.border-red-800{border-color:var(--color-red-800)}.border-slate-700{border-color:var(--color-slate-700)}.border-violet-200{border-color:var(--color-violet-200)}.bg-\[\#4F758B\]{background-color:#4f758b}.bg-\[\#4F758B\]\/5{background-color:#4f758b0d}.bg-\[\#4F758B\]\/10{background-color:#4f758b1a}.bg-\[\#6CC24A\]{background-color:#6cc24a}.bg-\[\#6CC24A\]\/5{background-color:#6cc24a0d}.bg-\[\#D0D0D0\]{background-color:#d0d0d0}.bg-\[\#F5F5F5\]{background-color:#f5f5f5}.bg-\[\#ba1a1a\]{background-color:#ba1a1a}.bg-amber-50{background-color:var(--color-amber-50)}.bg-black\/45{background-color:#00000073}@supports (color:color-mix(in lab,red,red)){.bg-black\/45{background-color:color-mix(in oklab,var(--color-black) 45%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-red-50{background-color:var(--color-red-50)}.bg-white{background-color:var(--color-white)}.fill-current{fill:currentColor}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pr-1{padding-right:var(--spacing)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pl-0{padding-left:0}.pl-9{padding-left:calc(var(--spacing) * 9)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-\[\#4F758B\]{color:#4f758b}.text-\[\#6CC24A\]{color:#6cc24a}.text-\[\#ba1a1a\]{color:#ba1a1a}.text-amber-800{color:var(--color-amber-800)}.text-blue-800{color:var(--color-blue-800)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-orange-800{color:var(--color-orange-800)}.text-violet-800{color:var(--color-violet-800)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.italic{font-style:italic}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.placeholder-neutral-400::placeholder{color:var(--color-neutral-400)}.opacity-25{opacity:.25}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.select-none{-webkit-user-select:none;user-select:none}.selection\:bg-\[\#6CC24A\] ::selection{background-color:#6cc24a}.selection\:bg-\[\#6CC24A\]::selection{background-color:#6cc24a}.selection\:bg-transparent ::selection{background-color:#0000}.selection\:bg-transparent::selection{background-color:#0000}.selection\:text-white ::selection{color:var(--color-white)}.selection\:text-white::selection{color:var(--color-white)}@media(hover:hover){.hover\:border-\[\#4F758B\]:hover{border-color:#4f758b}.hover\:border-\[\#4F758B\]\/40:hover{border-color:#4f758b66}.hover\:border-\[\#4F758B\]\/50:hover{border-color:#4f758b80}.hover\:border-emerald-400:hover{border-color:var(--color-emerald-400)}.hover\:bg-\[\#4F758B\]:hover{background-color:#4f758b}.hover\:bg-\[\#4F758B\]\/5:hover{background-color:#4f758b0d}.hover\:bg-\[\#5bb03c\]:hover{background-color:#5bb03c}.hover\:bg-\[\#41677b\]:hover{background-color:#41677b}.hover\:bg-emerald-100:hover{background-color:var(--color-emerald-100)}.hover\:bg-neutral-50:hover{background-color:var(--color-neutral-50)}.hover\:bg-neutral-50\/70:hover{background-color:#fafafab3}@supports (color:color-mix(in lab,red,red)){.hover\:bg-neutral-50\/70:hover{background-color:color-mix(in oklab,var(--color-neutral-50) 70%,transparent)}}.hover\:bg-neutral-100:hover{background-color:var(--color-neutral-100)}.hover\:bg-neutral-200:hover{background-color:var(--color-neutral-200)}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:bg-white\/20:hover{background-color:#fff3}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/20:hover{background-color:color-mix(in oklab,var(--color-white) 20%,transparent)}}.hover\:text-\[\#3d6378\]:hover{color:#3d6378}.hover\:text-\[\#4F758B\]:hover{color:#4f758b}.hover\:text-emerald-800:hover{color:var(--color-emerald-800)}.hover\:text-neutral-700:hover{color:var(--color-neutral-700)}.hover\:text-neutral-800:hover{color:var(--color-neutral-800)}.hover\:text-red-500:hover{color:var(--color-red-500)}.hover\:text-white:hover{color:var(--color-white)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:border-\[\#4F758B\]:focus{border-color:#4f758b}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:scale-98:active{--tw-scale-x:98%;--tw-scale-y:98%;--tw-scale-z:98%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-\[0\.99\]:active{scale:.99}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-neutral-300:disabled{background-color:var(--color-neutral-300)}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:40rem){.sm\:flex{display:flex}.sm\:inline-flex{display:inline-flex}.sm\:h-6{height:calc(var(--spacing) * 6)}.sm\:h-10{height:calc(var(--spacing) * 10)}.sm\:w-16{width:calc(var(--spacing) * 16)}.sm\:w-auto{width:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-2{gap:calc(var(--spacing) * 2)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:p-9{padding:calc(var(--spacing) * 9)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:px-8{padding-inline:calc(var(--spacing) * 8)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media(min-width:48rem){.md\:block{display:block}.md\:hidden{display:none}.md\:translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.md\:justify-center{justify-content:center}.md\:pl-64{padding-left:calc(var(--spacing) * 64)}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}}@media(min-width:64rem){.lg\:w-auto{width:auto}.lg\:max-w-md{max-width:var(--container-md)}.lg\:max-w-sm{max-width:var(--container-sm)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-between{justify-content:space-between}.lg\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}@media(min-width:80rem){.xl\:min-w-full{min-width:100%}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:px-4{padding-inline:calc(var(--spacing) * 4)}}}body{font-family:var(--font-sans);background-color:var(--color-gris-fondo)}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}} diff --git a/dist/assets/index-Cj1bVpIM.js b/dist/assets/index-Cj1bVpIM.js deleted file mode 100644 index 06885ae..0000000 --- a/dist/assets/index-Cj1bVpIM.js +++ /dev/null @@ -1,253 +0,0 @@ -(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))a(l);new MutationObserver(l=>{for(const u of l)if(u.type==="childList")for(const c of u.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&a(c)}).observe(document,{childList:!0,subtree:!0});function s(l){const u={};return l.integrity&&(u.integrity=l.integrity),l.referrerPolicy&&(u.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?u.credentials="include":l.crossOrigin==="anonymous"?u.credentials="omit":u.credentials="same-origin",u}function a(l){if(l.ep)return;l.ep=!0;const u=s(l);fetch(l.href,u)}})();var Eh={exports:{}},fr={};/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Ay;function US(){if(Ay)return fr;Ay=1;var n=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function s(a,l,u){var c=null;if(u!==void 0&&(c=""+u),l.key!==void 0&&(c=""+l.key),"key"in l){u={};for(var d in l)d!=="key"&&(u[d]=l[d])}else u=l;return l=u.ref,{$$typeof:n,type:a,key:c,ref:l!==void 0?l:null,props:u}}return fr.Fragment=e,fr.jsx=s,fr.jsxs=s,fr}var Ry;function BS(){return Ry||(Ry=1,Eh.exports=US()),Eh.exports}var b=BS(),Ah={exports:{}},pe={};/** - * @license React - * react.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Cy;function LS(){if(Cy)return pe;Cy=1;var n=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),l=Symbol.for("react.profiler"),u=Symbol.for("react.consumer"),c=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),m=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),v=Symbol.for("react.activity"),w=Symbol.iterator;function x(E){return E===null||typeof E!="object"?null:(E=w&&E[w]||E["@@iterator"],typeof E=="function"?E:null)}var S={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},A=Object.assign,k={};function C(E,B,K){this.props=E,this.context=B,this.refs=k,this.updater=K||S}C.prototype.isReactComponent={},C.prototype.setState=function(E,B){if(typeof E!="object"&&typeof E!="function"&&E!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,E,B,"setState")},C.prototype.forceUpdate=function(E){this.updater.enqueueForceUpdate(this,E,"forceUpdate")};function N(){}N.prototype=C.prototype;function O(E,B,K){this.props=E,this.context=B,this.refs=k,this.updater=K||S}var P=O.prototype=new N;P.constructor=O,A(P,C.prototype),P.isPureReactComponent=!0;var J=Array.isArray;function X(){}var G={H:null,A:null,T:null,S:null},Z=Object.prototype.hasOwnProperty;function ee(E,B,K){var W=K.ref;return{$$typeof:n,type:E,key:B,ref:W!==void 0?W:null,props:K}}function ne(E,B){return ee(E.type,B,E.props)}function le(E){return typeof E=="object"&&E!==null&&E.$$typeof===n}function de(E){var B={"=":"=0",":":"=2"};return"$"+E.replace(/[=:]/g,function(K){return B[K]})}var je=/\/+/g;function Re(E,B){return typeof E=="object"&&E!==null&&E.key!=null?de(""+E.key):B.toString(36)}function ke(E){switch(E.status){case"fulfilled":return E.value;case"rejected":throw E.reason;default:switch(typeof E.status=="string"?E.then(X,X):(E.status="pending",E.then(function(B){E.status==="pending"&&(E.status="fulfilled",E.value=B)},function(B){E.status==="pending"&&(E.status="rejected",E.reason=B)})),E.status){case"fulfilled":return E.value;case"rejected":throw E.reason}}throw E}function L(E,B,K,W,he){var ue=typeof E;(ue==="undefined"||ue==="boolean")&&(E=null);var _e=!1;if(E===null)_e=!0;else switch(ue){case"bigint":case"string":case"number":_e=!0;break;case"object":switch(E.$$typeof){case n:case e:_e=!0;break;case g:return _e=E._init,L(_e(E._payload),B,K,W,he)}}if(_e)return he=he(E),_e=W===""?"."+Re(E,0):W,J(he)?(K="",_e!=null&&(K=_e.replace(je,"$&/")+"/"),L(he,B,K,"",function(_n){return _n})):he!=null&&(le(he)&&(he=ne(he,K+(he.key==null||E&&E.key===he.key?"":(""+he.key).replace(je,"$&/")+"/")+_e)),B.push(he)),1;_e=0;var qe=W===""?".":W+":";if(J(E))for(var He=0;He>>1,oe=L[I];if(0>>1;Il(K,Y))Wl(he,K)?(L[I]=he,L[W]=Y,I=W):(L[I]=K,L[B]=Y,I=B);else if(Wl(he,Y))L[I]=he,L[W]=Y,I=W;else break e}}return Q}function l(L,Q){var Y=L.sortIndex-Q.sortIndex;return Y!==0?Y:L.id-Q.id}if(n.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var u=performance;n.unstable_now=function(){return u.now()}}else{var c=Date,d=c.now();n.unstable_now=function(){return c.now()-d}}var m=[],p=[],g=1,v=null,w=3,x=!1,S=!1,A=!1,k=!1,C=typeof setTimeout=="function"?setTimeout:null,N=typeof clearTimeout=="function"?clearTimeout:null,O=typeof setImmediate<"u"?setImmediate:null;function P(L){for(var Q=s(p);Q!==null;){if(Q.callback===null)a(p);else if(Q.startTime<=L)a(p),Q.sortIndex=Q.expirationTime,e(m,Q);else break;Q=s(p)}}function J(L){if(A=!1,P(L),!S)if(s(m)!==null)S=!0,X||(X=!0,de());else{var Q=s(p);Q!==null&&ke(J,Q.startTime-L)}}var X=!1,G=-1,Z=5,ee=-1;function ne(){return k?!0:!(n.unstable_now()-eeL&&ne());){var I=v.callback;if(typeof I=="function"){v.callback=null,w=v.priorityLevel;var oe=I(v.expirationTime<=L);if(L=n.unstable_now(),typeof oe=="function"){v.callback=oe,P(L),Q=!0;break t}v===s(m)&&a(m),P(L)}else a(m);v=s(m)}if(v!==null)Q=!0;else{var E=s(p);E!==null&&ke(J,E.startTime-L),Q=!1}}break e}finally{v=null,w=Y,x=!1}Q=void 0}}finally{Q?de():X=!1}}}var de;if(typeof O=="function")de=function(){O(le)};else if(typeof MessageChannel<"u"){var je=new MessageChannel,Re=je.port2;je.port1.onmessage=le,de=function(){Re.postMessage(null)}}else de=function(){C(le,0)};function ke(L,Q){G=C(function(){L(n.unstable_now())},Q)}n.unstable_IdlePriority=5,n.unstable_ImmediatePriority=1,n.unstable_LowPriority=4,n.unstable_NormalPriority=3,n.unstable_Profiling=null,n.unstable_UserBlockingPriority=2,n.unstable_cancelCallback=function(L){L.callback=null},n.unstable_forceFrameRate=function(L){0>L||125I?(L.sortIndex=Y,e(p,L),s(m)===null&&L===s(p)&&(A?(N(G),G=-1):A=!0,ke(J,Y-I))):(L.sortIndex=oe,e(m,L),S||x||(S=!0,X||(X=!0,de()))),L},n.unstable_shouldYield=ne,n.unstable_wrapCallback=function(L){var Q=w;return function(){var Y=w;w=Q;try{return L.apply(this,arguments)}finally{w=Y}}}})(kh)),kh}var Oy;function VS(){return Oy||(Oy=1,Ch.exports=zS()),Ch.exports}var jh={exports:{}},_t={};/** - * @license React - * react-dom.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Ny;function PS(){if(Ny)return _t;Ny=1;var n=qd();function e(m){var p="https://react.dev/errors/"+m;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}return n(),jh.exports=PS(),jh.exports}/** - * @license React - * react-dom-client.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var My;function $S(){if(My)return mr;My=1;var n=VS(),e=qd(),s=HS();function a(t){var i="https://react.dev/errors/"+t;if(1oe||(t.current=I[oe],I[oe]=null,oe--)}function K(t,i){oe++,I[oe]=t.current,t.current=i}var W=E(null),he=E(null),ue=E(null),_e=E(null);function qe(t,i){switch(K(ue,i),K(he,t),K(W,null),i.nodeType){case 9:case 11:t=(t=i.documentElement)&&(t=t.namespaceURI)?Xg(t):0;break;default:if(t=i.tagName,i=i.namespaceURI)i=Xg(i),t=Jg(i,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}B(W),K(W,t)}function He(){B(W),B(he),B(ue)}function _n(t){t.memoizedState!==null&&K(_e,t);var i=W.current,r=Jg(i,t.type);i!==r&&(K(he,t),K(W,r))}function wt(t){he.current===t&&(B(W),B(he)),_e.current===t&&(B(_e),ur._currentValue=Y)}var Yt,oi;function dt(t){if(Yt===void 0)try{throw Error()}catch(r){var i=r.stack.trim().match(/\n( *(at )?)/);Yt=i&&i[1]||"",oi=-1)":-1h||T[o]!==M[h]){var H=` -`+T[o].replace(" at new "," at ");return t.displayName&&H.includes("")&&(H=H.replace("",t.displayName)),H}while(1<=o&&0<=h);break}}}finally{Pi=!1,Error.prepareStackTrace=r}return(r=t?t.displayName||t.name:"")?dt(r):""}function ge(t,i){switch(t.tag){case 26:case 27:case 5:return dt(t.type);case 16:return dt("Lazy");case 13:return t.child!==i&&i!==null?dt("Suspense Fallback"):dt("Suspense");case 19:return dt("SuspenseList");case 0:case 15:return F(t.type,!1);case 11:return F(t.type.render,!1);case 1:return F(t.type,!0);case 31:return dt("Activity");default:return""}}function Fe(t){try{var i="",r=null;do i+=ge(t,r),r=t,t=t.return;while(t);return i}catch(o){return` -Error generating stack: `+o.message+` -`+o.stack}}var Bt=Object.prototype.hasOwnProperty,Ln=n.unstable_scheduleCallback,Yr=n.unstable_cancelCallback,xn=n.unstable_shouldYield,Xr=n.unstable_requestPaint,Me=n.unstable_now,Sn=n.unstable_getCurrentPriorityLevel,Rf=n.unstable_ImmediatePriority,Cf=n.unstable_UserBlockingPriority,Jr=n.unstable_NormalPriority,g_=n.unstable_LowPriority,kf=n.unstable_IdlePriority,y_=n.log,v_=n.unstable_setDisableYieldValue,_a=null,Lt=null;function li(t){if(typeof y_=="function"&&v_(t),Lt&&typeof Lt.setStrictMode=="function")try{Lt.setStrictMode(_a,t)}catch{}}var zt=Math.clz32?Math.clz32:__,b_=Math.log,w_=Math.LN2;function __(t){return t>>>=0,t===0?32:31-(b_(t)/w_|0)|0}var Qr=256,Zr=262144,Wr=4194304;function Hi(t){var i=t&42;if(i!==0)return i;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function eo(t,i,r){var o=t.pendingLanes;if(o===0)return 0;var h=0,f=t.suspendedLanes,y=t.pingedLanes;t=t.warmLanes;var _=o&134217727;return _!==0?(o=_&~f,o!==0?h=Hi(o):(y&=_,y!==0?h=Hi(y):r||(r=_&~t,r!==0&&(h=Hi(r))))):(_=o&~f,_!==0?h=Hi(_):y!==0?h=Hi(y):r||(r=o&~t,r!==0&&(h=Hi(r)))),h===0?0:i!==0&&i!==h&&(i&f)===0&&(f=h&-h,r=i&-i,f>=r||f===32&&(r&4194048)!==0)?i:h}function xa(t,i){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&i)===0}function x_(t,i){switch(t){case 1:case 2:case 4:case 8:case 64:return i+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return i+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function jf(){var t=Wr;return Wr<<=1,(Wr&62914560)===0&&(Wr=4194304),t}function fu(t){for(var i=[],r=0;31>r;r++)i.push(t);return i}function Sa(t,i){t.pendingLanes|=i,i!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function S_(t,i,r,o,h,f){var y=t.pendingLanes;t.pendingLanes=r,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=r,t.entangledLanes&=r,t.errorRecoveryDisabledLanes&=r,t.shellSuspendCounter=0;var _=t.entanglements,T=t.expirationTimes,M=t.hiddenUpdates;for(r=y&~r;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var k_=/[\n"\\]/g;function Jt(t){return t.replace(k_,function(i){return"\\"+i.charCodeAt(0).toString(16)+" "})}function bu(t,i,r,o,h,f,y,_){t.name="",y!=null&&typeof y!="function"&&typeof y!="symbol"&&typeof y!="boolean"?t.type=y:t.removeAttribute("type"),i!=null?y==="number"?(i===0&&t.value===""||t.value!=i)&&(t.value=""+Xt(i)):t.value!==""+Xt(i)&&(t.value=""+Xt(i)):y!=="submit"&&y!=="reset"||t.removeAttribute("value"),i!=null?wu(t,y,Xt(i)):r!=null?wu(t,y,Xt(r)):o!=null&&t.removeAttribute("value"),h==null&&f!=null&&(t.defaultChecked=!!f),h!=null&&(t.checked=h&&typeof h!="function"&&typeof h!="symbol"),_!=null&&typeof _!="function"&&typeof _!="symbol"&&typeof _!="boolean"?t.name=""+Xt(_):t.removeAttribute("name")}function qf(t,i,r,o,h,f,y,_){if(f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"&&(t.type=f),i!=null||r!=null){if(!(f!=="submit"&&f!=="reset"||i!=null)){vu(t);return}r=r!=null?""+Xt(r):"",i=i!=null?""+Xt(i):r,_||i===t.value||(t.value=i),t.defaultValue=i}o=o??h,o=typeof o!="function"&&typeof o!="symbol"&&!!o,t.checked=_?t.checked:!!o,t.defaultChecked=!!o,y!=null&&typeof y!="function"&&typeof y!="symbol"&&typeof y!="boolean"&&(t.name=y),vu(t)}function wu(t,i,r){i==="number"&&io(t.ownerDocument)===t||t.defaultValue===""+r||(t.defaultValue=""+r)}function _s(t,i,r,o){if(t=t.options,i){i={};for(var h=0;h"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Eu=!1;if(Pn)try{var Ra={};Object.defineProperty(Ra,"passive",{get:function(){Eu=!0}}),window.addEventListener("test",Ra,Ra),window.removeEventListener("test",Ra,Ra)}catch{Eu=!1}var ci=null,Au=null,ao=null;function Jf(){if(ao)return ao;var t,i=Au,r=i.length,o,h="value"in ci?ci.value:ci.textContent,f=h.length;for(t=0;t=ja),nm=" ",im=!1;function sm(t,i){switch(t){case"keyup":return ix.indexOf(i.keyCode)!==-1;case"keydown":return i.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function am(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Es=!1;function ax(t,i){switch(t){case"compositionend":return am(i);case"keypress":return i.which!==32?null:(im=!0,nm);case"textInput":return t=i.data,t===nm&&im?null:t;default:return null}}function rx(t,i){if(Es)return t==="compositionend"||!Ou&&sm(t,i)?(t=Jf(),ao=Au=ci=null,Es=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(i.ctrlKey||i.altKey||i.metaKey)||i.ctrlKey&&i.altKey){if(i.char&&1=i)return{node:r,offset:i-t};t=o}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=fm(r)}}function pm(t,i){return t&&i?t===i?!0:t&&t.nodeType===3?!1:i&&i.nodeType===3?pm(t,i.parentNode):"contains"in t?t.contains(i):t.compareDocumentPosition?!!(t.compareDocumentPosition(i)&16):!1:!1}function gm(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var i=io(t.document);i instanceof t.HTMLIFrameElement;){try{var r=typeof i.contentWindow.location.href=="string"}catch{r=!1}if(r)t=i.contentWindow;else break;i=io(t.document)}return i}function Mu(t){var i=t&&t.nodeName&&t.nodeName.toLowerCase();return i&&(i==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||i==="textarea"||t.contentEditable==="true")}var mx=Pn&&"documentMode"in document&&11>=document.documentMode,As=null,Uu=null,Ma=null,Bu=!1;function ym(t,i,r){var o=r.window===r?r.document:r.nodeType===9?r:r.ownerDocument;Bu||As==null||As!==io(o)||(o=As,"selectionStart"in o&&Mu(o)?o={start:o.selectionStart,end:o.selectionEnd}:(o=(o.ownerDocument&&o.ownerDocument.defaultView||window).getSelection(),o={anchorNode:o.anchorNode,anchorOffset:o.anchorOffset,focusNode:o.focusNode,focusOffset:o.focusOffset}),Ma&&Da(Ma,o)||(Ma=o,o=Wo(Uu,"onSelect"),0>=y,h-=y,Tn=1<<32-zt(i)+h|r<ve?(Se=re,re=null):Se=re.sibling;var Ae=U(j,re,D[ve],$);if(Ae===null){re===null&&(re=Se);break}t&&re&&Ae.alternate===null&&i(j,re),R=f(Ae,R,ve),Ee===null?ce=Ae:Ee.sibling=Ae,Ee=Ae,re=Se}if(ve===D.length)return r(j,re),Te&&$n(j,ve),ce;if(re===null){for(;veve?(Se=re,re=null):Se=re.sibling;var Ni=U(j,re,Ae.value,$);if(Ni===null){re===null&&(re=Se);break}t&&re&&Ni.alternate===null&&i(j,re),R=f(Ni,R,ve),Ee===null?ce=Ni:Ee.sibling=Ni,Ee=Ni,re=Se}if(Ae.done)return r(j,re),Te&&$n(j,ve),ce;if(re===null){for(;!Ae.done;ve++,Ae=D.next())Ae=q(j,Ae.value,$),Ae!==null&&(R=f(Ae,R,ve),Ee===null?ce=Ae:Ee.sibling=Ae,Ee=Ae);return Te&&$n(j,ve),ce}for(re=o(re);!Ae.done;ve++,Ae=D.next())Ae=z(re,j,ve,Ae.value,$),Ae!==null&&(t&&Ae.alternate!==null&&re.delete(Ae.key===null?ve:Ae.key),R=f(Ae,R,ve),Ee===null?ce=Ae:Ee.sibling=Ae,Ee=Ae);return t&&re.forEach(function(MS){return i(j,MS)}),Te&&$n(j,ve),ce}function Le(j,R,D,$){if(typeof D=="object"&&D!==null&&D.type===A&&D.key===null&&(D=D.props.children),typeof D=="object"&&D!==null){switch(D.$$typeof){case x:e:{for(var ce=D.key;R!==null;){if(R.key===ce){if(ce=D.type,ce===A){if(R.tag===7){r(j,R.sibling),$=h(R,D.props.children),$.return=j,j=$;break e}}else if(R.elementType===ce||typeof ce=="object"&&ce!==null&&ce.$$typeof===Z&&Zi(ce)===R.type){r(j,R.sibling),$=h(R,D.props),Pa($,D),$.return=j,j=$;break e}r(j,R);break}else i(j,R);R=R.sibling}D.type===A?($=Fi(D.props.children,j.mode,$,D.key),$.return=j,j=$):($=go(D.type,D.key,D.props,null,j.mode,$),Pa($,D),$.return=j,j=$)}return y(j);case S:e:{for(ce=D.key;R!==null;){if(R.key===ce)if(R.tag===4&&R.stateNode.containerInfo===D.containerInfo&&R.stateNode.implementation===D.implementation){r(j,R.sibling),$=h(R,D.children||[]),$.return=j,j=$;break e}else{r(j,R);break}else i(j,R);R=R.sibling}$=qu(D,j.mode,$),$.return=j,j=$}return y(j);case Z:return D=Zi(D),Le(j,R,D,$)}if(ke(D))return ie(j,R,D,$);if(de(D)){if(ce=de(D),typeof ce!="function")throw Error(a(150));return D=ce.call(D),me(j,R,D,$)}if(typeof D.then=="function")return Le(j,R,So(D),$);if(D.$$typeof===O)return Le(j,R,bo(j,D),$);To(j,D)}return typeof D=="string"&&D!==""||typeof D=="number"||typeof D=="bigint"?(D=""+D,R!==null&&R.tag===6?(r(j,R.sibling),$=h(R,D),$.return=j,j=$):(r(j,R),$=$u(D,j.mode,$),$.return=j,j=$),y(j)):r(j,R)}return function(j,R,D,$){try{Va=0;var ce=Le(j,R,D,$);return Ls=null,ce}catch(re){if(re===Bs||re===_o)throw re;var Ee=Pt(29,re,null,j.mode);return Ee.lanes=$,Ee.return=j,Ee}finally{}}}var es=Pm(!0),Hm=Pm(!1),pi=!1;function tc(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function nc(t,i){t=t.updateQueue,i.updateQueue===t&&(i.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function gi(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function yi(t,i,r){var o=t.updateQueue;if(o===null)return null;if(o=o.shared,(Ce&2)!==0){var h=o.pending;return h===null?i.next=i:(i.next=h.next,h.next=i),o.pending=i,i=po(t),Tm(t,null,r),i}return mo(t,o,i,r),po(t)}function Ha(t,i,r){if(i=i.updateQueue,i!==null&&(i=i.shared,(r&4194048)!==0)){var o=i.lanes;o&=t.pendingLanes,r|=o,i.lanes=r,Nf(t,r)}}function ic(t,i){var r=t.updateQueue,o=t.alternate;if(o!==null&&(o=o.updateQueue,r===o)){var h=null,f=null;if(r=r.firstBaseUpdate,r!==null){do{var y={lane:r.lane,tag:r.tag,payload:r.payload,callback:null,next:null};f===null?h=f=y:f=f.next=y,r=r.next}while(r!==null);f===null?h=f=i:f=f.next=i}else h=f=i;r={baseState:o.baseState,firstBaseUpdate:h,lastBaseUpdate:f,shared:o.shared,callbacks:o.callbacks},t.updateQueue=r;return}t=r.lastBaseUpdate,t===null?r.firstBaseUpdate=i:t.next=i,r.lastBaseUpdate=i}var sc=!1;function $a(){if(sc){var t=Us;if(t!==null)throw t}}function qa(t,i,r,o){sc=!1;var h=t.updateQueue;pi=!1;var f=h.firstBaseUpdate,y=h.lastBaseUpdate,_=h.shared.pending;if(_!==null){h.shared.pending=null;var T=_,M=T.next;T.next=null,y===null?f=M:y.next=M,y=T;var H=t.alternate;H!==null&&(H=H.updateQueue,_=H.lastBaseUpdate,_!==y&&(_===null?H.firstBaseUpdate=M:_.next=M,H.lastBaseUpdate=T))}if(f!==null){var q=h.baseState;y=0,H=M=T=null,_=f;do{var U=_.lane&-536870913,z=U!==_.lane;if(z?(xe&U)===U:(o&U)===U){U!==0&&U===Ms&&(sc=!0),H!==null&&(H=H.next={lane:0,tag:_.tag,payload:_.payload,callback:null,next:null});e:{var ie=t,me=_;U=i;var Le=r;switch(me.tag){case 1:if(ie=me.payload,typeof ie=="function"){q=ie.call(Le,q,U);break e}q=ie;break e;case 3:ie.flags=ie.flags&-65537|128;case 0:if(ie=me.payload,U=typeof ie=="function"?ie.call(Le,q,U):ie,U==null)break e;q=v({},q,U);break e;case 2:pi=!0}}U=_.callback,U!==null&&(t.flags|=64,z&&(t.flags|=8192),z=h.callbacks,z===null?h.callbacks=[U]:z.push(U))}else z={lane:U,tag:_.tag,payload:_.payload,callback:_.callback,next:null},H===null?(M=H=z,T=q):H=H.next=z,y|=U;if(_=_.next,_===null){if(_=h.shared.pending,_===null)break;z=_,_=z.next,z.next=null,h.lastBaseUpdate=z,h.shared.pending=null}}while(!0);H===null&&(T=q),h.baseState=T,h.firstBaseUpdate=M,h.lastBaseUpdate=H,f===null&&(h.shared.lanes=0),xi|=y,t.lanes=y,t.memoizedState=q}}function $m(t,i){if(typeof t!="function")throw Error(a(191,t));t.call(i)}function qm(t,i){var r=t.callbacks;if(r!==null)for(t.callbacks=null,t=0;tf?f:8;var y=L.T,_={};L.T=_,Sc(t,!1,i,r);try{var T=h(),M=L.S;if(M!==null&&M(_,T),T!==null&&typeof T=="object"&&typeof T.then=="function"){var H=Sx(T,o);Ka(t,i,H,Gt(t))}else Ka(t,i,o,Gt(t))}catch(q){Ka(t,i,{then:function(){},status:"rejected",reason:q},Gt())}finally{Q.p=f,y!==null&&_.types!==null&&(y.types=_.types),L.T=y}}function kx(){}function _c(t,i,r,o){if(t.tag!==5)throw Error(a(476));var h=_p(t).queue;wp(t,h,i,Y,r===null?kx:function(){return xp(t),r(o)})}function _p(t){var i=t.memoizedState;if(i!==null)return i;i={memoizedState:Y,baseState:Y,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Kn,lastRenderedState:Y},next:null};var r={};return i.next={memoizedState:r,baseState:r,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Kn,lastRenderedState:r},next:null},t.memoizedState=i,t=t.alternate,t!==null&&(t.memoizedState=i),i}function xp(t){var i=_p(t);i.next===null&&(i=t.alternate.memoizedState),Ka(t,i.next.queue,{},Gt())}function xc(){return pt(ur)}function Sp(){return We().memoizedState}function Tp(){return We().memoizedState}function jx(t){for(var i=t.return;i!==null;){switch(i.tag){case 24:case 3:var r=Gt();t=gi(r);var o=yi(i,t,r);o!==null&&(Dt(o,i,r),Ha(o,i,r)),i={cache:Qu()},t.payload=i;return}i=i.return}}function Ox(t,i,r){var o=Gt();r={lane:o,revertLane:0,gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Mo(t)?Ap(i,r):(r=Pu(t,i,r,o),r!==null&&(Dt(r,t,o),Rp(r,i,o)))}function Ep(t,i,r){var o=Gt();Ka(t,i,r,o)}function Ka(t,i,r,o){var h={lane:o,revertLane:0,gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null};if(Mo(t))Ap(i,h);else{var f=t.alternate;if(t.lanes===0&&(f===null||f.lanes===0)&&(f=i.lastRenderedReducer,f!==null))try{var y=i.lastRenderedState,_=f(y,r);if(h.hasEagerState=!0,h.eagerState=_,Vt(_,y))return mo(t,i,h,0),ze===null&&fo(),!1}catch{}finally{}if(r=Pu(t,i,h,o),r!==null)return Dt(r,t,o),Rp(r,i,o),!0}return!1}function Sc(t,i,r,o){if(o={lane:2,revertLane:th(),gesture:null,action:o,hasEagerState:!1,eagerState:null,next:null},Mo(t)){if(i)throw Error(a(479))}else i=Pu(t,r,o,2),i!==null&&Dt(i,t,2)}function Mo(t){var i=t.alternate;return t===ye||i!==null&&i===ye}function Ap(t,i){Vs=Ro=!0;var r=t.pending;r===null?i.next=i:(i.next=r.next,r.next=i),t.pending=i}function Rp(t,i,r){if((r&4194048)!==0){var o=i.lanes;o&=t.pendingLanes,r|=o,i.lanes=r,Nf(t,r)}}var Fa={readContext:pt,use:jo,useCallback:Ye,useContext:Ye,useEffect:Ye,useImperativeHandle:Ye,useLayoutEffect:Ye,useInsertionEffect:Ye,useMemo:Ye,useReducer:Ye,useRef:Ye,useState:Ye,useDebugValue:Ye,useDeferredValue:Ye,useTransition:Ye,useSyncExternalStore:Ye,useId:Ye,useHostTransitionStatus:Ye,useFormState:Ye,useActionState:Ye,useOptimistic:Ye,useMemoCache:Ye,useCacheRefresh:Ye};Fa.useEffectEvent=Ye;var Cp={readContext:pt,use:jo,useCallback:function(t,i){return St().memoizedState=[t,i===void 0?null:i],t},useContext:pt,useEffect:hp,useImperativeHandle:function(t,i,r){r=r!=null?r.concat([t]):null,No(4194308,4,pp.bind(null,i,t),r)},useLayoutEffect:function(t,i){return No(4194308,4,t,i)},useInsertionEffect:function(t,i){No(4,2,t,i)},useMemo:function(t,i){var r=St();i=i===void 0?null:i;var o=t();if(ts){li(!0);try{t()}finally{li(!1)}}return r.memoizedState=[o,i],o},useReducer:function(t,i,r){var o=St();if(r!==void 0){var h=r(i);if(ts){li(!0);try{r(i)}finally{li(!1)}}}else h=i;return o.memoizedState=o.baseState=h,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:h},o.queue=t,t=t.dispatch=Ox.bind(null,ye,t),[o.memoizedState,t]},useRef:function(t){var i=St();return t={current:t},i.memoizedState=t},useState:function(t){t=gc(t);var i=t.queue,r=Ep.bind(null,ye,i);return i.dispatch=r,[t.memoizedState,r]},useDebugValue:bc,useDeferredValue:function(t,i){var r=St();return wc(r,t,i)},useTransition:function(){var t=gc(!1);return t=wp.bind(null,ye,t.queue,!0,!1),St().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,i,r){var o=ye,h=St();if(Te){if(r===void 0)throw Error(a(407));r=r()}else{if(r=i(),ze===null)throw Error(a(349));(xe&127)!==0||Xm(o,i,r)}h.memoizedState=r;var f={value:r,getSnapshot:i};return h.queue=f,hp(Qm.bind(null,o,f,t),[t]),o.flags|=2048,Hs(9,{destroy:void 0},Jm.bind(null,o,f,r,i),null),r},useId:function(){var t=St(),i=ze.identifierPrefix;if(Te){var r=En,o=Tn;r=(o&~(1<<32-zt(o)-1)).toString(32)+r,i="_"+i+"R_"+r,r=Co++,0<\/script>",f=f.removeChild(f.firstChild);break;case"select":f=typeof o.is=="string"?y.createElement("select",{is:o.is}):y.createElement("select"),o.multiple?f.multiple=!0:o.size&&(f.size=o.size);break;default:f=typeof o.is=="string"?y.createElement(h,{is:o.is}):y.createElement(h)}}f[ft]=i,f[Rt]=o;e:for(y=i.child;y!==null;){if(y.tag===5||y.tag===6)f.appendChild(y.stateNode);else if(y.tag!==4&&y.tag!==27&&y.child!==null){y.child.return=y,y=y.child;continue}if(y===i)break e;for(;y.sibling===null;){if(y.return===null||y.return===i)break e;y=y.return}y.sibling.return=y.return,y=y.sibling}i.stateNode=f;e:switch(yt(f,h,o),h){case"button":case"input":case"select":case"textarea":o=!!o.autoFocus;break e;case"img":o=!0;break e;default:o=!1}o&&Yn(i)}}return Ge(i),Lc(i,i.type,t===null?null:t.memoizedProps,i.pendingProps,r),null;case 6:if(t&&i.stateNode!=null)t.memoizedProps!==o&&Yn(i);else{if(typeof o!="string"&&i.stateNode===null)throw Error(a(166));if(t=ue.current,Ns(i)){if(t=i.stateNode,r=i.memoizedProps,o=null,h=mt,h!==null)switch(h.tag){case 27:case 5:o=h.memoizedProps}t[ft]=i,t=!!(t.nodeValue===r||o!==null&&o.suppressHydrationWarning===!0||Fg(t.nodeValue,r)),t||fi(i,!0)}else t=el(t).createTextNode(o),t[ft]=i,i.stateNode=t}return Ge(i),null;case 31:if(r=i.memoizedState,t===null||t.memoizedState!==null){if(o=Ns(i),r!==null){if(t===null){if(!o)throw Error(a(318));if(t=i.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(a(557));t[ft]=i}else Yi(),(i.flags&128)===0&&(i.memoizedState=null),i.flags|=4;Ge(i),t=!1}else r=Fu(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=r),t=!0;if(!t)return i.flags&256?($t(i),i):($t(i),null);if((i.flags&128)!==0)throw Error(a(558))}return Ge(i),null;case 13:if(o=i.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(h=Ns(i),o!==null&&o.dehydrated!==null){if(t===null){if(!h)throw Error(a(318));if(h=i.memoizedState,h=h!==null?h.dehydrated:null,!h)throw Error(a(317));h[ft]=i}else Yi(),(i.flags&128)===0&&(i.memoizedState=null),i.flags|=4;Ge(i),h=!1}else h=Fu(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=h),h=!0;if(!h)return i.flags&256?($t(i),i):($t(i),null)}return $t(i),(i.flags&128)!==0?(i.lanes=r,i):(r=o!==null,t=t!==null&&t.memoizedState!==null,r&&(o=i.child,h=null,o.alternate!==null&&o.alternate.memoizedState!==null&&o.alternate.memoizedState.cachePool!==null&&(h=o.alternate.memoizedState.cachePool.pool),f=null,o.memoizedState!==null&&o.memoizedState.cachePool!==null&&(f=o.memoizedState.cachePool.pool),f!==h&&(o.flags|=2048)),r!==t&&r&&(i.child.flags|=8192),Vo(i,i.updateQueue),Ge(i),null);case 4:return He(),t===null&&ah(i.stateNode.containerInfo),Ge(i),null;case 10:return In(i.type),Ge(i),null;case 19:if(B(Ze),o=i.memoizedState,o===null)return Ge(i),null;if(h=(i.flags&128)!==0,f=o.rendering,f===null)if(h)Xa(o,!1);else{if(Xe!==0||t!==null&&(t.flags&128)!==0)for(t=i.child;t!==null;){if(f=Ao(t),f!==null){for(i.flags|=128,Xa(o,!1),t=f.updateQueue,i.updateQueue=t,Vo(i,t),i.subtreeFlags=0,t=r,r=i.child;r!==null;)Em(r,t),r=r.sibling;return K(Ze,Ze.current&1|2),Te&&$n(i,o.treeForkCount),i.child}t=t.sibling}o.tail!==null&&Me()>Io&&(i.flags|=128,h=!0,Xa(o,!1),i.lanes=4194304)}else{if(!h)if(t=Ao(f),t!==null){if(i.flags|=128,h=!0,t=t.updateQueue,i.updateQueue=t,Vo(i,t),Xa(o,!0),o.tail===null&&o.tailMode==="hidden"&&!f.alternate&&!Te)return Ge(i),null}else 2*Me()-o.renderingStartTime>Io&&r!==536870912&&(i.flags|=128,h=!0,Xa(o,!1),i.lanes=4194304);o.isBackwards?(f.sibling=i.child,i.child=f):(t=o.last,t!==null?t.sibling=f:i.child=f,o.last=f)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=Me(),t.sibling=null,r=Ze.current,K(Ze,h?r&1|2:r&1),Te&&$n(i,o.treeForkCount),t):(Ge(i),null);case 22:case 23:return $t(i),rc(),o=i.memoizedState!==null,t!==null?t.memoizedState!==null!==o&&(i.flags|=8192):o&&(i.flags|=8192),o?(r&536870912)!==0&&(i.flags&128)===0&&(Ge(i),i.subtreeFlags&6&&(i.flags|=8192)):Ge(i),r=i.updateQueue,r!==null&&Vo(i,r.retryQueue),r=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),o=null,i.memoizedState!==null&&i.memoizedState.cachePool!==null&&(o=i.memoizedState.cachePool.pool),o!==r&&(i.flags|=2048),t!==null&&B(Qi),null;case 24:return r=null,t!==null&&(r=t.memoizedState.cache),i.memoizedState.cache!==r&&(i.flags|=2048),In(tt),Ge(i),null;case 25:return null;case 30:return null}throw Error(a(156,i.tag))}function Bx(t,i){switch(Gu(i),i.tag){case 1:return t=i.flags,t&65536?(i.flags=t&-65537|128,i):null;case 3:return In(tt),He(),t=i.flags,(t&65536)!==0&&(t&128)===0?(i.flags=t&-65537|128,i):null;case 26:case 27:case 5:return wt(i),null;case 31:if(i.memoizedState!==null){if($t(i),i.alternate===null)throw Error(a(340));Yi()}return t=i.flags,t&65536?(i.flags=t&-65537|128,i):null;case 13:if($t(i),t=i.memoizedState,t!==null&&t.dehydrated!==null){if(i.alternate===null)throw Error(a(340));Yi()}return t=i.flags,t&65536?(i.flags=t&-65537|128,i):null;case 19:return B(Ze),null;case 4:return He(),null;case 10:return In(i.type),null;case 22:case 23:return $t(i),rc(),t!==null&&B(Qi),t=i.flags,t&65536?(i.flags=t&-65537|128,i):null;case 24:return In(tt),null;case 25:return null;default:return null}}function Zp(t,i){switch(Gu(i),i.tag){case 3:In(tt),He();break;case 26:case 27:case 5:wt(i);break;case 4:He();break;case 31:i.memoizedState!==null&&$t(i);break;case 13:$t(i);break;case 19:B(Ze);break;case 10:In(i.type);break;case 22:case 23:$t(i),rc(),t!==null&&B(Qi);break;case 24:In(tt)}}function Ja(t,i){try{var r=i.updateQueue,o=r!==null?r.lastEffect:null;if(o!==null){var h=o.next;r=h;do{if((r.tag&t)===t){o=void 0;var f=r.create,y=r.inst;o=f(),y.destroy=o}r=r.next}while(r!==h)}}catch(_){Ne(i,i.return,_)}}function wi(t,i,r){try{var o=i.updateQueue,h=o!==null?o.lastEffect:null;if(h!==null){var f=h.next;o=f;do{if((o.tag&t)===t){var y=o.inst,_=y.destroy;if(_!==void 0){y.destroy=void 0,h=i;var T=r,M=_;try{M()}catch(H){Ne(h,T,H)}}}o=o.next}while(o!==f)}}catch(H){Ne(i,i.return,H)}}function Wp(t){var i=t.updateQueue;if(i!==null){var r=t.stateNode;try{qm(i,r)}catch(o){Ne(t,t.return,o)}}}function eg(t,i,r){r.props=ns(t.type,t.memoizedProps),r.state=t.memoizedState;try{r.componentWillUnmount()}catch(o){Ne(t,i,o)}}function Qa(t,i){try{var r=t.ref;if(r!==null){switch(t.tag){case 26:case 27:case 5:var o=t.stateNode;break;case 30:o=t.stateNode;break;default:o=t.stateNode}typeof r=="function"?t.refCleanup=r(o):r.current=o}}catch(h){Ne(t,i,h)}}function An(t,i){var r=t.ref,o=t.refCleanup;if(r!==null)if(typeof o=="function")try{o()}catch(h){Ne(t,i,h)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof r=="function")try{r(null)}catch(h){Ne(t,i,h)}else r.current=null}function tg(t){var i=t.type,r=t.memoizedProps,o=t.stateNode;try{e:switch(i){case"button":case"input":case"select":case"textarea":r.autoFocus&&o.focus();break e;case"img":r.src?o.src=r.src:r.srcSet&&(o.srcset=r.srcSet)}}catch(h){Ne(t,t.return,h)}}function zc(t,i,r){try{var o=t.stateNode;sS(o,t.type,r,i),o[Rt]=i}catch(h){Ne(t,t.return,h)}}function ng(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&Ri(t.type)||t.tag===4}function Vc(t){e:for(;;){for(;t.sibling===null;){if(t.return===null||ng(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&Ri(t.type)||t.flags&2||t.child===null||t.tag===4)continue e;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Pc(t,i,r){var o=t.tag;if(o===5||o===6)t=t.stateNode,i?(r.nodeType===9?r.body:r.nodeName==="HTML"?r.ownerDocument.body:r).insertBefore(t,i):(i=r.nodeType===9?r.body:r.nodeName==="HTML"?r.ownerDocument.body:r,i.appendChild(t),r=r._reactRootContainer,r!=null||i.onclick!==null||(i.onclick=Vn));else if(o!==4&&(o===27&&Ri(t.type)&&(r=t.stateNode,i=null),t=t.child,t!==null))for(Pc(t,i,r),t=t.sibling;t!==null;)Pc(t,i,r),t=t.sibling}function Po(t,i,r){var o=t.tag;if(o===5||o===6)t=t.stateNode,i?r.insertBefore(t,i):r.appendChild(t);else if(o!==4&&(o===27&&Ri(t.type)&&(r=t.stateNode),t=t.child,t!==null))for(Po(t,i,r),t=t.sibling;t!==null;)Po(t,i,r),t=t.sibling}function ig(t){var i=t.stateNode,r=t.memoizedProps;try{for(var o=t.type,h=i.attributes;h.length;)i.removeAttributeNode(h[0]);yt(i,o,r),i[ft]=t,i[Rt]=r}catch(f){Ne(t,t.return,f)}}var Xn=!1,st=!1,Hc=!1,sg=typeof WeakSet=="function"?WeakSet:Set,ct=null;function Lx(t,i){if(t=t.containerInfo,lh=ol,t=gm(t),Mu(t)){if("selectionStart"in t)var r={start:t.selectionStart,end:t.selectionEnd};else e:{r=(r=t.ownerDocument)&&r.defaultView||window;var o=r.getSelection&&r.getSelection();if(o&&o.rangeCount!==0){r=o.anchorNode;var h=o.anchorOffset,f=o.focusNode;o=o.focusOffset;try{r.nodeType,f.nodeType}catch{r=null;break e}var y=0,_=-1,T=-1,M=0,H=0,q=t,U=null;t:for(;;){for(var z;q!==r||h!==0&&q.nodeType!==3||(_=y+h),q!==f||o!==0&&q.nodeType!==3||(T=y+o),q.nodeType===3&&(y+=q.nodeValue.length),(z=q.firstChild)!==null;)U=q,q=z;for(;;){if(q===t)break t;if(U===r&&++M===h&&(_=y),U===f&&++H===o&&(T=y),(z=q.nextSibling)!==null)break;q=U,U=q.parentNode}q=z}r=_===-1||T===-1?null:{start:_,end:T}}else r=null}r=r||{start:0,end:0}}else r=null;for(uh={focusedElem:t,selectionRange:r},ol=!1,ct=i;ct!==null;)if(i=ct,t=i.child,(i.subtreeFlags&1028)!==0&&t!==null)t.return=i,ct=t;else for(;ct!==null;){switch(i=ct,f=i.alternate,t=i.flags,i.tag){case 0:if((t&4)!==0&&(t=i.updateQueue,t=t!==null?t.events:null,t!==null))for(r=0;r title"))),yt(f,o,r),f[ft]=t,ut(f),o=f;break e;case"link":var y=cy("link","href",h).get(o+(r.href||""));if(y){for(var _=0;_Le&&(y=Le,Le=me,me=y);var j=mm(_,me),R=mm(_,Le);if(j&&R&&(z.rangeCount!==1||z.anchorNode!==j.node||z.anchorOffset!==j.offset||z.focusNode!==R.node||z.focusOffset!==R.offset)){var D=q.createRange();D.setStart(j.node,j.offset),z.removeAllRanges(),me>Le?(z.addRange(D),z.extend(R.node,R.offset)):(D.setEnd(R.node,R.offset),z.addRange(D))}}}}for(q=[],z=_;z=z.parentNode;)z.nodeType===1&&q.push({element:z,left:z.scrollLeft,top:z.scrollTop});for(typeof _.focus=="function"&&_.focus(),_=0;_r?32:r,L.T=null,r=Yc,Yc=null;var f=Ti,y=ei;if(ot=0,Ks=Ti=null,ei=0,(Ce&6)!==0)throw Error(a(331));var _=Ce;if(Ce|=4,pg(f.current),dg(f,f.current,y,r),Ce=_,ir(0,!1),Lt&&typeof Lt.onPostCommitFiberRoot=="function")try{Lt.onPostCommitFiberRoot(_a,f)}catch{}return!0}finally{Q.p=h,L.T=o,Dg(t,i)}}function Ug(t,i,r){i=Zt(r,i),i=Rc(t.stateNode,i,2),t=yi(t,i,2),t!==null&&(Sa(t,2),Rn(t))}function Ne(t,i,r){if(t.tag===3)Ug(t,t,r);else for(;i!==null;){if(i.tag===3){Ug(i,t,r);break}else if(i.tag===1){var o=i.stateNode;if(typeof i.type.getDerivedStateFromError=="function"||typeof o.componentDidCatch=="function"&&(Si===null||!Si.has(o))){t=Zt(r,t),r=Bp(2),o=yi(i,r,2),o!==null&&(Lp(r,o,i,t),Sa(o,2),Rn(o));break}}i=i.return}}function Zc(t,i,r){var o=t.pingCache;if(o===null){o=t.pingCache=new Px;var h=new Set;o.set(i,h)}else h=o.get(i),h===void 0&&(h=new Set,o.set(i,h));h.has(r)||(Ic=!0,h.add(r),t=Gx.bind(null,t,i,r),i.then(t,t))}function Gx(t,i,r){var o=t.pingCache;o!==null&&o.delete(i),t.pingedLanes|=t.suspendedLanes&r,t.warmLanes&=~r,ze===t&&(xe&r)===r&&(Xe===4||Xe===3&&(xe&62914560)===xe&&300>Me()-qo?(Ce&2)===0&&Fs(t,0):Gc|=r,Gs===xe&&(Gs=0)),Rn(t)}function Bg(t,i){i===0&&(i=jf()),t=Ki(t,i),t!==null&&(Sa(t,i),Rn(t))}function Kx(t){var i=t.memoizedState,r=0;i!==null&&(r=i.retryLane),Bg(t,r)}function Fx(t,i){var r=0;switch(t.tag){case 31:case 13:var o=t.stateNode,h=t.memoizedState;h!==null&&(r=h.retryLane);break;case 19:o=t.stateNode;break;case 22:o=t.stateNode._retryCache;break;default:throw Error(a(314))}o!==null&&o.delete(i),Bg(t,r)}function Yx(t,i){return Ln(t,i)}var Jo=null,Xs=null,Wc=!1,Qo=!1,eh=!1,Ai=0;function Rn(t){t!==Xs&&t.next===null&&(Xs===null?Jo=Xs=t:Xs=Xs.next=t),Qo=!0,Wc||(Wc=!0,Jx())}function ir(t,i){if(!eh&&Qo){eh=!0;do for(var r=!1,o=Jo;o!==null;){if(t!==0){var h=o.pendingLanes;if(h===0)var f=0;else{var y=o.suspendedLanes,_=o.pingedLanes;f=(1<<31-zt(42|t)+1)-1,f&=h&~(y&~_),f=f&201326741?f&201326741|1:f?f|2:0}f!==0&&(r=!0,Pg(o,f))}else f=xe,f=eo(o,o===ze?f:0,o.cancelPendingCommit!==null||o.timeoutHandle!==-1),(f&3)===0||xa(o,f)||(r=!0,Pg(o,f));o=o.next}while(r);eh=!1}}function Xx(){Lg()}function Lg(){Qo=Wc=!1;var t=0;Ai!==0&&rS()&&(t=Ai);for(var i=Me(),r=null,o=Jo;o!==null;){var h=o.next,f=zg(o,i);f===0?(o.next=null,r===null?Jo=h:r.next=h,h===null&&(Xs=r)):(r=o,(t!==0||(f&3)!==0)&&(Qo=!0)),o=h}ot!==0&&ot!==5||ir(t),Ai!==0&&(Ai=0)}function zg(t,i){for(var r=t.suspendedLanes,o=t.pingedLanes,h=t.expirationTimes,f=t.pendingLanes&-62914561;0_)break;var H=T.transferSize,q=T.initiatorType;H&&Yg(q)&&(T=T.responseEnd,y+=H*(T<_?1:(_-M)/(T-M)))}if(--o,i+=8*(f+y)/(h.duration/1e3),t++,10"u"?null:document;function ry(t,i,r){var o=Js;if(o&&typeof i=="string"&&i){var h=Jt(i);h='link[rel="'+t+'"][href="'+h+'"]',typeof r=="string"&&(h+='[crossorigin="'+r+'"]'),ay.has(h)||(ay.add(h),t={rel:t,crossOrigin:r,href:i},o.querySelector(h)===null&&(i=o.createElement("link"),yt(i,"link",t),ut(i),o.head.appendChild(i)))}}function pS(t){ti.D(t),ry("dns-prefetch",t,null)}function gS(t,i){ti.C(t,i),ry("preconnect",t,i)}function yS(t,i,r){ti.L(t,i,r);var o=Js;if(o&&t&&i){var h='link[rel="preload"][as="'+Jt(i)+'"]';i==="image"&&r&&r.imageSrcSet?(h+='[imagesrcset="'+Jt(r.imageSrcSet)+'"]',typeof r.imageSizes=="string"&&(h+='[imagesizes="'+Jt(r.imageSizes)+'"]')):h+='[href="'+Jt(t)+'"]';var f=h;switch(i){case"style":f=Qs(t);break;case"script":f=Zs(t)}an.has(f)||(t=v({rel:"preload",href:i==="image"&&r&&r.imageSrcSet?void 0:t,as:i},r),an.set(f,t),o.querySelector(h)!==null||i==="style"&&o.querySelector(or(f))||i==="script"&&o.querySelector(lr(f))||(i=o.createElement("link"),yt(i,"link",t),ut(i),o.head.appendChild(i)))}}function vS(t,i){ti.m(t,i);var r=Js;if(r&&t){var o=i&&typeof i.as=="string"?i.as:"script",h='link[rel="modulepreload"][as="'+Jt(o)+'"][href="'+Jt(t)+'"]',f=h;switch(o){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":f=Zs(t)}if(!an.has(f)&&(t=v({rel:"modulepreload",href:t},i),an.set(f,t),r.querySelector(h)===null)){switch(o){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(r.querySelector(lr(f)))return}o=r.createElement("link"),yt(o,"link",t),ut(o),r.head.appendChild(o)}}}function bS(t,i,r){ti.S(t,i,r);var o=Js;if(o&&t){var h=bs(o).hoistableStyles,f=Qs(t);i=i||"default";var y=h.get(f);if(!y){var _={loading:0,preload:null};if(y=o.querySelector(or(f)))_.loading=5;else{t=v({rel:"stylesheet",href:t,"data-precedence":i},r),(r=an.get(f))&&gh(t,r);var T=y=o.createElement("link");ut(T),yt(T,"link",t),T._p=new Promise(function(M,H){T.onload=M,T.onerror=H}),T.addEventListener("load",function(){_.loading|=1}),T.addEventListener("error",function(){_.loading|=2}),_.loading|=4,nl(y,i,o)}y={type:"stylesheet",instance:y,count:1,state:_},h.set(f,y)}}}function wS(t,i){ti.X(t,i);var r=Js;if(r&&t){var o=bs(r).hoistableScripts,h=Zs(t),f=o.get(h);f||(f=r.querySelector(lr(h)),f||(t=v({src:t,async:!0},i),(i=an.get(h))&&yh(t,i),f=r.createElement("script"),ut(f),yt(f,"link",t),r.head.appendChild(f)),f={type:"script",instance:f,count:1,state:null},o.set(h,f))}}function _S(t,i){ti.M(t,i);var r=Js;if(r&&t){var o=bs(r).hoistableScripts,h=Zs(t),f=o.get(h);f||(f=r.querySelector(lr(h)),f||(t=v({src:t,async:!0,type:"module"},i),(i=an.get(h))&&yh(t,i),f=r.createElement("script"),ut(f),yt(f,"link",t),r.head.appendChild(f)),f={type:"script",instance:f,count:1,state:null},o.set(h,f))}}function oy(t,i,r,o){var h=(h=ue.current)?tl(h):null;if(!h)throw Error(a(446));switch(t){case"meta":case"title":return null;case"style":return typeof r.precedence=="string"&&typeof r.href=="string"?(i=Qs(r.href),r=bs(h).hoistableStyles,o=r.get(i),o||(o={type:"style",instance:null,count:0,state:null},r.set(i,o)),o):{type:"void",instance:null,count:0,state:null};case"link":if(r.rel==="stylesheet"&&typeof r.href=="string"&&typeof r.precedence=="string"){t=Qs(r.href);var f=bs(h).hoistableStyles,y=f.get(t);if(y||(h=h.ownerDocument||h,y={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},f.set(t,y),(f=h.querySelector(or(t)))&&!f._p&&(y.instance=f,y.state.loading=5),an.has(t)||(r={rel:"preload",as:"style",href:r.href,crossOrigin:r.crossOrigin,integrity:r.integrity,media:r.media,hrefLang:r.hrefLang,referrerPolicy:r.referrerPolicy},an.set(t,r),f||xS(h,t,r,y.state))),i&&o===null)throw Error(a(528,""));return y}if(i&&o!==null)throw Error(a(529,""));return null;case"script":return i=r.async,r=r.src,typeof r=="string"&&i&&typeof i!="function"&&typeof i!="symbol"?(i=Zs(r),r=bs(h).hoistableScripts,o=r.get(i),o||(o={type:"script",instance:null,count:0,state:null},r.set(i,o)),o):{type:"void",instance:null,count:0,state:null};default:throw Error(a(444,t))}}function Qs(t){return'href="'+Jt(t)+'"'}function or(t){return'link[rel="stylesheet"]['+t+"]"}function ly(t){return v({},t,{"data-precedence":t.precedence,precedence:null})}function xS(t,i,r,o){t.querySelector('link[rel="preload"][as="style"]['+i+"]")?o.loading=1:(i=t.createElement("link"),o.preload=i,i.addEventListener("load",function(){return o.loading|=1}),i.addEventListener("error",function(){return o.loading|=2}),yt(i,"link",r),ut(i),t.head.appendChild(i))}function Zs(t){return'[src="'+Jt(t)+'"]'}function lr(t){return"script[async]"+t}function uy(t,i,r){if(i.count++,i.instance===null)switch(i.type){case"style":var o=t.querySelector('style[data-href~="'+Jt(r.href)+'"]');if(o)return i.instance=o,ut(o),o;var h=v({},r,{"data-href":r.href,"data-precedence":r.precedence,href:null,precedence:null});return o=(t.ownerDocument||t).createElement("style"),ut(o),yt(o,"style",h),nl(o,r.precedence,t),i.instance=o;case"stylesheet":h=Qs(r.href);var f=t.querySelector(or(h));if(f)return i.state.loading|=4,i.instance=f,ut(f),f;o=ly(r),(h=an.get(h))&&gh(o,h),f=(t.ownerDocument||t).createElement("link"),ut(f);var y=f;return y._p=new Promise(function(_,T){y.onload=_,y.onerror=T}),yt(f,"link",o),i.state.loading|=4,nl(f,r.precedence,t),i.instance=f;case"script":return f=Zs(r.src),(h=t.querySelector(lr(f)))?(i.instance=h,ut(h),h):(o=r,(h=an.get(f))&&(o=v({},r),yh(o,h)),t=t.ownerDocument||t,h=t.createElement("script"),ut(h),yt(h,"link",o),t.head.appendChild(h),i.instance=h);case"void":return null;default:throw Error(a(443,i.type))}else i.type==="stylesheet"&&(i.state.loading&4)===0&&(o=i.instance,i.state.loading|=4,nl(o,r.precedence,t));return i.instance}function nl(t,i,r){for(var o=r.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),h=o.length?o[o.length-1]:null,f=h,y=0;y title"):null)}function SS(t,i,r){if(r===1||i.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof i.precedence!="string"||typeof i.href!="string"||i.href==="")break;return!0;case"link":if(typeof i.rel!="string"||typeof i.href!="string"||i.href===""||i.onLoad||i.onError)break;switch(i.rel){case"stylesheet":return t=i.disabled,typeof i.precedence=="string"&&t==null;default:return!0}case"script":if(i.async&&typeof i.async!="function"&&typeof i.async!="symbol"&&!i.onLoad&&!i.onError&&i.src&&typeof i.src=="string")return!0}return!1}function dy(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function TS(t,i,r,o){if(r.type==="stylesheet"&&(typeof o.media!="string"||matchMedia(o.media).matches!==!1)&&(r.state.loading&4)===0){if(r.instance===null){var h=Qs(o.href),f=i.querySelector(or(h));if(f){i=f._p,i!==null&&typeof i=="object"&&typeof i.then=="function"&&(t.count++,t=sl.bind(t),i.then(t,t)),r.state.loading|=4,r.instance=f,ut(f);return}f=i.ownerDocument||i,o=ly(o),(h=an.get(h))&&gh(o,h),f=f.createElement("link"),ut(f);var y=f;y._p=new Promise(function(_,T){y.onload=_,y.onerror=T}),yt(f,"link",o),r.instance=f}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(r,i),(i=r.state.preload)&&(r.state.loading&3)===0&&(t.count++,r=sl.bind(t),i.addEventListener("load",r),i.addEventListener("error",r))}}var vh=0;function ES(t,i){return t.stylesheets&&t.count===0&&rl(t,t.stylesheets),0vh?50:800)+i);return t.unsuspend=r,function(){t.unsuspend=null,clearTimeout(o),clearTimeout(h)}}:null}function sl(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)rl(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var al=null;function rl(t,i){t.stylesheets=null,t.unsuspend!==null&&(t.count++,al=new Map,i.forEach(AS,t),al=null,sl.call(t))}function AS(t,i){if(!(i.state.loading&4)){var r=al.get(t);if(r)var o=r.get(null);else{r=new Map,al.set(t,r);for(var h=t.querySelectorAll("link[data-precedence],style[data-precedence]"),f=0;f"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}return n(),Rh.exports=$S(),Rh.exports}var IS=qS();const Id=V.createContext({});function Gd(n){const e=V.useRef(null);return e.current===null&&(e.current=n()),e.current}const GS=typeof window<"u",Kd=GS?V.useLayoutEffect:V.useEffect,iu=V.createContext(null);function Fd(n,e){n.indexOf(e)===-1&&n.push(e)}function Vl(n,e){const s=n.indexOf(e);s>-1&&n.splice(s,1)}const Mn=(n,e,s)=>s>e?e:s{};const Li={},eb=n=>/^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(n),tb=n=>typeof n=="object"&&n!==null,nb=n=>/^0[^.\s]+$/u.test(n);function ib(n){let e;return()=>(e===void 0&&(e=n()),e)}const ln=n=>n,qr=(...n)=>n.reduce((e,s)=>a=>s(e(a))),jr=(n,e,s)=>{const a=e-n;return a?(s-n)/a:1};class Xd{constructor(){this.subscriptions=[]}add(e){return Fd(this.subscriptions,e),()=>Vl(this.subscriptions,e)}notify(e,s,a){const l=this.subscriptions.length;if(l)if(l===1)this.subscriptions[0](e,s,a);else for(let u=0;un*1e3,on=n=>n/1e3,sb=(n,e)=>e?n*(1e3/e):0,ab=(n,e,s)=>(((1-3*s+3*e)*n+(3*s-6*e))*n+3*e)*n,KS=1e-7,FS=12;function YS(n,e,s,a,l){let u,c,d=0;do c=e+(s-e)/2,u=ab(c,a,l)-n,u>0?s=c:e=c;while(Math.abs(u)>KS&&++dYS(u,0,1,n,s);return u=>u===0||u===1?u:ab(l(u),e,a)}const rb=n=>e=>e<=.5?n(2*e)/2:(2-n(2*(1-e)))/2,ob=n=>e=>1-n(1-e),lb=Ir(.33,1.53,.69,.99),Jd=ob(lb),ub=rb(Jd),cb=n=>n>=1?1:(n*=2)<1?.5*Jd(n):.5*(2-Math.pow(2,-10*(n-1))),Qd=n=>1-Math.sin(Math.acos(n)),hb=ob(Qd),db=rb(Qd),XS=Ir(.42,0,1,1),JS=Ir(0,0,.58,1),fb=Ir(.42,0,.58,1),QS=n=>Array.isArray(n)&&typeof n[0]!="number",mb=n=>Array.isArray(n)&&typeof n[0]=="number",ZS={linear:ln,easeIn:XS,easeInOut:fb,easeOut:JS,circIn:Qd,circInOut:db,circOut:hb,backIn:Jd,backInOut:ub,backOut:lb,anticipate:cb},WS=n=>typeof n=="string",By=n=>{if(mb(n)){Yd(n.length===4);const[e,s,a,l]=n;return Ir(e,s,a,l)}else if(WS(n))return ZS[n];return n},ml=["setup","read","resolveKeyframes","preUpdate","update","preRender","render","postRender"];function e1(n){let e=new Set,s=new Set,a=!1,l=!1;const u=new WeakSet;let c={delta:0,timestamp:0,isProcessing:!1};function d(p){u.has(p)&&(m.schedule(p),n()),p(c)}const m={schedule:(p,g=!1,v=!1)=>{const x=v&&a?e:s;return g&&u.add(p),x.add(p),p},cancel:p=>{s.delete(p),u.delete(p)},process:p=>{if(c=p,a){l=!0;return}a=!0;const g=e;e=s,s=g,e.forEach(d),e.clear(),a=!1,l&&(l=!1,m.process(p))}};return m}const t1=40;function pb(n,e){let s=!1,a=!0;const l={delta:0,timestamp:0,isProcessing:!1},u=()=>s=!0,c=ml.reduce((O,P)=>(O[P]=e1(u),O),{}),{setup:d,read:m,resolveKeyframes:p,preUpdate:g,update:v,preRender:w,render:x,postRender:S}=c,A=()=>{const O=Li.useManualTiming,P=O?l.timestamp:performance.now();s=!1,O||(l.delta=a?1e3/60:Math.max(Math.min(P-l.timestamp,t1),1)),l.timestamp=P,l.isProcessing=!0,d.process(l),m.process(l),p.process(l),g.process(l),v.process(l),w.process(l),x.process(l),S.process(l),l.isProcessing=!1,s&&e&&(a=!1,n(A))},k=()=>{s=!0,a=!0,l.isProcessing||n(A)};return{schedule:ml.reduce((O,P)=>{const J=c[P];return O[P]=(X,G=!1,Z=!1)=>(s||k(),J.schedule(X,G,Z)),O},{}),cancel:O=>{for(let P=0;P(kl===void 0&&Tt.set(vt.isProcessing||Li.useManualTiming?vt.timestamp:performance.now()),kl),set:n=>{kl=n,queueMicrotask(n1)}},gb=n=>e=>typeof e=="string"&&e.startsWith(n),yb=gb("--"),i1=gb("var(--"),Zd=n=>i1(n)?s1.test(n.split("/*")[0].trim()):!1,s1=/var\(--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)$/iu;function Ly(n){return typeof n!="string"?!1:n.split("/*")[0].includes("var(--")}const ya={test:n=>typeof n=="number",parse:parseFloat,transform:n=>n},Or={...ya,transform:n=>Mn(0,1,n)},pl={...ya,default:1},xr=n=>Math.round(n*1e5)/1e5,Wd=/-?(?:\d+(?:\.\d+)?|\.\d+)/gu;function a1(n){return n==null}const r1=/^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))$/iu,ef=(n,e)=>s=>!!(typeof s=="string"&&r1.test(s)&&s.startsWith(n)||e&&!a1(s)&&Object.prototype.hasOwnProperty.call(s,e)),vb=(n,e,s)=>a=>{if(typeof a!="string")return a;const[l,u,c,d]=a.match(Wd);return{[n]:parseFloat(l),[e]:parseFloat(u),[s]:parseFloat(c),alpha:d!==void 0?parseFloat(d):1}},o1=n=>Mn(0,255,n),Nh={...ya,transform:n=>Math.round(o1(n))},hs={test:ef("rgb","red"),parse:vb("red","green","blue"),transform:({red:n,green:e,blue:s,alpha:a=1})=>"rgba("+Nh.transform(n)+", "+Nh.transform(e)+", "+Nh.transform(s)+", "+xr(Or.transform(a))+")"};function l1(n){let e="",s="",a="",l="";return n.length>5?(e=n.substring(1,3),s=n.substring(3,5),a=n.substring(5,7),l=n.substring(7,9)):(e=n.substring(1,2),s=n.substring(2,3),a=n.substring(3,4),l=n.substring(4,5),e+=e,s+=s,a+=a,l+=l),{red:parseInt(e,16),green:parseInt(s,16),blue:parseInt(a,16),alpha:l?parseInt(l,16)/255:1}}const sd={test:ef("#"),parse:l1,transform:hs.transform},Gr=n=>({test:e=>typeof e=="string"&&e.endsWith(n)&&e.split(" ").length===1,parse:parseFloat,transform:e=>`${e}${n}`}),ii=Gr("deg"),Dn=Gr("%"),se=Gr("px"),u1=Gr("vh"),c1=Gr("vw"),zy={...Dn,parse:n=>Dn.parse(n)/100,transform:n=>Dn.transform(n*100)},ua={test:ef("hsl","hue"),parse:vb("hue","saturation","lightness"),transform:({hue:n,saturation:e,lightness:s,alpha:a=1})=>"hsla("+Math.round(n)+", "+Dn.transform(xr(e))+", "+Dn.transform(xr(s))+", "+xr(Or.transform(a))+")"},rt={test:n=>hs.test(n)||sd.test(n)||ua.test(n),parse:n=>hs.test(n)?hs.parse(n):ua.test(n)?ua.parse(n):sd.parse(n),transform:n=>typeof n=="string"?n:n.hasOwnProperty("red")?hs.transform(n):ua.transform(n),getAnimatableNone:n=>{const e=rt.parse(n);return e.alpha=0,rt.transform(e)}},h1=/(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))/giu;function d1(n){var e,s;return isNaN(n)&&typeof n=="string"&&(((e=n.match(Wd))==null?void 0:e.length)||0)+(((s=n.match(h1))==null?void 0:s.length)||0)>0}const bb="number",wb="color",f1="var",m1="var(",Vy="${}",p1=/var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu;function pa(n){const e=n.toString(),s=[],a={color:[],number:[],var:[]},l=[];let u=0;const d=e.replace(p1,m=>(rt.test(m)?(a.color.push(u),l.push(wb),s.push(rt.parse(m))):m.startsWith(m1)?(a.var.push(u),l.push(f1),s.push(m)):(a.number.push(u),l.push(bb),s.push(parseFloat(m))),++u,Vy)).split(Vy);return{values:s,split:d,indexes:a,types:l}}function g1(n){return pa(n).values}function _b({split:n,types:e}){const s=n.length;return a=>{let l="";for(let u=0;utypeof n=="number"?0:rt.test(n)?rt.getAnimatableNone(n):n,b1=(n,e)=>typeof n=="number"?e!=null&&e.trim().endsWith("/")?n:0:v1(n);function w1(n){const e=pa(n);return _b(e)(e.values.map((a,l)=>b1(a,e.split[l])))}const wn={test:d1,parse:g1,createTransformer:y1,getAnimatableNone:w1};function Dh(n,e,s){return s<0&&(s+=1),s>1&&(s-=1),s<1/6?n+(e-n)*6*s:s<1/2?e:s<2/3?n+(e-n)*(2/3-s)*6:n}function _1({hue:n,saturation:e,lightness:s,alpha:a}){n/=360,e/=100,s/=100;let l=0,u=0,c=0;if(!e)l=u=c=s;else{const d=s<.5?s*(1+e):s+e-s*e,m=2*s-d;l=Dh(m,d,n+1/3),u=Dh(m,d,n),c=Dh(m,d,n-1/3)}return{red:Math.round(l*255),green:Math.round(u*255),blue:Math.round(c*255),alpha:a}}function Pl(n,e){return s=>s>0?e:n}const Ve=(n,e,s)=>n+(e-n)*s,Mh=(n,e,s)=>{const a=n*n,l=s*(e*e-a)+a;return l<0?0:Math.sqrt(l)},x1=[sd,hs,ua],S1=n=>x1.find(e=>e.test(n));function Py(n){const e=S1(n);if(!e)return!1;let s=e.parse(n);return e===ua&&(s=_1(s)),s}const Hy=(n,e)=>{const s=Py(n),a=Py(e);if(!s||!a)return Pl(n,e);const l={...s};return u=>(l.red=Mh(s.red,a.red,u),l.green=Mh(s.green,a.green,u),l.blue=Mh(s.blue,a.blue,u),l.alpha=Ve(s.alpha,a.alpha,u),hs.transform(l))},ad=new Set(["none","hidden"]);function T1(n,e){return ad.has(n)?s=>s<=0?n:e:s=>s>=1?e:n}function E1(n,e){return s=>Ve(n,e,s)}function tf(n){return typeof n=="number"?E1:typeof n=="string"?Zd(n)?Pl:rt.test(n)?Hy:C1:Array.isArray(n)?xb:typeof n=="object"?rt.test(n)?Hy:A1:Pl}function xb(n,e){const s=[...n],a=s.length,l=n.map((u,c)=>tf(u)(u,e[c]));return u=>{for(let c=0;c{for(const u in a)s[u]=a[u](l);return s}}function R1(n,e){const s=[],a={color:0,var:0,number:0};for(let l=0;l{const s=wn.createTransformer(e),a=pa(n),l=pa(e);return a.indexes.var.length===l.indexes.var.length&&a.indexes.color.length===l.indexes.color.length&&a.indexes.number.length>=l.indexes.number.length?ad.has(n)&&!l.values.length||ad.has(e)&&!a.values.length?T1(n,e):qr(xb(R1(a,l),l.values),s):Pl(n,e)};function Sb(n,e,s){return typeof n=="number"&&typeof e=="number"&&typeof s=="number"?Ve(n,e,s):tf(n)(n,e)}const k1=n=>{const e=({timestamp:s})=>n(s);return{start:(s=!0)=>Pe.update(e,s),stop:()=>zi(e),now:()=>vt.isProcessing?vt.timestamp:Tt.now()}},Tb=(n,e,s=10)=>{let a="";const l=Math.max(Math.round(e/s),2);for(let u=0;u=Hl?1/0:e}function j1(n,e=100,s){const a=s({...n,keyframes:[0,e]}),l=Math.min(nf(a),Hl);return{type:"keyframes",ease:u=>a.next(l*u).value/e,duration:on(l)}}const Qe={stiffness:100,damping:10,mass:1,velocity:0,duration:800,bounce:.3,visualDuration:.3,restSpeed:{granular:.01,default:2},restDelta:{granular:.005,default:.5},minDuration:.01,maxDuration:10,minDamping:.05,maxDamping:1};function rd(n,e){return n*Math.sqrt(1-e*e)}const O1=12;function N1(n,e,s){let a=s;for(let l=1;l{const g=p*c,v=g*n,w=g-s,x=rd(p,c),S=Math.exp(-v);return Uh-w/x*S},u=p=>{const v=p*c*n,w=v*s+s,x=Math.pow(c,2)*Math.pow(p,2)*n,S=Math.exp(-v),A=rd(Math.pow(p,2),c);return(-l(p)+Uh>0?-1:1)*((w-x)*S)/A}):(l=p=>{const g=Math.exp(-p*n),v=(p-s)*n+1;return-Uh+g*v},u=p=>{const g=Math.exp(-p*n),v=(s-p)*(n*n);return g*v});const d=5/n,m=N1(l,u,d);if(n=Ft(n),isNaN(m))return{stiffness:Qe.stiffness,damping:Qe.damping,duration:n};{const p=Math.pow(m,2)*a;return{stiffness:p,damping:c*2*Math.sqrt(a*p),duration:n}}}const M1=["duration","bounce"],U1=["stiffness","damping","mass"];function $y(n,e){return e.some(s=>n[s]!==void 0)}function B1(n){let e={velocity:Qe.velocity,stiffness:Qe.stiffness,damping:Qe.damping,mass:Qe.mass,isResolvedFromDuration:!1,...n};if(!$y(n,U1)&&$y(n,M1))if(e.velocity=0,n.visualDuration){const s=n.visualDuration,a=2*Math.PI/(s*1.2),l=a*a,u=2*Mn(.05,1,1-(n.bounce||0))*Math.sqrt(l);e={...e,mass:Qe.mass,stiffness:l,damping:u}}else{const s=D1({...n,velocity:0});e={...e,...s,mass:Qe.mass},e.isResolvedFromDuration=!0}return e}function $l(n=Qe.visualDuration,e=Qe.bounce){const s=typeof n!="object"?{visualDuration:n,keyframes:[0,1],bounce:e}:n;let{restSpeed:a,restDelta:l}=s;const u=s.keyframes[0],c=s.keyframes[s.keyframes.length-1],d={done:!1,value:u},{stiffness:m,damping:p,mass:g,duration:v,velocity:w,isResolvedFromDuration:x}=B1({...s,velocity:-on(s.velocity||0)}),S=w||0,A=p/(2*Math.sqrt(m*g)),k=c-u,C=on(Math.sqrt(m/g)),N=Math.abs(k)<5;a||(a=N?Qe.restSpeed.granular:Qe.restSpeed.default),l||(l=N?Qe.restDelta.granular:Qe.restDelta.default);let O,P,J,X,G,Z;if(A<1)J=rd(C,A),X=(S+A*C*k)/J,O=ne=>{const le=Math.exp(-A*C*ne);return c-le*(X*Math.sin(J*ne)+k*Math.cos(J*ne))},G=A*C*X+k*J,Z=A*C*k-X*J,P=ne=>Math.exp(-A*C*ne)*(G*Math.sin(J*ne)+Z*Math.cos(J*ne));else if(A===1){O=le=>c-Math.exp(-C*le)*(k+(S+C*k)*le);const ne=S+C*k;P=le=>Math.exp(-C*le)*(C*ne*le-S)}else{const ne=C*Math.sqrt(A*A-1);O=Re=>{const ke=Math.exp(-A*C*Re),L=Math.min(ne*Re,300);return c-ke*((S+A*C*k)*Math.sinh(L)+ne*k*Math.cosh(L))/ne};const le=(S+A*C*k)/ne,de=A*C*le-k*ne,je=A*C*k-le*ne;P=Re=>{const ke=Math.exp(-A*C*Re),L=Math.min(ne*Re,300);return ke*(de*Math.sinh(L)+je*Math.cosh(L))}}const ee={calculatedDuration:x&&v||null,velocity:ne=>Ft(P(ne)),next:ne=>{if(!x&&A<1){const de=Math.exp(-A*C*ne),je=Math.sin(J*ne),Re=Math.cos(J*ne),ke=c-de*(X*je+k*Re),L=Ft(de*(G*je+Z*Re));return d.done=Math.abs(L)<=a&&Math.abs(c-ke)<=l,d.value=d.done?c:ke,d}const le=O(ne);if(x)d.done=ne>=v;else{const de=Ft(P(ne));d.done=Math.abs(de)<=a&&Math.abs(c-le)<=l}return d.value=d.done?c:le,d},toString:()=>{const ne=Math.min(nf(ee),Hl),le=Tb(de=>ee.next(ne*de).value,ne,30);return ne+"ms "+le},toTransition:()=>{}};return ee}$l.applyToOptions=n=>{const e=j1(n,100,$l);return n.ease=e.ease,n.duration=Ft(e.duration),n.type="keyframes",n};const L1=5;function Eb(n,e,s){const a=Math.max(e-L1,0);return sb(s-n(a),e-a)}function od({keyframes:n,velocity:e=0,power:s=.8,timeConstant:a=325,bounceDamping:l=10,bounceStiffness:u=500,modifyTarget:c,min:d,max:m,restDelta:p=.5,restSpeed:g}){const v=n[0],w={done:!1,value:v},x=Z=>d!==void 0&&Zm,S=Z=>d===void 0?m:m===void 0||Math.abs(d-Z)-A*Math.exp(-Z/a),O=Z=>C+N(Z),P=Z=>{const ee=N(Z),ne=O(Z);w.done=Math.abs(ee)<=p,w.value=w.done?C:ne};let J,X;const G=Z=>{x(w.value)&&(J=Z,X=$l({keyframes:[w.value,S(w.value)],velocity:Eb(O,Z,w.value),damping:l,stiffness:u,restDelta:p,restSpeed:g}))};return G(0),{calculatedDuration:null,next:Z=>{let ee=!1;return!X&&J===void 0&&(ee=!0,P(Z),G(Z)),J!==void 0&&Z>=J?X.next(Z-J):(!ee&&P(Z),w)}}}function z1(n,e,s){const a=[],l=s||Li.mix||Sb,u=n.length-1;for(let c=0;ce[0];if(u===2&&e[0]===e[1])return()=>e[1];const c=n[0]===n[1];n[0]>n[u-1]&&(n=[...n].reverse(),e=[...e].reverse());const d=z1(e,a,l),m=d.length,p=g=>{if(c&&g1)for(;vp(Mn(n[0],n[u-1],g)):p}function P1(n,e){const s=n[n.length-1];for(let a=1;a<=e;a++){const l=jr(0,e,a);n.push(Ve(s,1,l))}}function H1(n){const e=[0];return P1(e,n.length-1),e}function $1(n,e){return n.map(s=>s*e)}function q1(n,e){return n.map(()=>e||fb).splice(0,n.length-1)}function Sr({duration:n=300,keyframes:e,times:s,ease:a="easeInOut"}){const l=QS(a)?a.map(By):By(a),u={done:!1,value:e[0]},c=$1(s&&s.length===e.length?s:H1(e),n),d=V1(c,e,{ease:Array.isArray(l)?l:q1(e,l)});return{calculatedDuration:n,next:m=>(u.value=d(m),u.done=m>=n,u)}}const I1=n=>n!==null;function su(n,{repeat:e,repeatType:s="loop"},a,l=1){const u=n.filter(I1),d=l<0||e&&s!=="loop"&&e%2===1?0:u.length-1;return!d||a===void 0?u[d]:a}const G1={decay:od,inertia:od,tween:Sr,keyframes:Sr,spring:$l};function Ab(n){typeof n.type=="string"&&(n.type=G1[n.type])}class sf{constructor(){this.updateFinished()}get finished(){return this._finished}updateFinished(){this._finished=new Promise(e=>{this.resolve=e})}notifyFinished(){this.resolve()}then(e,s){return this.finished.then(e,s)}}const K1=n=>n/100;class ql extends sf{constructor(e){super(),this.state="idle",this.startTime=null,this.isStopped=!1,this.currentTime=0,this.holdTime=null,this.playbackSpeed=1,this.delayState={done:!1,value:void 0},this.stop=()=>{var a,l;const{motionValue:s}=this.options;s&&s.updatedAt!==Tt.now()&&this.tick(Tt.now()),this.isStopped=!0,this.state!=="idle"&&(this.teardown(),(l=(a=this.options).onStop)==null||l.call(a))},this.options=e,this.initAnimation(),this.play(),e.autoplay===!1&&this.pause()}initAnimation(){const{options:e}=this;Ab(e);const{type:s=Sr,repeat:a=0,repeatDelay:l=0,repeatType:u,velocity:c=0}=e;let{keyframes:d}=e;const m=s||Sr;m!==Sr&&typeof d[0]!="number"&&(this.mixKeyframes=qr(K1,Sb(d[0],d[1])),d=[0,100]);const p=m({...e,keyframes:d});u==="mirror"&&(this.mirroredGenerator=m({...e,keyframes:[...d].reverse(),velocity:-c})),p.calculatedDuration===null&&(p.calculatedDuration=nf(p));const{calculatedDuration:g}=p;this.calculatedDuration=g,this.resolvedDuration=g+l,this.totalDuration=this.resolvedDuration*(a+1)-l,this.generator=p}updateTime(e){const s=Math.round(e-this.startTime)*this.playbackSpeed;this.holdTime!==null?this.currentTime=this.holdTime:this.currentTime=s}tick(e,s=!1){const{generator:a,totalDuration:l,mixKeyframes:u,mirroredGenerator:c,resolvedDuration:d,calculatedDuration:m}=this;if(this.startTime===null)return a.next(0);const{delay:p=0,keyframes:g,repeat:v,repeatType:w,repeatDelay:x,type:S,onUpdate:A,finalKeyframe:k}=this.options;this.speed>0?this.startTime=Math.min(this.startTime,e):this.speed<0&&(this.startTime=Math.min(e-l/this.speed,this.startTime)),s?this.currentTime=e:this.updateTime(e);const C=this.currentTime-p*(this.playbackSpeed>=0?1:-1),N=this.playbackSpeed>=0?C<0:C>l;this.currentTime=Math.max(C,0),this.state==="finished"&&this.holdTime===null&&(this.currentTime=l);let O=this.currentTime,P=a;if(v){const Z=Math.min(this.currentTime,l)/d;let ee=Math.floor(Z),ne=Z%1;!ne&&Z>=1&&(ne=1),ne===1&&ee--,ee=Math.min(ee,v+1),!!(ee%2)&&(w==="reverse"?(ne=1-ne,x&&(ne-=x/d)):w==="mirror"&&(P=c)),O=Mn(0,1,ne)*d}let J;N?(this.delayState.value=g[0],J=this.delayState):J=P.next(O),u&&!N&&(J.value=u(J.value));let{done:X}=J;!N&&m!==null&&(X=this.playbackSpeed>=0?this.currentTime>=l:this.currentTime<=0);const G=this.holdTime===null&&(this.state==="finished"||this.state==="running"&&X);return G&&S!==od&&(J.value=su(g,this.options,k,this.speed)),A&&A(J.value),G&&this.finish(),J}then(e,s){return this.finished.then(e,s)}get duration(){return on(this.calculatedDuration)}get iterationDuration(){const{delay:e=0}=this.options||{};return this.duration+on(e)}get time(){return on(this.currentTime)}set time(e){e=Ft(e),this.currentTime=e,this.startTime===null||this.holdTime!==null||this.playbackSpeed===0?this.holdTime=e:this.driver&&(this.startTime=this.driver.now()-e/this.playbackSpeed),this.driver?this.driver.start(!1):(this.startTime=0,this.state="paused",this.holdTime=e,this.tick(e))}getGeneratorVelocity(){const e=this.currentTime;if(e<=0)return this.options.velocity||0;if(this.generator.velocity)return this.generator.velocity(e);const s=this.generator.next(e).value;return Eb(a=>this.generator.next(a).value,e,s)}get speed(){return this.playbackSpeed}set speed(e){const s=this.playbackSpeed!==e;s&&this.driver&&this.updateTime(Tt.now()),this.playbackSpeed=e,s&&this.driver&&(this.time=on(this.currentTime))}play(){var l,u;if(this.isStopped)return;const{driver:e=k1,startTime:s}=this.options;this.driver||(this.driver=e(c=>this.tick(c))),(u=(l=this.options).onPlay)==null||u.call(l);const a=this.driver.now();this.state==="finished"?(this.updateFinished(),this.startTime=a):this.holdTime!==null?this.startTime=a-this.holdTime:this.startTime||(this.startTime=s??a),this.state==="finished"&&this.speed<0&&(this.startTime+=this.calculatedDuration),this.holdTime=null,this.state="running",this.driver.start()}pause(){this.state="paused",this.updateTime(Tt.now()),this.holdTime=this.currentTime}complete(){this.state!=="running"&&this.play(),this.state="finished",this.holdTime=null}finish(){var e,s;this.notifyFinished(),this.teardown(),this.state="finished",(s=(e=this.options).onComplete)==null||s.call(e)}cancel(){var e,s;this.holdTime=null,this.startTime=0,this.tick(0),this.teardown(),(s=(e=this.options).onCancel)==null||s.call(e)}teardown(){this.state="idle",this.stopDriver(),this.startTime=this.holdTime=null}stopDriver(){this.driver&&(this.driver.stop(),this.driver=void 0)}sample(e){return this.startTime=0,this.tick(e,!0)}attachTimeline(e){var s;return this.options.allowFlatten&&(this.options.type="keyframes",this.options.ease="linear",this.initAnimation()),(s=this.driver)==null||s.stop(),e.observe(this)}}function F1(n){for(let e=1;en*180/Math.PI,ld=n=>{const e=ds(Math.atan2(n[1],n[0]));return ud(e)},Y1={x:4,y:5,translateX:4,translateY:5,scaleX:0,scaleY:3,scale:n=>(Math.abs(n[0])+Math.abs(n[3]))/2,rotate:ld,rotateZ:ld,skewX:n=>ds(Math.atan(n[1])),skewY:n=>ds(Math.atan(n[2])),skew:n=>(Math.abs(n[1])+Math.abs(n[2]))/2},ud=n=>(n=n%360,n<0&&(n+=360),n),qy=ld,Iy=n=>Math.sqrt(n[0]*n[0]+n[1]*n[1]),Gy=n=>Math.sqrt(n[4]*n[4]+n[5]*n[5]),X1={x:12,y:13,z:14,translateX:12,translateY:13,translateZ:14,scaleX:Iy,scaleY:Gy,scale:n=>(Iy(n)+Gy(n))/2,rotateX:n=>ud(ds(Math.atan2(n[6],n[5]))),rotateY:n=>ud(ds(Math.atan2(-n[2],n[0]))),rotateZ:qy,rotate:qy,skewX:n=>ds(Math.atan(n[4])),skewY:n=>ds(Math.atan(n[1])),skew:n=>(Math.abs(n[1])+Math.abs(n[4]))/2};function cd(n){return n.includes("scale")?1:0}function hd(n,e){if(!n||n==="none")return cd(e);const s=n.match(/^matrix3d\(([-\d.e\s,]+)\)$/u);let a,l;if(s)a=X1,l=s;else{const d=n.match(/^matrix\(([-\d.e\s,]+)\)$/u);a=Y1,l=d}if(!l)return cd(e);const u=a[e],c=l[1].split(",").map(Q1);return typeof u=="function"?u(c):c[u]}const J1=(n,e)=>{const{transform:s="none"}=getComputedStyle(n);return hd(s,e)};function Q1(n){return parseFloat(n.trim())}const va=["transformPerspective","x","y","z","translateX","translateY","translateZ","scale","scaleX","scaleY","rotate","rotateX","rotateY","rotateZ","skew","skewX","skewY"],ba=new Set([...va,"pathRotation"]),Ky=n=>n===ya||n===se,Z1=new Set(["x","y","z"]),W1=va.filter(n=>!Z1.has(n));function eT(n){const e=[];return W1.forEach(s=>{const a=n.getValue(s);a!==void 0&&(e.push([s,a.get()]),a.set(s.startsWith("scale")?1:0))}),e}const Bi={width:({x:n},{paddingLeft:e="0",paddingRight:s="0",boxSizing:a})=>{const l=n.max-n.min;return a==="border-box"?l:l-parseFloat(e)-parseFloat(s)},height:({y:n},{paddingTop:e="0",paddingBottom:s="0",boxSizing:a})=>{const l=n.max-n.min;return a==="border-box"?l:l-parseFloat(e)-parseFloat(s)},top:(n,{top:e})=>parseFloat(e),left:(n,{left:e})=>parseFloat(e),bottom:({y:n},{top:e})=>parseFloat(e)+(n.max-n.min),right:({x:n},{left:e})=>parseFloat(e)+(n.max-n.min),x:(n,{transform:e})=>hd(e,"x"),y:(n,{transform:e})=>hd(e,"y")};Bi.translateX=Bi.x;Bi.translateY=Bi.y;const ms=new Set;let dd=!1,fd=!1,md=!1;function Rb(){if(fd){const n=Array.from(ms).filter(a=>a.needsMeasurement),e=new Set(n.map(a=>a.element)),s=new Map;e.forEach(a=>{const l=eT(a);l.length&&(s.set(a,l),a.render())}),n.forEach(a=>a.measureInitialState()),e.forEach(a=>{a.render();const l=s.get(a);l&&l.forEach(([u,c])=>{var d;(d=a.getValue(u))==null||d.set(c)})}),n.forEach(a=>a.measureEndState()),n.forEach(a=>{a.suspendedScrollY!==void 0&&window.scrollTo(0,a.suspendedScrollY)})}fd=!1,dd=!1,ms.forEach(n=>n.complete(md)),ms.clear()}function Cb(){ms.forEach(n=>{n.readKeyframes(),n.needsMeasurement&&(fd=!0)})}function tT(){md=!0,Cb(),Rb(),md=!1}class af{constructor(e,s,a,l,u,c=!1){this.state="pending",this.isAsync=!1,this.needsMeasurement=!1,this.unresolvedKeyframes=[...e],this.onComplete=s,this.name=a,this.motionValue=l,this.element=u,this.isAsync=c}scheduleResolve(){this.state="scheduled",this.isAsync?(ms.add(this),dd||(dd=!0,Pe.read(Cb),Pe.resolveKeyframes(Rb))):(this.readKeyframes(),this.complete())}readKeyframes(){const{unresolvedKeyframes:e,name:s,element:a,motionValue:l}=this;if(e[0]===null){const u=l==null?void 0:l.get(),c=e[e.length-1];if(u!==void 0)e[0]=u;else if(a&&s){const d=a.readValue(s,c);d!=null&&(e[0]=d)}e[0]===void 0&&(e[0]=c),l&&u===void 0&&l.set(e[0])}F1(e)}setFinalKeyframe(){}measureInitialState(){}renderEndStyles(){}measureEndState(){}complete(e=!1){this.state="complete",this.onComplete(this.unresolvedKeyframes,this.finalKeyframe,e),ms.delete(this)}cancel(){this.state==="scheduled"&&(ms.delete(this),this.state="pending")}resume(){this.state==="pending"&&this.scheduleResolve()}}const nT=n=>n.startsWith("--");function kb(n,e,s){nT(e)?n.style.setProperty(e,s):n.style[e]=s}const iT={};function jb(n,e){const s=ib(n);return()=>iT[e]??s()}const sT=jb(()=>window.ScrollTimeline!==void 0,"scrollTimeline"),Ob=jb(()=>{try{document.createElement("div").animate({opacity:0},{easing:"linear(0, 1)"})}catch{return!1}return!0},"linearEasing"),yr=([n,e,s,a])=>`cubic-bezier(${n}, ${e}, ${s}, ${a})`,Fy={linear:"linear",ease:"ease",easeIn:"ease-in",easeOut:"ease-out",easeInOut:"ease-in-out",circIn:yr([0,.65,.55,1]),circOut:yr([.55,0,1,.45]),backIn:yr([.31,.01,.66,-.59]),backOut:yr([.33,1.53,.69,.99])};function Nb(n,e){if(n)return typeof n=="function"?Ob()?Tb(n,e):"ease-out":mb(n)?yr(n):Array.isArray(n)?n.map(s=>Nb(s,e)||Fy.easeOut):Fy[n]}function aT(n,e,s,{delay:a=0,duration:l=300,repeat:u=0,repeatType:c="loop",ease:d="easeOut",times:m}={},p=void 0){const g={[e]:s};m&&(g.offset=m);const v=Nb(d,l);Array.isArray(v)&&(g.easing=v);const w={delay:a,duration:l,easing:Array.isArray(v)?"linear":v,fill:"both",iterations:u+1,direction:c==="reverse"?"alternate":"normal"};return p&&(w.pseudoElement=p),n.animate(g,w)}function Db(n){return typeof n=="function"&&"applyToOptions"in n}function rT({type:n,...e}){return Db(n)&&Ob()?n.applyToOptions(e):(e.duration??(e.duration=300),e.ease??(e.ease="easeOut"),e)}class Mb extends sf{constructor(e){if(super(),this.finishedTime=null,this.isStopped=!1,this.manualStartTime=null,!e)return;const{element:s,name:a,keyframes:l,pseudoElement:u,allowFlatten:c=!1,finalKeyframe:d,onComplete:m}=e;this.isPseudoElement=!!u,this.allowFlatten=c,this.options=e,Yd(typeof e.type!="string");const p=rT(e);this.animation=aT(s,a,l,p,u),p.autoplay===!1&&this.animation.pause(),this.animation.onfinish=()=>{if(this.finishedTime=this.time,!u){const g=su(l,this.options,d,this.speed);this.updateMotionValue&&this.updateMotionValue(g),kb(s,a,g),this.animation.cancel()}m==null||m(),this.notifyFinished()}}play(){this.isStopped||(this.manualStartTime=null,this.animation.play(),this.state==="finished"&&this.updateFinished())}pause(){this.animation.pause()}complete(){var e,s;(s=(e=this.animation).finish)==null||s.call(e)}cancel(){try{this.animation.cancel()}catch{}}stop(){if(this.isStopped)return;this.isStopped=!0;const{state:e}=this;e==="idle"||e==="finished"||(this.updateMotionValue?this.updateMotionValue():this.commitStyles(),this.isPseudoElement||this.cancel())}commitStyles(){var s,a,l;const e=(s=this.options)==null?void 0:s.element;!this.isPseudoElement&&(e!=null&&e.isConnected)&&((l=(a=this.animation).commitStyles)==null||l.call(a))}get duration(){var s,a;const e=((a=(s=this.animation.effect)==null?void 0:s.getComputedTiming)==null?void 0:a.call(s).duration)||0;return on(Number(e))}get iterationDuration(){const{delay:e=0}=this.options||{};return this.duration+on(e)}get time(){return on(Number(this.animation.currentTime)||0)}set time(e){const s=this.finishedTime!==null;this.manualStartTime=null,this.finishedTime=null,this.animation.currentTime=Ft(e),s&&this.animation.pause()}get speed(){return this.animation.playbackRate}set speed(e){e<0&&(this.finishedTime=null),this.animation.playbackRate=e}get state(){return this.finishedTime!==null?"finished":this.animation.playState}get startTime(){return this.manualStartTime??Number(this.animation.startTime)}set startTime(e){this.manualStartTime=this.animation.startTime=e}attachTimeline({timeline:e,rangeStart:s,rangeEnd:a,observe:l}){var u;return this.allowFlatten&&((u=this.animation.effect)==null||u.updateTiming({easing:"linear"})),this.animation.onfinish=null,e&&sT()?(this.animation.timeline=e,s&&(this.animation.rangeStart=s),a&&(this.animation.rangeEnd=a),ln):l(this)}}const Ub={anticipate:cb,backInOut:ub,circInOut:db};function oT(n){return n in Ub}function lT(n){typeof n.ease=="string"&&oT(n.ease)&&(n.ease=Ub[n.ease])}const Bh=10;class uT extends Mb{constructor(e){lT(e),Ab(e),super(e),e.startTime!==void 0&&e.autoplay!==!1&&(this.startTime=e.startTime),this.options=e}updateMotionValue(e){const{motionValue:s,onUpdate:a,onComplete:l,element:u,...c}=this.options;if(!s)return;if(e!==void 0){s.set(e);return}const d=new ql({...c,autoplay:!1}),m=Math.max(Bh,Tt.now()-this.startTime),p=Mn(0,Bh,m-Bh),g=d.sample(m).value,{name:v}=this.options;u&&v&&kb(u,v,g),s.setWithVelocity(d.sample(Math.max(0,m-p)).value,g,p),d.stop()}}const Yy=(n,e)=>e==="zIndex"?!1:!!(typeof n=="number"||Array.isArray(n)||typeof n=="string"&&(wn.test(n)||n==="0")&&!n.startsWith("url("));function cT(n){const e=n[0];if(n.length===1)return!0;for(let s=0;sObject.hasOwnProperty.call(Element.prototype,"animate"));function gT(n){var v;const{motionValue:e,name:s,repeatDelay:a,repeatType:l,damping:u,type:c,keyframes:d}=n;if(!(((v=e==null?void 0:e.owner)==null?void 0:v.current)instanceof HTMLElement))return!1;const{onUpdate:p,transformTemplate:g}=e.owner.getProps();return pT()&&s&&(Bb.has(s)||mT.has(s)&&fT(d))&&(s!=="transform"||!g)&&!p&&!a&&l!=="mirror"&&u!==0&&c!=="inertia"}const yT=40;class vT extends sf{constructor({autoplay:e=!0,delay:s=0,type:a="keyframes",repeat:l=0,repeatDelay:u=0,repeatType:c="loop",keyframes:d,name:m,motionValue:p,element:g,...v}){var S;super(),this.stop=()=>{var A,k;this._animation&&(this._animation.stop(),(A=this.stopTimeline)==null||A.call(this)),(k=this.keyframeResolver)==null||k.cancel()},this.createdAt=Tt.now();const w={autoplay:e,delay:s,type:a,repeat:l,repeatDelay:u,repeatType:c,name:m,motionValue:p,element:g,...v},x=(g==null?void 0:g.KeyframeResolver)||af;this.keyframeResolver=new x(d,(A,k,C)=>this.onKeyframesResolved(A,k,w,!C),m,p,g),(S=this.keyframeResolver)==null||S.scheduleResolve()}onKeyframesResolved(e,s,a,l){var C,N;this.keyframeResolver=void 0;const{name:u,type:c,velocity:d,delay:m,isHandoff:p,onUpdate:g}=a;this.resolvedAt=Tt.now();let v=!0;hT(e,u,c,d)||(v=!1,(Li.instantAnimations||!m)&&(g==null||g(su(e,a,s))),e[0]=e[e.length-1],pd(a),a.repeat=0);const x={startTime:l?this.resolvedAt?this.resolvedAt-this.createdAt>yT?this.resolvedAt:this.createdAt:this.createdAt:void 0,finalKeyframe:s,...a,keyframes:e},S=v&&!p&&gT(x),A=(N=(C=x.motionValue)==null?void 0:C.owner)==null?void 0:N.current;let k;if(S)try{k=new uT({...x,element:A})}catch{k=new ql(x)}else k=new ql(x);k.finished.then(()=>{this.notifyFinished()}).catch(ln),this.pendingTimeline&&(this.stopTimeline=k.attachTimeline(this.pendingTimeline),this.pendingTimeline=void 0),this._animation=k}get finished(){return this._animation?this.animation.finished:this._finished}then(e,s){return this.finished.finally(e).then(()=>{})}get animation(){var e;return this._animation||((e=this.keyframeResolver)==null||e.resume(),tT()),this._animation}get duration(){return this.animation.duration}get iterationDuration(){return this.animation.iterationDuration}get time(){return this.animation.time}set time(e){this.animation.time=e}get speed(){return this.animation.speed}get state(){return this.animation.state}set speed(e){this.animation.speed=e}get startTime(){return this.animation.startTime}attachTimeline(e){return this._animation?this.stopTimeline=this.animation.attachTimeline(e):this.pendingTimeline=e,()=>this.stop()}play(){this.animation.play()}pause(){this.animation.pause()}complete(){this.animation.complete()}cancel(){var e;this._animation&&this.animation.cancel(),(e=this.keyframeResolver)==null||e.cancel()}}function Lb(n,e,s,a=0,l=1){const u=Array.from(n).sort((p,g)=>p.sortNodePosition(g)).indexOf(e),c=n.size,d=(c-1)*a;return typeof s=="function"?s(u,c):l===1?u*a:d-u*a}const Xy=30,bT=n=>!isNaN(parseFloat(n));class wT{constructor(e,s={}){this.canTrackVelocity=null,this.events={},this.updateAndNotify=a=>{var u;const l=Tt.now();if(this.updatedAt!==l&&this.setPrevFrameValue(),this.prev=this.current,this.setCurrent(a),this.current!==this.prev&&((u=this.events.change)==null||u.notify(this.current),this.dependents))for(const c of this.dependents)c.dirty()},this.hasAnimated=!1,this.setCurrent(e),this.owner=s.owner}setCurrent(e){this.current=e,this.updatedAt=Tt.now(),this.canTrackVelocity===null&&e!==void 0&&(this.canTrackVelocity=bT(this.current))}setPrevFrameValue(e=this.current){this.prevFrameValue=e,this.prevUpdatedAt=this.updatedAt}onChange(e){return this.on("change",e)}on(e,s){this.events[e]||(this.events[e]=new Xd);const a=this.events[e].add(s);return e==="change"?()=>{a(),Pe.read(()=>{this.events.change.getSize()||this.stop()})}:a}clearListeners(){for(const e in this.events)this.events[e].clear()}attach(e,s){this.passiveEffect=e,this.stopPassiveEffect=s}set(e){this.passiveEffect?this.passiveEffect(e,this.updateAndNotify):this.updateAndNotify(e)}setWithVelocity(e,s,a){this.set(s),this.prev=void 0,this.prevFrameValue=e,this.prevUpdatedAt=this.updatedAt-a}jump(e,s=!0){this.updateAndNotify(e),this.prev=e,this.prevUpdatedAt=this.prevFrameValue=void 0,s&&this.stop(),this.stopPassiveEffect&&this.stopPassiveEffect()}dirty(){var e;(e=this.events.change)==null||e.notify(this.current)}addDependent(e){this.dependents||(this.dependents=new Set),this.dependents.add(e)}removeDependent(e){this.dependents&&this.dependents.delete(e)}get(){return this.current}getPrevious(){return this.prev}getVelocity(){const e=Tt.now();if(!this.canTrackVelocity||this.prevFrameValue===void 0||e-this.updatedAt>Xy)return 0;const s=Math.min(this.updatedAt-this.prevUpdatedAt,Xy);return sb(parseFloat(this.current)-parseFloat(this.prevFrameValue),s)}start(e){return this.stop(),new Promise(s=>{this.hasAnimated=!0,this.animation=e(s),this.events.animationStart&&this.events.animationStart.notify()}).then(()=>{this.events.animationComplete&&this.events.animationComplete.notify(),this.clearAnimation()})}stop(){this.animation&&(this.animation.stop(),this.events.animationCancel&&this.events.animationCancel.notify()),this.clearAnimation()}isAnimating(){return!!this.animation}clearAnimation(){delete this.animation}destroy(){var e,s;(e=this.dependents)==null||e.clear(),(s=this.events.destroy)==null||s.notify(),this.clearListeners(),this.stop(),this.stopPassiveEffect&&this.stopPassiveEffect()}}function ga(n,e){return new wT(n,e)}function zb(n,e){if(n!=null&&n.inherit&&e){const{inherit:s,...a}=n;return{...e,...a}}return n}function rf(n,e){const s=(n==null?void 0:n[e])??(n==null?void 0:n.default)??n;return s!==n?zb(s,n):s}const _T={type:"spring",stiffness:500,damping:25,restSpeed:10},xT=n=>({type:"spring",stiffness:550,damping:n===0?2*Math.sqrt(550):30,restSpeed:10}),ST={type:"keyframes",duration:.8},TT={type:"keyframes",ease:[.25,.1,.35,1],duration:.3},ET=(n,{keyframes:e})=>e.length>2?ST:ba.has(n)?n.startsWith("scale")?xT(e[1]):_T:TT,AT=new Set(["when","delay","delayChildren","staggerChildren","staggerDirection","repeat","repeatType","repeatDelay","from","elapsed"]);function RT(n){for(const e in n)if(!AT.has(e))return!0;return!1}const of=(n,e,s,a={},l,u)=>c=>{const d=rf(a,n)||{},m=d.delay||a.delay||0;let{elapsed:p=0}=a;p=p-Ft(m);const g={keyframes:Array.isArray(s)?s:[null,s],ease:"easeOut",velocity:e.getVelocity(),...d,delay:-p,onUpdate:w=>{e.set(w),d.onUpdate&&d.onUpdate(w)},onComplete:()=>{c(),d.onComplete&&d.onComplete()},name:n,motionValue:e,element:u?void 0:l};RT(d)||Object.assign(g,ET(n,g)),g.duration&&(g.duration=Ft(g.duration)),g.repeatDelay&&(g.repeatDelay=Ft(g.repeatDelay)),g.from!==void 0&&(g.keyframes[0]=g.from);let v=!1;if((g.type===!1||g.duration===0&&!g.repeatDelay)&&(pd(g),g.delay===0&&(v=!0)),(Li.instantAnimations||Li.skipAnimations||l!=null&&l.shouldSkipAnimations||d.skipAnimations)&&(v=!0,pd(g),g.delay=0),g.allowFlatten=!d.type&&!d.ease,v&&!u&&e.get()!==void 0){const w=su(g.keyframes,d);if(w!==void 0){Pe.update(()=>{g.onUpdate(w),g.onComplete()});return}}return d.isSync?new ql(g):new vT(g)},CT=/^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;function kT(n){const e=CT.exec(n);if(!e)return[,];const[,s,a,l]=e;return[`--${s??a}`,l]}function Vb(n,e,s=1){const[a,l]=kT(n);if(!a)return;const u=window.getComputedStyle(e).getPropertyValue(a);if(u){const c=u.trim();return eb(c)?parseFloat(c):c}return Zd(l)?Vb(l,e,s+1):l}function Jy(n){const e=[{},{}];return n==null||n.values.forEach((s,a)=>{e[0][a]=s.get(),e[1][a]=s.getVelocity()}),e}function lf(n,e,s,a){if(typeof e=="function"){const[l,u]=Jy(a);e=e(s!==void 0?s:n.custom,l,u)}if(typeof e=="string"&&(e=n.variants&&n.variants[e]),typeof e=="function"){const[l,u]=Jy(a);e=e(s!==void 0?s:n.custom,l,u)}return e}function ps(n,e,s){const a=n.getProps();return lf(a,e,s!==void 0?s:a.custom,n)}const Pb=new Set(["width","height","top","left","right","bottom",...va]),gd=n=>Array.isArray(n);function jT(n,e,s){n.hasValue(e)?n.getValue(e).set(s):n.addValue(e,ga(s))}function OT(n){return gd(n)?n[n.length-1]||0:n}function NT(n,e){const s=ps(n,e);let{transitionEnd:a={},transition:l={},...u}=s||{};u={...u,...a};for(const c in u){const d=OT(u[c]);jT(n,c,d)}}const bt=n=>!!(n&&n.getVelocity);function DT(n){return!!(bt(n)&&n.add)}function yd(n,e){const s=n.getValue("willChange");if(DT(s))return s.add(e);if(!s&&Li.WillChange){const a=new Li.WillChange("auto");n.addValue("willChange",a),a.add(e)}}function uf(n){return n.replace(/([A-Z])/g,e=>`-${e.toLowerCase()}`)}const MT="framerAppearId",Hb="data-"+uf(MT);function $b(n){return n.props[Hb]}function UT({protectedKeys:n,needsAnimating:e},s){const a=n.hasOwnProperty(s)&&e[s]!==!0;return e[s]=!1,a}function qb(n,e,{delay:s=0,transitionOverride:a,type:l}={}){let{transition:u,transitionEnd:c,...d}=e;const m=n.getDefaultTransition();u=u?zb(u,m):m;const p=u==null?void 0:u.reduceMotion,g=u==null?void 0:u.skipAnimations;a&&(u=a);const v=[],w=l&&n.animationState&&n.animationState.getState()[l],x=u==null?void 0:u.path;x&&x.animateVisualElement(n,d,u,s,v);for(const S in d){const A=n.getValue(S,n.latestValues[S]??null),k=d[S];if(k===void 0||w&&UT(w,S))continue;const C={delay:s,...rf(u||{},S)};g&&(C.skipAnimations=!0);const N=A.get();if(N!==void 0&&!A.isAnimating()&&!Array.isArray(k)&&k===N&&!C.velocity){Pe.update(()=>A.set(k));continue}let O=!1;if(window.MotionHandoffAnimation){const X=$b(n);if(X){const G=window.MotionHandoffAnimation(X,S,Pe);G!==null&&(C.startTime=G,O=!0)}}yd(n,S);const P=p??n.shouldReduceMotion;A.start(of(S,A,k,P&&Pb.has(S)?{type:!1}:C,n,O));const J=A.animation;J&&v.push(J)}if(c){const S=()=>Pe.update(()=>{c&&NT(n,c)});v.length?Promise.all(v).then(S):S()}return v}function vd(n,e,s={}){var m;const a=ps(n,e,s.type==="exit"?(m=n.presenceContext)==null?void 0:m.custom:void 0);let{transition:l=n.getDefaultTransition()||{}}=a||{};s.transitionOverride&&(l=s.transitionOverride);const u=a?()=>Promise.all(qb(n,a,s)):()=>Promise.resolve(),c=n.variantChildren&&n.variantChildren.size?(p=0)=>{const{delayChildren:g=0,staggerChildren:v,staggerDirection:w}=l;return BT(n,e,p,g,v,w,s)}:()=>Promise.resolve(),{when:d}=l;if(d){const[p,g]=d==="beforeChildren"?[u,c]:[c,u];return p().then(()=>g())}else return Promise.all([u(),c(s.delay)])}function BT(n,e,s=0,a=0,l=0,u=1,c){const d=[];for(const m of n.variantChildren)m.notify("AnimationStart",e),d.push(vd(m,e,{...c,delay:s+(typeof a=="function"?0:a)+Lb(n.variantChildren,m,a,l,u)}).then(()=>m.notify("AnimationComplete",e)));return Promise.all(d)}function LT(n,e,s={}){n.notify("AnimationStart",e);let a;if(Array.isArray(e)){const l=e.map(u=>vd(n,u,s));a=Promise.all(l)}else if(typeof e=="string")a=vd(n,e,s);else{const l=typeof e=="function"?ps(n,e,s.custom):e;a=Promise.all(qb(n,l,s))}return a.then(()=>{n.notify("AnimationComplete",e)})}const zT={test:n=>n==="auto",parse:n=>n},Ib=n=>e=>e.test(n),Gb=[ya,se,Dn,ii,c1,u1,zT],Qy=n=>Gb.find(Ib(n));function VT(n){return typeof n=="number"?n===0:n!==null?n==="none"||n==="0"||nb(n):!0}const PT=new Set(["brightness","contrast","saturate","opacity"]);function HT(n){const[e,s]=n.slice(0,-1).split("(");if(e==="drop-shadow")return n;const[a]=s.match(Wd)||[];if(!a)return n;const l=s.replace(a,"");let u=PT.has(e)?1:0;return a!==s&&(u*=100),e+"("+u+l+")"}const $T=/\b([a-z-]*)\(.*?\)/gu,bd={...wn,getAnimatableNone:n=>{const e=n.match($T);return e?e.map(HT).join(" "):n}},wd={...wn,getAnimatableNone:n=>{const e=wn.parse(n);return wn.createTransformer(n)(e.map(a=>typeof a=="number"?0:typeof a=="object"?{...a,alpha:1}:a))}},Zy={...ya,transform:Math.round},qT={rotate:ii,pathRotation:ii,rotateX:ii,rotateY:ii,rotateZ:ii,scale:pl,scaleX:pl,scaleY:pl,scaleZ:pl,skew:ii,skewX:ii,skewY:ii,distance:se,translateX:se,translateY:se,translateZ:se,x:se,y:se,z:se,perspective:se,transformPerspective:se,opacity:Or,originX:zy,originY:zy,originZ:se},Il={borderWidth:se,borderTopWidth:se,borderRightWidth:se,borderBottomWidth:se,borderLeftWidth:se,borderRadius:se,borderTopLeftRadius:se,borderTopRightRadius:se,borderBottomRightRadius:se,borderBottomLeftRadius:se,width:se,maxWidth:se,height:se,maxHeight:se,top:se,right:se,bottom:se,left:se,inset:se,insetBlock:se,insetBlockStart:se,insetBlockEnd:se,insetInline:se,insetInlineStart:se,insetInlineEnd:se,padding:se,paddingTop:se,paddingRight:se,paddingBottom:se,paddingLeft:se,paddingBlock:se,paddingBlockStart:se,paddingBlockEnd:se,paddingInline:se,paddingInlineStart:se,paddingInlineEnd:se,margin:se,marginTop:se,marginRight:se,marginBottom:se,marginLeft:se,marginBlock:se,marginBlockStart:se,marginBlockEnd:se,marginInline:se,marginInlineStart:se,marginInlineEnd:se,fontSize:se,backgroundPositionX:se,backgroundPositionY:se,...qT,zIndex:Zy,fillOpacity:Or,strokeOpacity:Or,numOctaves:Zy},IT={...Il,color:rt,backgroundColor:rt,outlineColor:rt,fill:rt,stroke:rt,borderColor:rt,borderTopColor:rt,borderRightColor:rt,borderBottomColor:rt,borderLeftColor:rt,filter:bd,WebkitFilter:bd,mask:wd,WebkitMask:wd},Kb=n=>IT[n],GT=new Set([bd,wd]);function Fb(n,e){let s=Kb(n);return GT.has(s)||(s=wn),s.getAnimatableNone?s.getAnimatableNone(e):void 0}const KT=new Set(["auto","none","0"]);function FT(n,e,s){let a=0,l;for(;a{e.getValue(m).set(p)}),this.resolveNoneKeyframes()}}const cf=["borderTopLeftRadius","borderTopRightRadius","borderBottomRightRadius","borderBottomLeftRadius"];function Yb(n,e,s){if(n==null)return[];if(n instanceof EventTarget)return[n];if(typeof n=="string"){let a=document;const l=(s==null?void 0:s[n])??a.querySelectorAll(n);return l?Array.from(l):[]}return Array.from(n).filter(a=>a!=null)}const _d=(n,e)=>e&&typeof n=="number"?e.transform(n):n;function jl(n){return tb(n)&&"offsetHeight"in n&&!("ownerSVGElement"in n)}const{schedule:hf}=pb(queueMicrotask,!1),mn={x:!1,y:!1};function Xb(){return mn.x||mn.y}function XT(n){return n==="x"||n==="y"?mn[n]?null:(mn[n]=!0,()=>{mn[n]=!1}):mn.x||mn.y?null:(mn.x=mn.y=!0,()=>{mn.x=mn.y=!1})}function Jb(n,e){const s=Yb(n),a=new AbortController,l={passive:!0,...e,signal:a.signal};return[s,l,()=>a.abort()]}function JT(n){return!(n.pointerType==="touch"||Xb())}function QT(n,e,s={}){const[a,l,u]=Jb(n,s);return a.forEach(c=>{let d=!1,m=!1,p;const g=()=>{c.removeEventListener("pointerleave",S)},v=k=>{p&&(p(k),p=void 0),g()},w=k=>{d=!1,window.removeEventListener("pointerup",w),window.removeEventListener("pointercancel",w),m&&(m=!1,v(k))},x=()=>{d=!0,window.addEventListener("pointerup",w,l),window.addEventListener("pointercancel",w,l)},S=k=>{if(k.pointerType!=="touch"){if(d){m=!0;return}v(k)}},A=k=>{if(!JT(k))return;m=!1;const C=e(c,k);typeof C=="function"&&(p=C,c.addEventListener("pointerleave",S,l))};c.addEventListener("pointerenter",A,l),c.addEventListener("pointerdown",x,l)}),u}const Qb=(n,e)=>e?n===e?!0:Qb(n,e.parentElement):!1,df=n=>n.pointerType==="mouse"?typeof n.button!="number"||n.button<=0:n.isPrimary!==!1,ZT=new Set(["BUTTON","INPUT","SELECT","TEXTAREA","A"]);function WT(n){return ZT.has(n.tagName)||n.isContentEditable===!0}const eE=new Set(["INPUT","SELECT","TEXTAREA"]);function tE(n){return eE.has(n.tagName)||n.isContentEditable===!0}const Ol=new WeakSet;function Wy(n){return e=>{e.key==="Enter"&&n(e)}}function Lh(n,e){n.dispatchEvent(new PointerEvent("pointer"+e,{isPrimary:!0,bubbles:!0}))}const nE=(n,e)=>{const s=n.currentTarget;if(!s)return;const a=Wy(()=>{if(Ol.has(s))return;Lh(s,"down");const l=Wy(()=>{Lh(s,"up")}),u=()=>Lh(s,"cancel");s.addEventListener("keyup",l,e),s.addEventListener("blur",u,e)});s.addEventListener("keydown",a,e),s.addEventListener("blur",()=>s.removeEventListener("keydown",a),e)};function e0(n){return df(n)&&!Xb()}const t0=new WeakSet;function iE(n,e,s={}){const[a,l,u]=Jb(n,s),c=d=>{const m=d.currentTarget;if(!e0(d)||t0.has(d))return;Ol.add(m),s.stopPropagation&&t0.add(d);const p=e(m,d),g={...l,capture:!0},v=(S,A)=>{window.removeEventListener("pointerup",w,g),window.removeEventListener("pointercancel",x,g),Ol.has(m)&&Ol.delete(m),e0(S)&&typeof p=="function"&&p(S,{success:A})},w=S=>{v(S,m===window||m===document||s.useGlobalTarget||Qb(m,S.target))},x=S=>{v(S,!1)};window.addEventListener("pointerup",w,g),window.addEventListener("pointercancel",x,g)};return a.forEach(d=>{(s.useGlobalTarget?window:d).addEventListener("pointerdown",c,l),jl(d)&&(d.addEventListener("focus",p=>nE(p,l)),!WT(d)&&!d.hasAttribute("tabindex")&&(d.tabIndex=0))}),u}function ff(n){return tb(n)&&"ownerSVGElement"in n}const Nl=new WeakMap;let Di;const Zb=(n,e,s)=>(a,l)=>l&&l[0]?l[0][n+"Size"]:ff(a)&&"getBBox"in a?a.getBBox()[e]:a[s],sE=Zb("inline","width","offsetWidth"),aE=Zb("block","height","offsetHeight");function rE({target:n,borderBoxSize:e}){var s;(s=Nl.get(n))==null||s.forEach(a=>{a(n,{get width(){return sE(n,e)},get height(){return aE(n,e)}})})}function oE(n){n.forEach(rE)}function lE(){typeof ResizeObserver>"u"||(Di=new ResizeObserver(oE))}function uE(n,e){Di||lE();const s=Yb(n);return s.forEach(a=>{let l=Nl.get(a);l||(l=new Set,Nl.set(a,l)),l.add(e),Di==null||Di.observe(a)}),()=>{s.forEach(a=>{const l=Nl.get(a);l==null||l.delete(e),l!=null&&l.size||Di==null||Di.unobserve(a)})}}const Dl=new Set;let ca;function cE(){ca=()=>{const n={get width(){return window.innerWidth},get height(){return window.innerHeight}};Dl.forEach(e=>e(n))},window.addEventListener("resize",ca)}function hE(n){return Dl.add(n),ca||cE(),()=>{Dl.delete(n),!Dl.size&&typeof ca=="function"&&(window.removeEventListener("resize",ca),ca=void 0)}}function n0(n,e){return typeof n=="function"?hE(n):uE(n,e)}function dE(n){return ff(n)&&n.tagName==="svg"}const fE=[...Gb,rt,wn],mE=n=>fE.find(Ib(n)),i0=()=>({translate:0,scale:1,origin:0,originPoint:0}),ha=()=>({x:i0(),y:i0()}),s0=()=>({min:0,max:0}),lt=()=>({x:s0(),y:s0()}),pE=new WeakMap;function au(n){return n!==null&&typeof n=="object"&&typeof n.start=="function"}function Nr(n){return typeof n=="string"||Array.isArray(n)}const mf=["animate","whileInView","whileFocus","whileHover","whileTap","whileDrag","exit"],pf=["initial",...mf];function ru(n){return au(n.animate)||pf.some(e=>Nr(n[e]))}function Wb(n){return!!(ru(n)||n.variants)}function gE(n,e,s){for(const a in e){const l=e[a],u=s[a];if(bt(l))n.addValue(a,l);else if(bt(u))n.addValue(a,ga(l,{owner:n}));else if(u!==l)if(n.hasValue(a)){const c=n.getValue(a);c.liveStyle===!0?c.jump(l):c.hasAnimated||c.set(l)}else{const c=n.getStaticValue(a);n.addValue(a,ga(c!==void 0?c:l,{owner:n}))}}for(const a in s)e[a]===void 0&&n.removeValue(a);return e}const xd={current:null},ew={current:!1},yE=typeof window<"u";function vE(){if(ew.current=!0,!!yE)if(window.matchMedia){const n=window.matchMedia("(prefers-reduced-motion)"),e=()=>xd.current=n.matches;n.addEventListener("change",e),e()}else xd.current=!1}const a0=["AnimationStart","AnimationComplete","Update","BeforeLayoutMeasure","LayoutMeasure","LayoutAnimationStart","LayoutAnimationComplete"];let Gl={};function tw(n){Gl=n}function bE(){return Gl}class wE{scrapeMotionValuesFromProps(e,s,a){return{}}constructor({parent:e,props:s,presenceContext:a,reducedMotionConfig:l,skipAnimations:u,blockInitialAnimation:c,visualState:d},m={}){this.current=null,this.children=new Set,this.isVariantNode=!1,this.isControllingVariants=!1,this.shouldReduceMotion=null,this.shouldSkipAnimations=!1,this.values=new Map,this.KeyframeResolver=af,this.features={},this.valueSubscriptions=new Map,this.prevMotionValues={},this.hasBeenMounted=!1,this.events={},this.propEventSubscriptions={},this.notifyUpdate=()=>this.notify("Update",this.latestValues),this.render=()=>{this.current&&(this.triggerBuild(),this.renderInstance(this.current,this.renderState,this.props.style,this.projection))},this.renderScheduledAt=0,this.scheduleRender=()=>{const x=Tt.now();this.renderScheduledAtthis.bindToMotionValue(u,l)),this.reducedMotionConfig==="never"?this.shouldReduceMotion=!1:this.reducedMotionConfig==="always"?this.shouldReduceMotion=!0:(ew.current||vE(),this.shouldReduceMotion=xd.current),this.shouldSkipAnimations=this.skipAnimationsConfig??!1,(a=this.parent)==null||a.addChild(this),this.update(this.props,this.presenceContext),this.hasBeenMounted=!0}unmount(){var e;this.projection&&this.projection.unmount(),zi(this.notifyUpdate),zi(this.render),this.valueSubscriptions.forEach(s=>s()),this.valueSubscriptions.clear(),this.removeFromVariantTree&&this.removeFromVariantTree(),(e=this.parent)==null||e.removeChild(this);for(const s in this.events)this.events[s].clear();for(const s in this.features){const a=this.features[s];a&&(a.unmount(),a.isMounted=!1)}this.current=null}addChild(e){this.children.add(e),this.enteringChildren??(this.enteringChildren=new Set),this.enteringChildren.add(e)}removeChild(e){this.children.delete(e),this.enteringChildren&&this.enteringChildren.delete(e)}bindToMotionValue(e,s){if(this.valueSubscriptions.has(e)&&this.valueSubscriptions.get(e)(),s.accelerate&&Bb.has(e)&&this.current instanceof HTMLElement){const{factory:c,keyframes:d,times:m,ease:p,duration:g}=s.accelerate,v=new Mb({element:this.current,name:e,keyframes:d,times:m,ease:p,duration:Ft(g)}),w=c(v);this.valueSubscriptions.set(e,()=>{w(),v.cancel()});return}const a=ba.has(e);a&&this.onBindTransform&&this.onBindTransform();const l=s.on("change",c=>{this.latestValues[e]=c,this.props.onUpdate&&Pe.preRender(this.notifyUpdate),a&&this.projection&&(this.projection.isTransformDirty=!0),this.scheduleRender()});let u;typeof window<"u"&&window.MotionCheckAppearSync&&(u=window.MotionCheckAppearSync(this,e,s)),this.valueSubscriptions.set(e,()=>{l(),u&&u()})}sortNodePosition(e){return!this.current||!this.sortInstanceNodePosition||this.type!==e.type?0:this.sortInstanceNodePosition(this.current,e.current)}updateFeatures(){let e="animation";for(e in Gl){const s=Gl[e];if(!s)continue;const{isEnabled:a,Feature:l}=s;if(!this.features[e]&&l&&a(this.props)&&(this.features[e]=new l(this)),this.features[e]){const u=this.features[e];u.isMounted?u.update():(u.mount(),u.isMounted=!0)}}}triggerBuild(){this.build(this.renderState,this.latestValues,this.props)}measureViewportBox(){return this.current?this.measureInstanceViewportBox(this.current,this.props):lt()}getStaticValue(e){return this.latestValues[e]}setStaticValue(e,s){this.latestValues[e]=s}update(e,s){(e.transformTemplate||this.props.transformTemplate)&&this.scheduleRender(),this.prevProps=this.props,this.props=e,this.prevPresenceContext=this.presenceContext,this.presenceContext=s;for(let a=0;as.variantChildren.delete(e)}addValue(e,s){const a=this.values.get(e);s!==a&&(a&&this.removeValue(e),this.bindToMotionValue(e,s),this.values.set(e,s),this.latestValues[e]=s.get())}removeValue(e){this.values.delete(e);const s=this.valueSubscriptions.get(e);s&&(s(),this.valueSubscriptions.delete(e)),delete this.latestValues[e],this.removeValueFromRenderState(e,this.renderState)}hasValue(e){return this.values.has(e)}getValue(e,s){if(this.props.values&&this.props.values[e])return this.props.values[e];let a=this.values.get(e);return a===void 0&&s!==void 0&&(a=ga(s===null?void 0:s,{owner:this}),this.addValue(e,a)),a}readValue(e,s){let a=this.latestValues[e]!==void 0||!this.current?this.latestValues[e]:this.getBaseTargetFromProps(this.props,e)??this.readValueFromInstance(this.current,e,this.options);return a!=null&&(typeof a=="string"&&(eb(a)||nb(a))?a=parseFloat(a):!mE(a)&&wn.test(s)&&(a=Fb(e,s)),this.setBaseTarget(e,bt(a)?a.get():a)),bt(a)?a.get():a}setBaseTarget(e,s){this.baseTarget[e]=s}getBaseTarget(e){var u;const{initial:s}=this.props;let a;if(typeof s=="string"||typeof s=="object"){const c=lf(this.props,s,(u=this.presenceContext)==null?void 0:u.custom);c&&(a=c[e])}if(s&&a!==void 0)return a;const l=this.getBaseTargetFromProps(this.props,e);return l!==void 0&&!bt(l)?l:this.initialValues[e]!==void 0&&a===void 0?void 0:this.baseTarget[e]}on(e,s){return this.events[e]||(this.events[e]=new Xd),this.events[e].add(s)}notify(e,...s){this.events[e]&&this.events[e].notify(...s)}scheduleRenderMicrotask(){hf.render(this.render)}}class nw extends wE{constructor(){super(...arguments),this.KeyframeResolver=YT}sortInstanceNodePosition(e,s){return e.compareDocumentPosition(s)&2?1:-1}getBaseTargetFromProps(e,s){const a=e.style;return a?a[s]:void 0}removeValueFromRenderState(e,{vars:s,style:a}){delete s[e],delete a[e]}handleChildMotionValue(){this.childSubscription&&(this.childSubscription(),delete this.childSubscription);const{children:e}=this.props;bt(e)&&(this.childSubscription=e.on("change",s=>{this.current&&(this.current.textContent=`${s}`)}))}}class Vi{constructor(e){this.isMounted=!1,this.node=e}update(){}}function iw({top:n,left:e,right:s,bottom:a}){return{x:{min:e,max:s},y:{min:n,max:a}}}function _E({x:n,y:e}){return{top:e.min,right:n.max,bottom:e.max,left:n.min}}function xE(n,e){if(!e)return n;const s=e({x:n.left,y:n.top}),a=e({x:n.right,y:n.bottom});return{top:s.y,left:s.x,bottom:a.y,right:a.x}}function zh(n){return n===void 0||n===1}function Sd({scale:n,scaleX:e,scaleY:s}){return!zh(n)||!zh(e)||!zh(s)}function ls(n){return Sd(n)||sw(n)||n.z||n.rotate||n.rotateX||n.rotateY||n.skewX||n.skewY}function sw(n){return r0(n.x)||r0(n.y)}function r0(n){return n&&n!=="0%"}function Kl(n,e,s){const a=n-s,l=e*a;return s+l}function o0(n,e,s,a,l){return l!==void 0&&(n=Kl(n,l,a)),Kl(n,s,a)+e}function Td(n,e=0,s=1,a,l){n.min=o0(n.min,e,s,a,l),n.max=o0(n.max,e,s,a,l)}function aw(n,{x:e,y:s}){Td(n.x,e.translate,e.scale,e.originPoint),Td(n.y,s.translate,s.scale,s.originPoint)}const l0=.999999999999,u0=1.0000000000001;function SE(n,e,s,a=!1){var d;const l=s.length;if(!l)return;e.x=e.y=1;let u,c;for(let m=0;ml0&&(e.x=1),e.yl0&&(e.y=1)}function On(n,e){n.min+=e,n.max+=e}function c0(n,e,s,a,l=.5){const u=Ve(n.min,n.max,l);Td(n,e,s,u,a)}function h0(n,e){return typeof n=="string"?parseFloat(n)/100*(e.max-e.min):n}function Ml(n,e,s){const a=s??n;c0(n.x,h0(e.x,a.x),e.scaleX,e.scale,e.originX),c0(n.y,h0(e.y,a.y),e.scaleY,e.scale,e.originY)}function rw(n,e){return iw(xE(n.getBoundingClientRect(),e))}function TE(n,e,s){const a=rw(n,s),{scroll:l}=e;return l&&(On(a.x,l.offset.x),On(a.y,l.offset.y)),a}const EE={x:"translateX",y:"translateY",z:"translateZ",transformPerspective:"perspective"},AE=va.length;function RE(n,e,s){let a="",l=!0;for(let c=0;c{if(!e.target)return n;if(typeof n=="string")if(se.test(n))n=parseFloat(n);else return n;const s=d0(n,e.target.x),a=d0(n,e.target.y);return`${s}% ${a}%`}},CE={correct:(n,{treeScale:e,projectionDelta:s})=>{const a=n,l=wn.parse(n);if(l.length>5)return a;const u=wn.createTransformer(n),c=typeof l[0]!="number"?1:0,d=s.x.scale*e.x,m=s.y.scale*e.y;l[0+c]/=d,l[1+c]/=m;const p=Ve(d,m,.5);return typeof l[2+c]=="number"&&(l[2+c]/=p),typeof l[3+c]=="number"&&(l[3+c]/=p),u(l)}},Ed={borderRadius:{...pr,applyTo:[...cf]},borderTopLeftRadius:pr,borderTopRightRadius:pr,borderBottomLeftRadius:pr,borderBottomRightRadius:pr,boxShadow:CE};function lw(n,{layout:e,layoutId:s}){return ba.has(n)||n.startsWith("origin")||(e||s!==void 0)&&(!!Ed[n]||n==="opacity")}function yf(n,e,s){var c;const a=n.style,l=e==null?void 0:e.style,u={};if(!a)return u;for(const d in a)(bt(a[d])||l&&bt(l[d])||lw(d,n)||((c=s==null?void 0:s.getValue(d))==null?void 0:c.liveStyle)!==void 0)&&(u[d]=a[d]);return u}function kE(n){return window.getComputedStyle(n)}class jE extends nw{constructor(){super(...arguments),this.type="html",this.renderInstance=ow}readValueFromInstance(e,s){var a;if(ba.has(s))return(a=this.projection)!=null&&a.isProjecting?cd(s):J1(e,s);{const l=kE(e),u=(yb(s)?l.getPropertyValue(s):l[s])||0;return typeof u=="string"?u.trim():u}}measureInstanceViewportBox(e,{transformPagePoint:s}){return rw(e,s)}build(e,s,a){gf(e,s,a.transformTemplate)}scrapeMotionValuesFromProps(e,s,a){return yf(e,s,a)}}const OE={offset:"stroke-dashoffset",array:"stroke-dasharray"},NE={offset:"strokeDashoffset",array:"strokeDasharray"};function DE(n,e,s=1,a=0,l=!0){n.pathLength=1;const u=l?OE:NE;n[u.offset]=`${-a}`,n[u.array]=`${e} ${s}`}const ME=["offsetDistance","offsetPath","offsetRotate","offsetAnchor"];function uw(n,{attrX:e,attrY:s,attrScale:a,pathLength:l,pathSpacing:u=1,pathOffset:c=0,...d},m,p,g){if(gf(n,d,p),m){n.style.viewBox&&(n.attrs.viewBox=n.style.viewBox);return}n.attrs=n.style,n.style={};const{attrs:v,style:w}=n;v.transform&&(w.transform=v.transform,delete v.transform),(w.transform||v.transformOrigin)&&(w.transformOrigin=v.transformOrigin??"50% 50%",delete v.transformOrigin),w.transform&&(w.transformBox=(g==null?void 0:g.transformBox)??"fill-box",delete v.transformBox);for(const x of ME)v[x]!==void 0&&(w[x]=v[x],delete v[x]);e!==void 0&&(v.x=e),s!==void 0&&(v.y=s),a!==void 0&&(v.scale=a),l!==void 0&&DE(v,l,u,c,!1)}const cw=new Set(["baseFrequency","diffuseConstant","kernelMatrix","kernelUnitLength","keySplines","keyTimes","limitingConeAngle","markerHeight","markerWidth","numOctaves","targetX","targetY","surfaceScale","specularConstant","specularExponent","stdDeviation","tableValues","viewBox","gradientTransform","pathLength","startOffset","textLength","lengthAdjust"]),hw=n=>typeof n=="string"&&n.toLowerCase()==="svg";function UE(n,e,s,a){ow(n,e,void 0,a);for(const l in e.attrs)n.setAttribute(cw.has(l)?l:uf(l),e.attrs[l])}function dw(n,e,s){const a=yf(n,e,s);for(const l in n)if(bt(n[l])||bt(e[l])){const u=va.indexOf(l)!==-1?"attr"+l.charAt(0).toUpperCase()+l.substring(1):l;a[u]=n[l]}return a}class BE extends nw{constructor(){super(...arguments),this.type="svg",this.isSVGTag=!1,this.measureInstanceViewportBox=lt}getBaseTargetFromProps(e,s){return e[s]}readValueFromInstance(e,s){if(ba.has(s)){const a=Kb(s);return a&&a.default||0}return s=cw.has(s)?s:uf(s),e.getAttribute(s)}scrapeMotionValuesFromProps(e,s,a){return dw(e,s,a)}build(e,s,a){uw(e,s,this.isSVGTag,a.transformTemplate,a.style)}renderInstance(e,s,a,l){UE(e,s,a,l)}mount(e){this.isSVGTag=hw(e.tagName),super.mount(e)}}const LE=pf.length;function fw(n){if(!n)return;if(!n.isControllingVariants){const s=n.parent?fw(n.parent)||{}:{};return n.props.initial!==void 0&&(s.initial=n.props.initial),s}const e={};for(let s=0;sPromise.all(e.map(({animation:s,options:a})=>LT(n,s,a)))}function HE(n){let e=PE(n),s=f0(),a=!0,l=!1;const u=p=>(g,v)=>{var x;const w=ps(n,v,p==="exit"?(x=n.presenceContext)==null?void 0:x.custom:void 0);if(w){const{transition:S,transitionEnd:A,...k}=w;g={...g,...k,...A}}return g};function c(p){e=p(n)}function d(p){const{props:g}=n,v=fw(n.parent)||{},w=[],x=new Set;let S={},A=1/0;for(let C=0;CA&&J,ne=!1;const le=Array.isArray(P)?P:[P];let de=le.reduce(u(N),{});X===!1&&(de={});const{prevResolvedValues:je={}}=O,Re={...je,...de},ke=Y=>{ee=!0,x.has(Y)&&(ne=!0,x.delete(Y)),O.needsAnimating[Y]=!0;const I=n.getValue(Y);I&&(I.liveStyle=!1)};for(const Y in Re){const I=de[Y],oe=je[Y];if(S.hasOwnProperty(Y))continue;let E=!1;gd(I)&&gd(oe)?E=!mw(I,oe)||Z:E=I!==oe,E?I!=null?ke(Y):x.add(Y):I!==void 0&&x.has(Y)?ke(Y):O.protectedKeys[Y]=!0}O.prevProp=P,O.prevResolvedValues=de,O.isActive&&(S={...S,...de}),(a||l)&&n.blockInitialAnimation&&(ee=!1);const L=G&&Z;ee&&(!L||ne)&&w.push(...le.map(Y=>{const I={type:N};if(typeof Y=="string"&&(a||l)&&!L&&n.manuallyAnimateOnMount&&n.parent){const{parent:oe}=n,E=ps(oe,Y);if(oe.enteringChildren&&E){const{delayChildren:B}=E.transition||{};I.delay=Lb(oe.enteringChildren,n,B)}}return{animation:Y,options:I}}))}if(x.size){const C={};if(typeof g.initial!="boolean"){const N=ps(n,Array.isArray(g.initial)?g.initial[0]:g.initial);N&&N.transition&&(C.transition=N.transition)}x.forEach(N=>{const O=n.getBaseTarget(N),P=n.getValue(N);P&&(P.liveStyle=!0),C[N]=O??null}),w.push({animation:C})}let k=!!w.length;return a&&(g.initial===!1||g.initial===g.animate)&&!n.manuallyAnimateOnMount&&(k=!1),a=!1,l=!1,k?e(w):Promise.resolve()}function m(p,g){var w;if(s[p].isActive===g)return Promise.resolve();(w=n.variantChildren)==null||w.forEach(x=>{var S;return(S=x.animationState)==null?void 0:S.setActive(p,g)}),s[p].isActive=g;const v=d(p);for(const x in s)s[x].protectedKeys={};return v}return{animateChanges:d,setActive:m,setAnimateFunction:c,getState:()=>s,reset:()=>{s=f0(),l=!0}}}function $E(n,e){return typeof e=="string"?e!==n:Array.isArray(e)?!mw(e,n):!1}function as(n=!1){return{isActive:n,protectedKeys:{},needsAnimating:{},prevResolvedValues:{}}}function f0(){return{animate:as(!0),whileInView:as(),whileHover:as(),whileTap:as(),whileDrag:as(),whileFocus:as(),exit:as()}}function Ad(n,e){n.min=e.min,n.max=e.max}function dn(n,e){Ad(n.x,e.x),Ad(n.y,e.y)}function m0(n,e){n.translate=e.translate,n.scale=e.scale,n.originPoint=e.originPoint,n.origin=e.origin}const pw=1e-4,qE=1-pw,IE=1+pw,gw=.01,GE=0-gw,KE=0+gw;function Et(n){return n.max-n.min}function FE(n,e,s){return Math.abs(n-e)<=s}function p0(n,e,s,a=.5){n.origin=a,n.originPoint=Ve(e.min,e.max,n.origin),n.scale=Et(s)/Et(e),n.translate=Ve(s.min,s.max,n.origin)-n.originPoint,(n.scale>=qE&&n.scale<=IE||isNaN(n.scale))&&(n.scale=1),(n.translate>=GE&&n.translate<=KE||isNaN(n.translate))&&(n.translate=0)}function Tr(n,e,s,a){p0(n.x,e.x,s.x,a?a.originX:void 0),p0(n.y,e.y,s.y,a?a.originY:void 0)}function g0(n,e,s,a=0){const l=a?Ve(s.min,s.max,a):s.min;n.min=l+e.min,n.max=n.min+Et(e)}function YE(n,e,s,a){g0(n.x,e.x,s.x,a==null?void 0:a.x),g0(n.y,e.y,s.y,a==null?void 0:a.y)}function y0(n,e,s,a=0){const l=a?Ve(s.min,s.max,a):s.min;n.min=e.min-l,n.max=n.min+Et(e)}function Fl(n,e,s,a){y0(n.x,e.x,s.x,a==null?void 0:a.x),y0(n.y,e.y,s.y,a==null?void 0:a.y)}function v0(n,e,s,a,l){return n-=e,n=Kl(n,1/s,a),l!==void 0&&(n=Kl(n,1/l,a)),n}function XE(n,e=0,s=1,a=.5,l,u=n,c=n){if(Dn.test(e)&&(e=parseFloat(e),e=Ve(c.min,c.max,e/100)-c.min),typeof e!="number")return;let d=Ve(u.min,u.max,a);n===u&&(d-=e),n.min=v0(n.min,e,s,d,l),n.max=v0(n.max,e,s,d,l)}function b0(n,e,[s,a,l],u,c){XE(n,e[s],e[a],e[l],e.scale,u,c)}const JE=["x","scaleX","originX"],QE=["y","scaleY","originY"];function w0(n,e,s,a){b0(n.x,e,JE,s?s.x:void 0,a?a.x:void 0),b0(n.y,e,QE,s?s.y:void 0,a?a.y:void 0)}function _0(n){return n.translate===0&&n.scale===1}function yw(n){return _0(n.x)&&_0(n.y)}function x0(n,e){return n.min===e.min&&n.max===e.max}function ZE(n,e){return x0(n.x,e.x)&&x0(n.y,e.y)}function S0(n,e){return Math.round(n.min)===Math.round(e.min)&&Math.round(n.max)===Math.round(e.max)}function vw(n,e){return S0(n.x,e.x)&&S0(n.y,e.y)}function T0(n){return Et(n.x)/Et(n.y)}function E0(n,e){return n.translate===e.translate&&n.scale===e.scale&&n.originPoint===e.originPoint}function kn(n){return[n("x"),n("y")]}function WE(n,e,s){let a="";const l=n.x.translate/e.x,u=n.y.translate/e.y,c=(s==null?void 0:s.z)||0;if((l||u||c)&&(a=`translate3d(${l}px, ${u}px, ${c}px) `),(e.x!==1||e.y!==1)&&(a+=`scale(${1/e.x}, ${1/e.y}) `),s){const{transformPerspective:p,rotate:g,pathRotation:v,rotateX:w,rotateY:x,skewX:S,skewY:A}=s;p&&(a=`perspective(${p}px) ${a}`),g&&(a+=`rotate(${g}deg) `),v&&(a+=`rotate(${v}deg) `),w&&(a+=`rotateX(${w}deg) `),x&&(a+=`rotateY(${x}deg) `),S&&(a+=`skewX(${S}deg) `),A&&(a+=`skewY(${A}deg) `)}const d=n.x.scale*e.x,m=n.y.scale*e.y;return(d!==1||m!==1)&&(a+=`scale(${d}, ${m})`),a||"none"}const eA=cf.length,A0=n=>typeof n=="string"?parseFloat(n):n,R0=n=>typeof n=="number"||se.test(n);function tA(n,e,s,a,l,u){l?(n.opacity=Ve(0,s.opacity??1,nA(a)),n.opacityExit=Ve(e.opacity??1,0,iA(a))):u&&(n.opacity=Ve(e.opacity??1,s.opacity??1,a));for(let c=0;cae?1:s(jr(n,e,a))}function sA(n,e,s){const a=bt(n)?n:ga(n);return a.start(of("",a,e,s)),a.animation}function Dr(n,e,s,a={passive:!0}){return n.addEventListener(e,s,a),()=>n.removeEventListener(e,s,a)}const aA=(n,e)=>n.depth-e.depth;class rA{constructor(){this.children=[],this.isDirty=!1}add(e){Fd(this.children,e),this.isDirty=!0}remove(e){Vl(this.children,e),this.isDirty=!0}forEach(e){this.isDirty&&this.children.sort(aA),this.isDirty=!1,this.children.forEach(e)}}function oA(n,e){const s=Tt.now(),a=({timestamp:l})=>{const u=l-s;u>=e&&(zi(a),n(u-e))};return Pe.setup(a,!0),()=>zi(a)}function Ul(n){return bt(n)?n.get():n}class lA{constructor(){this.members=[]}add(e){Fd(this.members,e);for(let s=this.members.length-1;s>=0;s--){const a=this.members[s];if(a===e||a===this.lead||a===this.prevLead)continue;const l=a.instance;(!l||l.isConnected===!1)&&!a.snapshot&&(Vl(this.members,a),a.unmount())}e.scheduleRender()}remove(e){if(Vl(this.members,e),e===this.prevLead&&(this.prevLead=void 0),e===this.lead){const s=this.members[this.members.length-1];s&&this.promote(s)}}relegate(e){var s;for(let a=this.members.indexOf(e)-1;a>=0;a--){const l=this.members[a];if(l.isPresent!==!1&&((s=l.instance)==null?void 0:s.isConnected)!==!1)return this.promote(l),!0}return!1}promote(e,s){var l;const a=this.lead;if(e!==a&&(this.prevLead=a,this.lead=e,e.show(),a)){a.updateSnapshot(),e.scheduleRender();const{layoutDependency:u}=a.options,{layoutDependency:c}=e.options;(u===void 0||u!==c)&&(e.resumeFrom=a,s&&(a.preserveOpacity=!0),a.snapshot&&(e.snapshot=a.snapshot,e.snapshot.latestValues=a.animationValues||a.latestValues),(l=e.root)!=null&&l.isUpdating&&(e.isLayoutDirty=!0)),e.options.crossfade===!1&&a.hide()}}exitAnimationComplete(){this.members.forEach(e=>{var s,a,l,u,c;(a=(s=e.options).onExitComplete)==null||a.call(s),(c=(l=e.resumingFrom)==null?void 0:(u=l.options).onExitComplete)==null||c.call(u)})}scheduleRender(){this.members.forEach(e=>e.instance&&e.scheduleRender(!1))}removeLeadSnapshot(){var e;(e=this.lead)!=null&&e.snapshot&&(this.lead.snapshot=void 0)}}const Bl={hasAnimatedSinceResize:!0,hasEverUpdated:!1},Vh=["","X","Y","Z"],uA=1e3;let cA=0;function Ph(n,e,s,a){const{latestValues:l}=e;l[n]&&(s[n]=l[n],e.setStaticValue(n,0),a&&(a[n]=0))}function ww(n){if(n.hasCheckedOptimisedAppear=!0,n.root===n)return;const{visualElement:e}=n.options;if(!e)return;const s=$b(e);if(window.MotionHasOptimisedAnimation(s,"transform")){const{layout:l,layoutId:u}=n.options;window.MotionCancelOptimisedAnimation(s,"transform",Pe,!(l||u))}const{parent:a}=n;a&&!a.hasCheckedOptimisedAppear&&ww(a)}function _w({attachResizeListener:n,defaultParent:e,measureScroll:s,checkIsScrollRoot:a,resetTransform:l}){return class{constructor(c={},d=e==null?void 0:e()){this.id=cA++,this.animationId=0,this.animationCommitId=0,this.children=new Set,this.options={},this.isTreeAnimating=!1,this.isAnimationBlocked=!1,this.isLayoutDirty=!1,this.isProjectionDirty=!1,this.isSharedProjectionDirty=!1,this.isTransformDirty=!1,this.updateManuallyBlocked=!1,this.updateBlockedByResize=!1,this.isUpdating=!1,this.isSVG=!1,this.needsReset=!1,this.shouldResetTransform=!1,this.hasCheckedOptimisedAppear=!1,this.treeScale={x:1,y:1},this.eventHandlers=new Map,this.hasTreeAnimated=!1,this.layoutVersion=0,this.updateScheduled=!1,this.scheduleUpdate=()=>this.update(),this.projectionUpdateScheduled=!1,this.checkUpdateFailed=()=>{this.isUpdating&&(this.isUpdating=!1,this.clearAllSnapshots())},this.updateProjection=()=>{this.projectionUpdateScheduled=!1,this.nodes.forEach(fA),this.nodes.forEach(bA),this.nodes.forEach(wA),this.nodes.forEach(mA)},this.resolvedRelativeTargetAt=0,this.linkedParentVersion=0,this.hasProjected=!1,this.isVisible=!0,this.animationProgress=0,this.sharedNodes=new Map,this.latestValues=c,this.root=d?d.root||d:this,this.path=d?[...d.path,d]:[],this.parent=d,this.depth=d?d.depth+1:0;for(let m=0;mthis.root.updateBlockedByResize=!1;Pe.read(()=>{v=window.innerWidth}),n(c,()=>{const x=window.innerWidth;x!==v&&(v=x,this.root.updateBlockedByResize=!0,g&&g(),g=oA(w,250),Bl.hasAnimatedSinceResize&&(Bl.hasAnimatedSinceResize=!1,this.nodes.forEach(O0)))})}d&&this.root.registerSharedNode(d,this),this.options.animate!==!1&&p&&(d||m)&&this.addEventListener("didUpdate",({delta:g,hasLayoutChanged:v,hasRelativeLayoutChanged:w,layout:x})=>{if(this.isTreeAnimationBlocked()){this.target=void 0,this.relativeTarget=void 0;return}const S=this.options.transition||p.getDefaultTransition()||EA,{onLayoutAnimationStart:A,onLayoutAnimationComplete:k}=p.getProps(),C=!this.targetLayout||!vw(this.targetLayout,x),N=!v&&w;if(this.options.layoutRoot||this.resumeFrom||N||v&&(C||!this.currentAnimation)){this.resumeFrom&&(this.resumingFrom=this.resumeFrom,this.resumingFrom.resumingFrom=void 0);const O={...rf(S,"layout"),onPlay:A,onComplete:k};(p.shouldReduceMotion||this.options.layoutRoot)&&(O.delay=0,O.type=!1),this.startAnimation(O),this.setAnimationOrigin(g,N,O.path)}else v||O0(this),this.isLead()&&this.options.onExitComplete&&this.options.onExitComplete();this.targetLayout=x})}unmount(){this.options.layoutId&&this.willUpdate(),this.root.nodes.remove(this);const c=this.getStack();c&&c.remove(this),this.parent&&this.parent.children.delete(this),this.instance=void 0,this.eventHandlers.clear(),zi(this.updateProjection)}blockUpdate(){this.updateManuallyBlocked=!0}unblockUpdate(){this.updateManuallyBlocked=!1}isUpdateBlocked(){return this.updateManuallyBlocked||this.updateBlockedByResize}isTreeAnimationBlocked(){return this.isAnimationBlocked||this.parent&&this.parent.isTreeAnimationBlocked()||!1}startUpdate(){this.isUpdateBlocked()||(this.isUpdating=!0,this.nodes&&this.nodes.forEach(_A),this.animationId++)}getTransformTemplate(){const{visualElement:c}=this.options;return c&&c.getProps().transformTemplate}willUpdate(c=!0){if(this.root.hasTreeAnimated=!0,this.root.isUpdateBlocked()){this.options.onExitComplete&&this.options.onExitComplete();return}if(window.MotionCancelOptimisedAnimation&&!this.hasCheckedOptimisedAppear&&ww(this),!this.root.isUpdating&&this.root.startUpdate(),this.isLayoutDirty)return;this.isLayoutDirty=!0;for(let g=0;g{this.isLayoutDirty?this.root.didUpdate():this.root.checkUpdateFailed()})}updateSnapshot(){this.snapshot||!this.instance||(this.snapshot=this.measure(),this.snapshot&&!Et(this.snapshot.measuredBox.x)&&!Et(this.snapshot.measuredBox.y)&&(this.snapshot=void 0))}updateLayout(){if(!this.instance||(this.updateScroll(),!(this.options.alwaysMeasureLayout&&this.isLead())&&!this.isLayoutDirty))return;if(this.resumeFrom&&!this.resumeFrom.instance)for(let m=0;m{const G=X/1e3,Z=J==null?void 0:J(G);Z?(w.x.translate=Z.x,w.x.scale=Ve(c.x.scale,1,G),w.x.origin=c.x.origin,w.x.originPoint=c.x.originPoint,w.y.translate=Z.y,w.y.scale=Ve(c.y.scale,1,G),w.y.origin=c.y.origin,w.y.originPoint=c.y.originPoint):(N0(w.x,c.x,G),N0(w.y,c.y,G)),this.setTargetDelta(w),this.relativeTarget&&this.relativeTargetOrigin&&this.layout&&this.relativeParent&&this.relativeParent.layout&&(Fl(x,this.layout.layoutBox,this.relativeParent.layout.layoutBox,this.options.layoutAnchor||void 0),SA(this.relativeTarget,this.relativeTargetOrigin,x,G),P&&ZE(this.relativeTarget,P)&&(this.isProjectionDirty=!1),P||(P=lt()),dn(P,this.relativeTarget)),k&&(this.animationValues=v,tA(v,g,this.latestValues,G,O,N)),Z&&Z.rotate!==void 0&&(this.animationValues||(this.animationValues=v),this.animationValues.pathRotation=Z.rotate),this.root.scheduleUpdateProjection(),this.scheduleRender(),this.animationProgress=G},this.mixTargetDelta(this.options.layoutRoot?1e3:0)}startAnimation(c){var d,m,p;this.notifyListeners("animationStart"),(d=this.currentAnimation)==null||d.stop(),(p=(m=this.resumingFrom)==null?void 0:m.currentAnimation)==null||p.stop(),this.pendingAnimation&&(zi(this.pendingAnimation),this.pendingAnimation=void 0),this.pendingAnimation=Pe.update(()=>{Bl.hasAnimatedSinceResize=!0,this.motionValue||(this.motionValue=ga(0)),this.motionValue.jump(0,!1),this.currentAnimation=sA(this.motionValue,[0,1e3],{...c,velocity:0,isSync:!0,onUpdate:g=>{this.mixTargetDelta(g),c.onUpdate&&c.onUpdate(g)},onComplete:()=>{c.onComplete&&c.onComplete(),this.completeAnimation()}}),this.resumingFrom&&(this.resumingFrom.currentAnimation=this.currentAnimation),this.pendingAnimation=void 0})}completeAnimation(){this.resumingFrom&&(this.resumingFrom.currentAnimation=void 0,this.resumingFrom.preserveOpacity=void 0);const c=this.getStack();c&&c.exitAnimationComplete(),this.resumingFrom=this.currentAnimation=this.animationValues=void 0,this.notifyListeners("animationComplete")}finishAnimation(){this.currentAnimation&&(this.mixTargetDelta&&this.mixTargetDelta(uA),this.currentAnimation.stop()),this.completeAnimation()}applyTransformsToTarget(){const c=this.getLead();let{targetWithTransforms:d,target:m,layout:p,latestValues:g}=c;if(!(!d||!m||!p)){if(this!==c&&this.layout&&p&&xw(this.options.animationType,this.layout.layoutBox,p.layoutBox)){m=this.target||lt();const v=Et(this.layout.layoutBox.x);m.x.min=c.target.x.min,m.x.max=m.x.min+v;const w=Et(this.layout.layoutBox.y);m.y.min=c.target.y.min,m.y.max=m.y.min+w}dn(d,m),Ml(d,g),Tr(this.projectionDeltaWithTransform,this.layoutCorrected,d,g)}}registerSharedNode(c,d){this.sharedNodes.has(c)||this.sharedNodes.set(c,new lA),this.sharedNodes.get(c).add(d);const p=d.options.initialPromotionConfig;d.promote({transition:p?p.transition:void 0,preserveFollowOpacity:p&&p.shouldPreserveFollowOpacity?p.shouldPreserveFollowOpacity(d):void 0})}isLead(){const c=this.getStack();return c?c.lead===this:!0}getLead(){var d;const{layoutId:c}=this.options;return c?((d=this.getStack())==null?void 0:d.lead)||this:this}getPrevLead(){var d;const{layoutId:c}=this.options;return c?(d=this.getStack())==null?void 0:d.prevLead:void 0}getStack(){const{layoutId:c}=this.options;if(c)return this.root.sharedNodes.get(c)}promote({needsReset:c,transition:d,preserveFollowOpacity:m}={}){const p=this.getStack();p&&p.promote(this,m),c&&(this.projectionDelta=void 0,this.needsReset=!0),d&&this.setOptions({transition:d})}relegate(){const c=this.getStack();return c?c.relegate(this):!1}resetSkewAndRotation(){const{visualElement:c}=this.options;if(!c)return;let d=!1;const{latestValues:m}=c;if((m.z||m.rotate||m.rotateX||m.rotateY||m.rotateZ||m.skewX||m.skewY)&&(d=!0),!d)return;const p={};m.z&&Ph("z",c,p,this.animationValues);for(let g=0;g{var d;return(d=c.currentAnimation)==null?void 0:d.stop()}),this.root.nodes.forEach(k0),this.root.sharedNodes.clear()}}}function hA(n){n.updateLayout()}function dA(n){var s;const e=((s=n.resumeFrom)==null?void 0:s.snapshot)||n.snapshot;if(n.isLead()&&n.layout&&e&&n.hasListeners("didUpdate")){const{layoutBox:a,measuredBox:l}=n.layout,{animationType:u}=n.options,c=e.source!==n.layout.source;if(u==="size")kn(v=>{const w=c?e.measuredBox[v]:e.layoutBox[v],x=Et(w);w.min=a[v].min,w.max=w.min+x});else if(u==="x"||u==="y"){const v=u==="x"?"y":"x";Ad(c?e.measuredBox[v]:e.layoutBox[v],a[v])}else xw(u,e.layoutBox,a)&&kn(v=>{const w=c?e.measuredBox[v]:e.layoutBox[v],x=Et(a[v]);w.max=w.min+x,n.relativeTarget&&!n.currentAnimation&&(n.isProjectionDirty=!0,n.relativeTarget[v].max=n.relativeTarget[v].min+x)});const d=ha();Tr(d,a,e.layoutBox);const m=ha();c?Tr(m,n.applyTransform(l,!0),e.measuredBox):Tr(m,a,e.layoutBox);const p=!yw(d);let g=!1;if(!n.resumeFrom){const v=n.getClosestProjectingParent();if(v&&!v.resumeFrom){const{snapshot:w,layout:x}=v;if(w&&x){const S=n.options.layoutAnchor||void 0,A=lt();Fl(A,e.layoutBox,w.layoutBox,S);const k=lt();Fl(k,a,x.layoutBox,S),vw(A,k)||(g=!0),v.options.layoutRoot&&(n.relativeTarget=k,n.relativeTargetOrigin=A,n.relativeParent=v)}}}n.notifyListeners("didUpdate",{layout:a,snapshot:e,delta:m,layoutDelta:d,hasLayoutChanged:p,hasRelativeLayoutChanged:g})}else if(n.isLead()){const{onExitComplete:a}=n.options;a&&a()}n.options.transition=void 0}function fA(n){n.parent&&(n.isProjecting()||(n.isProjectionDirty=n.parent.isProjectionDirty),n.isSharedProjectionDirty||(n.isSharedProjectionDirty=!!(n.isProjectionDirty||n.parent.isProjectionDirty||n.parent.isSharedProjectionDirty)),n.isTransformDirty||(n.isTransformDirty=n.parent.isTransformDirty))}function mA(n){n.isProjectionDirty=n.isSharedProjectionDirty=n.isTransformDirty=!1}function pA(n){n.clearSnapshot()}function k0(n){n.clearMeasurements()}function gA(n){n.isLayoutDirty=!0,n.updateLayout()}function j0(n){n.isLayoutDirty=!1}function yA(n){n.isAnimationBlocked&&n.layout&&!n.isLayoutDirty&&(n.snapshot=n.layout,n.isLayoutDirty=!0)}function vA(n){const{visualElement:e}=n.options;e&&e.getProps().onBeforeLayoutMeasure&&e.notify("BeforeLayoutMeasure"),n.resetTransform()}function O0(n){n.finishAnimation(),n.targetDelta=n.relativeTarget=n.target=void 0,n.isProjectionDirty=!0}function bA(n){n.resolveTargetDelta()}function wA(n){n.calcProjection()}function _A(n){n.resetSkewAndRotation()}function xA(n){n.removeLeadSnapshot()}function N0(n,e,s){n.translate=Ve(e.translate,0,s),n.scale=Ve(e.scale,1,s),n.origin=e.origin,n.originPoint=e.originPoint}function D0(n,e,s,a){n.min=Ve(e.min,s.min,a),n.max=Ve(e.max,s.max,a)}function SA(n,e,s,a){D0(n.x,e.x,s.x,a),D0(n.y,e.y,s.y,a)}function TA(n){return n.animationValues&&n.animationValues.opacityExit!==void 0}const EA={duration:.45,ease:[.4,0,.1,1]},M0=n=>typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().includes(n),U0=M0("applewebkit/")&&!M0("chrome/")?Math.round:ln;function B0(n){n.min=U0(n.min),n.max=U0(n.max)}function AA(n){B0(n.x),B0(n.y)}function xw(n,e,s){return n==="position"||n==="preserve-aspect"&&!FE(T0(e),T0(s),.2)}function RA(n){var e;return n!==n.root&&((e=n.scroll)==null?void 0:e.wasRoot)}const CA=_w({attachResizeListener:(n,e)=>Dr(n,"resize",e),measureScroll:()=>{var n,e;return{x:document.documentElement.scrollLeft||((n=document.body)==null?void 0:n.scrollLeft)||0,y:document.documentElement.scrollTop||((e=document.body)==null?void 0:e.scrollTop)||0}},checkIsScrollRoot:()=>!0}),Hh={current:void 0},Sw=_w({measureScroll:n=>({x:n.scrollLeft,y:n.scrollTop}),defaultParent:()=>{if(!Hh.current){const n=new CA({});n.mount(window),n.setOptions({layoutScroll:!0}),Hh.current=n}return Hh.current},resetTransform:(n,e)=>{n.style.transform=e!==void 0?e:"none"},checkIsScrollRoot:n=>window.getComputedStyle(n).position==="fixed"}),vf=V.createContext({transformPagePoint:n=>n,isStatic:!1,reducedMotion:"never"});function L0(n,e){if(typeof n=="function")return n(e);n!=null&&(n.current=e)}function kA(...n){return e=>{let s=!1;const a=n.map(l=>{const u=L0(l,e);return!s&&typeof u=="function"&&(s=!0),u});if(s)return()=>{for(let l=0;l{const{width:x,height:S,top:A,left:k,right:C,bottom:N,direction:O}=m.current;if(e||u===!1||!d.current||!x||!S)return;const P=O==="rtl",J=s==="left"?P?`right: ${C}`:`left: ${k}`:P?`left: ${k}`:`right: ${C}`,X=a==="bottom"?`bottom: ${N}`:`top: ${A}`;d.current.dataset.motionPopId=c;const G=document.createElement("style");p&&(G.nonce=p);const Z=l??document.head;return Z.appendChild(G),G.sheet&&G.sheet.insertRule(` - [data-motion-pop-id="${c}"] { - position: absolute !important; - width: ${x}px !important; - height: ${S}px !important; - ${J}px !important; - ${X}px !important; - } - `),()=>{var ee;(ee=d.current)==null||ee.removeAttribute("data-motion-pop-id"),Z.contains(G)&&Z.removeChild(G)}},[e]),b.jsx(OA,{isPresent:e,childRef:d,sizeRef:m,pop:u,children:u===!1?n:V.cloneElement(n,{ref:v})})}const DA=({children:n,initial:e,isPresent:s,onExitComplete:a,custom:l,presenceAffectsLayout:u,mode:c,anchorX:d,anchorY:m,root:p})=>{const g=Gd(MA),v=V.useId(),w=V.useRef(s),x=V.useRef(a);Kd(()=>{w.current=s,x.current=a});let S=!0,A=V.useMemo(()=>(S=!1,{id:v,initial:e,isPresent:s,custom:l,onExitComplete:k=>{g.set(k,!0);for(const C of g.values())if(!C)return;a&&a()},register:k=>(g.set(k,!1),()=>{var C;g.delete(k),!w.current&&!g.size&&((C=x.current)==null||C.call(x))})}),[s,g,a]);return u&&S&&(A={...A}),V.useMemo(()=>{g.forEach((k,C)=>g.set(C,!1))},[s]),V.useEffect(()=>{!s&&!g.size&&a&&a()},[s]),n=b.jsx(NA,{pop:c==="popLayout",isPresent:s,anchorX:d,anchorY:m,root:p,children:n}),b.jsx(iu.Provider,{value:A,children:n})};function MA(){return new Map}function Tw(n=!0){const e=V.useContext(iu);if(e===null)return[!0,null];const{isPresent:s,onExitComplete:a,register:l}=e,u=V.useId();V.useEffect(()=>{if(n)return l(u)},[n]);const c=V.useCallback(()=>n&&a&&a(u),[u,a,n]);return!s&&a?[!1,c]:[!0]}const gl=n=>n.key||"";function z0(n){const e=[];return V.Children.forEach(n,s=>{V.isValidElement(s)&&e.push(s)}),e}const cs=({children:n,custom:e,initial:s=!0,onExitComplete:a,presenceAffectsLayout:l=!0,mode:u="sync",propagate:c=!1,anchorX:d="left",anchorY:m="top",root:p})=>{const[g,v]=Tw(c),w=V.useMemo(()=>z0(n),[n]),x=c&&!g?[]:w.map(gl),S=V.useRef(!0),A=V.useRef(w),k=Gd(()=>new Map),C=V.useRef(new Set),[N,O]=V.useState(w),[P,J]=V.useState(w);Kd(()=>{S.current=!1,A.current=w;for(let Z=0;Z{const ee=gl(Z),ne=c&&!g?!1:w===P||x.includes(ee),le=()=>{if(C.current.has(ee))return;if(k.has(ee))C.current.add(ee),k.set(ee,!0);else return;let de=!0;k.forEach(je=>{je||(de=!1)}),de&&(G==null||G(),J(A.current),c&&(v==null||v()),a&&a())};return b.jsx(DA,{isPresent:ne,initial:!S.current||s?void 0:!1,custom:e,presenceAffectsLayout:l,mode:u,root:p,onExitComplete:ne?void 0:le,anchorX:d,anchorY:m,children:Z},ee)})})},Ew=V.createContext({strict:!1}),V0={animation:["animate","variants","whileHover","whileTap","exit","whileInView","whileFocus","whileDrag"],exit:["exit"],drag:["drag","dragControls"],focus:["whileFocus"],hover:["whileHover","onHoverStart","onHoverEnd"],tap:["whileTap","onTap","onTapStart","onTapCancel"],pan:["onPan","onPanStart","onPanSessionStart","onPanEnd"],inView:["whileInView","onViewportEnter","onViewportLeave"],layout:["layout","layoutId"]};let P0=!1;function UA(){if(P0)return;const n={};for(const e in V0)n[e]={isEnabled:s=>V0[e].some(a=>!!s[a])};tw(n),P0=!0}function Aw(){return UA(),bE()}function BA(n){const e=Aw();for(const s in n)e[s]={...e[s],...n[s]};tw(e)}const LA=new Set(["animate","exit","variants","initial","style","values","variants","transition","transformTemplate","custom","inherit","onBeforeLayoutMeasure","onAnimationStart","onAnimationComplete","onUpdate","onDragStart","onDrag","onDragEnd","onMeasureDragConstraints","onDirectionLock","onDragTransitionEnd","_dragX","_dragY","onHoverStart","onHoverEnd","onViewportEnter","onViewportLeave","globalTapTarget","propagate","ignoreStrict","viewport"]);function Yl(n){return n.startsWith("while")||n.startsWith("drag")&&n!=="draggable"||n.startsWith("layout")||n.startsWith("onTap")||n.startsWith("onPan")||n.startsWith("onLayout")||LA.has(n)}let Rw=n=>!Yl(n);function zA(n){typeof n=="function"&&(Rw=e=>e.startsWith("on")?!Yl(e):n(e))}try{zA(require("@emotion/is-prop-valid").default)}catch{}function VA(n,e,s){const a={};for(const l in n)l==="values"&&typeof n.values=="object"||bt(n[l])||(Rw(l)||s===!0&&Yl(l)||!e&&!Yl(l)||n.draggable&&l.startsWith("onDrag"))&&(a[l]=n[l]);return a}const ou=V.createContext({});function PA(n,e){if(ru(n)){const{initial:s,animate:a}=n;return{initial:s===!1||Nr(s)?s:void 0,animate:Nr(a)?a:void 0}}return n.inherit!==!1?e:{}}function HA(n){const{initial:e,animate:s}=PA(n,V.useContext(ou));return V.useMemo(()=>({initial:e,animate:s}),[H0(e),H0(s)])}function H0(n){return Array.isArray(n)?n.join(" "):n}const bf=()=>({style:{},transform:{},transformOrigin:{},vars:{}});function Cw(n,e,s){for(const a in e)!bt(e[a])&&!lw(a,s)&&(n[a]=e[a])}function $A({transformTemplate:n},e){return V.useMemo(()=>{const s=bf();return gf(s,e,n),Object.assign({},s.vars,s.style)},[e])}function qA(n,e){const s=n.style||{},a={};return Cw(a,s,n),Object.assign(a,$A(n,e)),a}function IA(n,e){const s={},a=qA(n,e);return n.drag&&n.dragListener!==!1&&(s.draggable=!1,a.userSelect=a.WebkitUserSelect=a.WebkitTouchCallout="none",a.touchAction=n.drag===!0?"none":`pan-${n.drag==="x"?"y":"x"}`),n.tabIndex===void 0&&(n.onTap||n.onTapStart||n.whileTap)&&(s.tabIndex=0),s.style=a,s}const kw=()=>({...bf(),attrs:{}});function GA(n,e,s,a){const l=V.useMemo(()=>{const u=kw();return uw(u,e,hw(a),n.transformTemplate,n.style),{...u.attrs,style:{...u.style}}},[e]);if(n.style){const u={};Cw(u,n.style,n),l.style={...u,...l.style}}return l}const KA=["animate","circle","defs","desc","ellipse","g","image","line","filter","marker","mask","metadata","path","pattern","polygon","polyline","rect","stop","switch","symbol","svg","text","tspan","use","view"];function wf(n){return typeof n!="string"||n.includes("-")?!1:!!(KA.indexOf(n)>-1||/[A-Z]/u.test(n))}function FA(n,e,s,{latestValues:a},l,u=!1,c){const m=(c??wf(n)?GA:IA)(e,a,l,n),p=VA(e,typeof n=="string",u),g=n!==V.Fragment?{...p,...m,ref:s}:{},{children:v}=e,w=V.useMemo(()=>bt(v)?v.get():v,[v]);return V.createElement(n,{...g,children:w})}function YA({scrapeMotionValuesFromProps:n,createRenderState:e},s,a,l){return{latestValues:XA(s,a,l,n),renderState:e()}}function XA(n,e,s,a){const l={},u=a(n,{});for(const w in u)l[w]=Ul(u[w]);let{initial:c,animate:d}=n;const m=ru(n),p=Wb(n);e&&p&&!m&&n.inherit!==!1&&(c===void 0&&(c=e.initial),d===void 0&&(d=e.animate));let g=s?s.initial===!1:!1;g=g||c===!1;const v=g?d:c;if(v&&typeof v!="boolean"&&!au(v)){const w=Array.isArray(v)?v:[v];for(let x=0;x(e,s)=>{const a=V.useContext(ou),l=V.useContext(iu),u=()=>YA(n,e,a,l);return s?u():Gd(u)},JA=jw({scrapeMotionValuesFromProps:yf,createRenderState:bf}),QA=jw({scrapeMotionValuesFromProps:dw,createRenderState:kw}),ZA=Symbol.for("motionComponentSymbol");function WA(n,e,s){const a=V.useRef(s);V.useInsertionEffect(()=>{a.current=s});const l=V.useRef(null);return V.useCallback(u=>{var d;u&&((d=n.onMount)==null||d.call(n,u)),e&&(u?e.mount(u):e.unmount());const c=a.current;if(typeof c=="function")if(u){const m=c(u);typeof m=="function"&&(l.current=m)}else l.current?(l.current(),l.current=null):c(u);else c&&(c.current=u)},[e])}const Ow=V.createContext({});function aa(n){return n&&typeof n=="object"&&Object.prototype.hasOwnProperty.call(n,"current")}function e2(n,e,s,a,l,u){var O,P;const{visualElement:c}=V.useContext(ou),d=V.useContext(Ew),m=V.useContext(iu),p=V.useContext(vf),g=p.reducedMotion,v=p.skipAnimations,w=V.useRef(null),x=V.useRef(!1);a=a||d.renderer,!w.current&&a&&(w.current=a(n,{visualState:e,parent:c,props:s,presenceContext:m,blockInitialAnimation:m?m.initial===!1:!1,reducedMotionConfig:g,skipAnimations:v,isSVG:u}),x.current&&w.current&&(w.current.manuallyAnimateOnMount=!0));const S=w.current,A=V.useContext(Ow);S&&!S.projection&&l&&(S.type==="html"||S.type==="svg")&&t2(w.current,s,l,A);const k=V.useRef(!1);V.useInsertionEffect(()=>{S&&k.current&&S.update(s,m)});const C=s[Hb],N=V.useRef(!!C&&typeof window<"u"&&!((O=window.MotionHandoffIsComplete)!=null&&O.call(window,C))&&((P=window.MotionHasOptimisedAnimation)==null?void 0:P.call(window,C)));return Kd(()=>{x.current=!0,S&&(k.current=!0,window.MotionIsMounted=!0,S.updateFeatures(),S.scheduleRenderMicrotask(),N.current&&S.animationState&&S.animationState.animateChanges())}),V.useEffect(()=>{S&&(!N.current&&S.animationState&&S.animationState.animateChanges(),N.current&&(queueMicrotask(()=>{var J;(J=window.MotionHandoffMarkAsComplete)==null||J.call(window,C)}),N.current=!1),S.enteringChildren=void 0)}),S}function t2(n,e,s,a){const{layoutId:l,layout:u,drag:c,dragConstraints:d,layoutScroll:m,layoutRoot:p,layoutAnchor:g,layoutCrossfade:v}=e;n.projection=new s(n.latestValues,e["data-framer-portal-id"]?void 0:Nw(n.parent)),n.projection.setOptions({layoutId:l,layout:u,alwaysMeasureLayout:!!c||d&&aa(d),visualElement:n,animationType:typeof u=="string"?u:"both",initialPromotionConfig:a,crossfade:v,layoutScroll:m,layoutRoot:p,layoutAnchor:g})}function Nw(n){if(n)return n.options.allowProjection!==!1?n.projection:Nw(n.parent)}function $h(n,{forwardMotionProps:e=!1,type:s}={},a,l){a&&BA(a);const u=s?s==="svg":wf(n),c=u?QA:JA;function d(p,g){let v;const w={...V.useContext(vf),...p,layoutId:n2(p)},{isStatic:x}=w,S=HA(p),A=c(p,x);if(!x&&typeof window<"u"){i2();const k=s2(w);v=k.MeasureLayout,S.visualElement=e2(n,A,w,l,k.ProjectionNode,u)}return b.jsxs(ou.Provider,{value:S,children:[v&&S.visualElement?b.jsx(v,{visualElement:S.visualElement,...w}):null,FA(n,p,WA(A,S.visualElement,g),A,x,e,u)]})}d.displayName=`motion.${typeof n=="string"?n:`create(${n.displayName??n.name??""})`}`;const m=V.forwardRef(d);return m[ZA]=n,m}function n2({layoutId:n}){const e=V.useContext(Id).id;return e&&n!==void 0?e+"-"+n:n}function i2(n,e){V.useContext(Ew).strict}function s2(n){const e=Aw(),{drag:s,layout:a}=e;if(!s&&!a)return{};const l={...s,...a};return{MeasureLayout:s!=null&&s.isEnabled(n)||a!=null&&a.isEnabled(n)?l.MeasureLayout:void 0,ProjectionNode:l.ProjectionNode}}function a2(n,e){if(typeof Proxy>"u")return $h;const s=new Map,a=(u,c)=>$h(u,c,n,e),l=(u,c)=>a(u,c);return new Proxy(l,{get:(u,c)=>c==="create"?a:(s.has(c)||s.set(c,$h(c,void 0,n,e)),s.get(c))})}const r2=(n,e)=>e.isSVG??wf(n)?new BE(e):new jE(e,{allowProjection:n!==V.Fragment});class o2 extends Vi{constructor(e){super(e),e.animationState||(e.animationState=HE(e))}updateAnimationControlsSubscription(){const{animate:e}=this.node.getProps();au(e)&&(this.unmountControls=e.subscribe(this.node))}mount(){this.updateAnimationControlsSubscription()}update(){const{animate:e}=this.node.getProps(),{animate:s}=this.node.prevProps||{};e!==s&&this.updateAnimationControlsSubscription()}unmount(){var e;this.node.animationState.reset(),(e=this.unmountControls)==null||e.call(this)}}let l2=0;class u2 extends Vi{constructor(){super(...arguments),this.id=l2++,this.isExitComplete=!1}update(){var u;if(!this.node.presenceContext)return;const{isPresent:e,onExitComplete:s}=this.node.presenceContext,{isPresent:a}=this.node.prevPresenceContext||{};if(!this.node.animationState||e===a)return;if(e&&a===!1){if(this.isExitComplete){const{initial:c,custom:d}=this.node.getProps();if(typeof c=="string"||typeof c=="object"&&c!==null&&!Array.isArray(c)){const m=ps(this.node,c,d);if(m){const{transition:p,transitionEnd:g,...v}=m;for(const w in v)(u=this.node.getValue(w))==null||u.jump(v[w])}}this.node.animationState.reset(),this.node.animationState.animateChanges()}else this.node.animationState.setActive("exit",!1);this.isExitComplete=!1;return}const l=this.node.animationState.setActive("exit",!e);s&&!e&&l.then(()=>{this.isExitComplete=!0,s(this.id)})}mount(){const{register:e,onExitComplete:s}=this.node.presenceContext||{};s&&s(this.id),e&&(this.unmount=e(this.id))}unmount(){}}const c2={animation:{Feature:o2},exit:{Feature:u2}};function Kr(n){return{point:{x:n.pageX,y:n.pageY}}}const h2=n=>e=>df(e)&&n(e,Kr(e));function Er(n,e,s,a){return Dr(n,e,h2(s),a)}const Dw=({current:n})=>n?n.ownerDocument.defaultView:null,$0=(n,e)=>Math.abs(n-e);function d2(n,e){const s=$0(n.x,e.x),a=$0(n.y,e.y);return Math.sqrt(s**2+a**2)}const q0=new Set(["auto","scroll"]);class Mw{constructor(e,s,{transformPagePoint:a,contextWindow:l=window,dragSnapToOrigin:u=!1,distanceThreshold:c=3,element:d}={}){if(this.startEvent=null,this.lastMoveEvent=null,this.lastMoveEventInfo=null,this.lastRawMoveEventInfo=null,this.handlers={},this.contextWindow=window,this.scrollPositions=new Map,this.removeScrollListeners=null,this.onElementScroll=S=>{this.handleScroll(S.target)},this.onWindowScroll=()=>{this.handleScroll(window)},this.updatePoint=()=>{if(!(this.lastMoveEvent&&this.lastMoveEventInfo))return;this.lastRawMoveEventInfo&&(this.lastMoveEventInfo=yl(this.lastRawMoveEventInfo,this.transformPagePoint));const S=qh(this.lastMoveEventInfo,this.history),A=this.startEvent!==null,k=d2(S.offset,{x:0,y:0})>=this.distanceThreshold;if(!A&&!k)return;const{point:C}=S,{timestamp:N}=vt;this.history.push({...C,timestamp:N});const{onStart:O,onMove:P}=this.handlers;A||(O&&O(this.lastMoveEvent,S),this.startEvent=this.lastMoveEvent),P&&P(this.lastMoveEvent,S)},this.handlePointerMove=(S,A)=>{this.lastMoveEvent=S,this.lastRawMoveEventInfo=A,this.lastMoveEventInfo=yl(A,this.transformPagePoint),Pe.update(this.updatePoint,!0)},this.handlePointerUp=(S,A)=>{this.end();const{onEnd:k,onSessionEnd:C,resumeAnimation:N}=this.handlers;if((this.dragSnapToOrigin||!this.startEvent)&&N&&N(),!(this.lastMoveEvent&&this.lastMoveEventInfo))return;const O=qh(S.type==="pointercancel"?this.lastMoveEventInfo:yl(A,this.transformPagePoint),this.history);this.startEvent&&k&&k(S,O),C&&C(S,O)},!df(e))return;this.dragSnapToOrigin=u,this.handlers=s,this.transformPagePoint=a,this.distanceThreshold=c,this.contextWindow=l||window;const m=Kr(e),p=yl(m,this.transformPagePoint),{point:g}=p,{timestamp:v}=vt;this.history=[{...g,timestamp:v}];const{onSessionStart:w}=s;w&&w(e,qh(p,this.history));const x={passive:!0,capture:!0};this.removeListeners=qr(Er(this.contextWindow,"pointermove",this.handlePointerMove,x),Er(this.contextWindow,"pointerup",this.handlePointerUp,x),Er(this.contextWindow,"pointercancel",this.handlePointerUp,x)),d&&this.startScrollTracking(d)}startScrollTracking(e){let s=e.parentElement;for(;s;){const a=getComputedStyle(s);(q0.has(a.overflowX)||q0.has(a.overflowY))&&this.scrollPositions.set(s,{x:s.scrollLeft,y:s.scrollTop}),s=s.parentElement}this.scrollPositions.set(window,{x:window.scrollX,y:window.scrollY}),window.addEventListener("scroll",this.onElementScroll,{capture:!0}),window.addEventListener("scroll",this.onWindowScroll),this.removeScrollListeners=()=>{window.removeEventListener("scroll",this.onElementScroll,{capture:!0}),window.removeEventListener("scroll",this.onWindowScroll)}}handleScroll(e){const s=this.scrollPositions.get(e);if(!s)return;const a=e===window,l=a?{x:window.scrollX,y:window.scrollY}:{x:e.scrollLeft,y:e.scrollTop},u={x:l.x-s.x,y:l.y-s.y};u.x===0&&u.y===0||(a?this.lastMoveEventInfo&&(this.lastMoveEventInfo.point.x+=u.x,this.lastMoveEventInfo.point.y+=u.y):this.history.length>0&&(this.history[0].x-=u.x,this.history[0].y-=u.y),this.scrollPositions.set(e,l),Pe.update(this.updatePoint,!0))}updateHandlers(e){this.handlers=e}end(){this.removeListeners&&this.removeListeners(),this.removeScrollListeners&&this.removeScrollListeners(),this.scrollPositions.clear(),zi(this.updatePoint)}}function yl(n,e){return e?{point:e(n.point)}:n}function I0(n,e){return{x:n.x-e.x,y:n.y-e.y}}function qh({point:n},e){return{point:n,delta:I0(n,Uw(e)),offset:I0(n,f2(e)),velocity:m2(e,.1)}}function f2(n){return n[0]}function Uw(n){return n[n.length-1]}function m2(n,e){if(n.length<2)return{x:0,y:0};let s=n.length-1,a=null;const l=Uw(n);for(;s>=0&&(a=n[s],!(l.timestamp-a.timestamp>Ft(e)));)s--;if(!a)return{x:0,y:0};a===n[0]&&n.length>2&&l.timestamp-a.timestamp>Ft(e)*2&&(a=n[1]);const u=on(l.timestamp-a.timestamp);if(u===0)return{x:0,y:0};const c={x:(l.x-a.x)/u,y:(l.y-a.y)/u};return c.x===1/0&&(c.x=0),c.y===1/0&&(c.y=0),c}function p2(n,{min:e,max:s},a){return e!==void 0&&ns&&(n=a?Ve(s,n,a.max):Math.min(n,s)),n}function G0(n,e,s){return{min:e!==void 0?n.min+e:void 0,max:s!==void 0?n.max+s-(n.max-n.min):void 0}}function g2(n,{top:e,left:s,bottom:a,right:l}){return{x:G0(n.x,s,l),y:G0(n.y,e,a)}}function K0(n,e){let s=e.min-n.min,a=e.max-n.max;return e.max-e.mina?s=jr(e.min,e.max-a,n.min):a>l&&(s=jr(n.min,n.max-l,e.min)),Mn(0,1,s)}function b2(n,e){const s={};return e.min!==void 0&&(s.min=e.min-n.min),e.max!==void 0&&(s.max=e.max-n.min),s}const Rd=.35;function w2(n=Rd){return n===!1?n=0:n===!0&&(n=Rd),{x:F0(n,"left","right"),y:F0(n,"top","bottom")}}function F0(n,e,s){return{min:Y0(n,e),max:Y0(n,s)}}function Y0(n,e){return typeof n=="number"?n:n[e]||0}const _2=new WeakMap;class x2{constructor(e){this.openDragLock=null,this.isDragging=!1,this.currentDirection=null,this.originPoint={x:0,y:0},this.constraints=!1,this.hasMutatedConstraints=!1,this.elastic=lt(),this.latestPointerEvent=null,this.latestPanInfo=null,this.visualElement=e}start(e,{snapToCursor:s=!1,distanceThreshold:a}={}){const{presenceContext:l}=this.visualElement;if(l&&l.isPresent===!1)return;const u=v=>{s&&this.snapToCursor(Kr(v).point),this.stopAnimation()},c=(v,w)=>{const{drag:x,dragPropagation:S,onDragStart:A}=this.getProps();if(x&&!S&&(this.openDragLock&&this.openDragLock(),this.openDragLock=XT(x),!this.openDragLock))return;this.latestPointerEvent=v,this.latestPanInfo=w,this.isDragging=!0,this.currentDirection=null,this.resolveConstraints(),this.visualElement.projection&&(this.visualElement.projection.isAnimationBlocked=!0,this.visualElement.projection.target=void 0),kn(C=>{let N=this.getAxisMotionValue(C).get()||0;if(Dn.test(N)){const{projection:O}=this.visualElement;if(O&&O.layout){const P=O.layout.layoutBox[C];P&&(N=Et(P)*(parseFloat(N)/100))}}this.originPoint[C]=N}),A&&Pe.update(()=>A(v,w),!1,!0),yd(this.visualElement,"transform");const{animationState:k}=this.visualElement;k&&k.setActive("whileDrag",!0)},d=(v,w)=>{this.latestPointerEvent=v,this.latestPanInfo=w;const{dragPropagation:x,dragDirectionLock:S,onDirectionLock:A,onDrag:k}=this.getProps();if(!x&&!this.openDragLock)return;const{offset:C}=w;if(S&&this.currentDirection===null){this.currentDirection=T2(C),this.currentDirection!==null&&A&&A(this.currentDirection);return}this.updateAxis("x",w.point,C),this.updateAxis("y",w.point,C),this.visualElement.render(),k&&Pe.update(()=>k(v,w),!1,!0)},m=(v,w)=>{this.latestPointerEvent=v,this.latestPanInfo=w,this.stop(v,w),this.latestPointerEvent=null,this.latestPanInfo=null},p=()=>{const{dragSnapToOrigin:v}=this.getProps();(v||this.constraints)&&this.startAnimation({x:0,y:0})},{dragSnapToOrigin:g}=this.getProps();this.panSession=new Mw(e,{onSessionStart:u,onStart:c,onMove:d,onSessionEnd:m,resumeAnimation:p},{transformPagePoint:this.visualElement.getTransformPagePoint(),dragSnapToOrigin:g,distanceThreshold:a,contextWindow:Dw(this.visualElement),element:this.visualElement.current})}stop(e,s){const a=e||this.latestPointerEvent,l=s||this.latestPanInfo,u=this.isDragging;if(this.cancel(),!u||!l||!a)return;const{velocity:c}=l;this.startAnimation(c);const{onDragEnd:d}=this.getProps();d&&Pe.postRender(()=>d(a,l))}cancel(){this.isDragging=!1;const{projection:e,animationState:s}=this.visualElement;e&&(e.isAnimationBlocked=!1),this.endPanSession();const{dragPropagation:a}=this.getProps();!a&&this.openDragLock&&(this.openDragLock(),this.openDragLock=null),s&&s.setActive("whileDrag",!1)}endPanSession(){this.panSession&&this.panSession.end(),this.panSession=void 0}updateAxis(e,s,a){const{drag:l}=this.getProps();if(!a||!vl(e,l,this.currentDirection))return;const u=this.getAxisMotionValue(e);let c=this.originPoint[e]+a[e];this.constraints&&this.constraints[e]&&(c=p2(c,this.constraints[e],this.elastic[e])),u.set(c)}resolveConstraints(){var u;const{dragConstraints:e,dragElastic:s}=this.getProps(),a=this.visualElement.projection&&!this.visualElement.projection.layout?this.visualElement.projection.measure(!1):(u=this.visualElement.projection)==null?void 0:u.layout,l=this.constraints;e&&aa(e)?this.constraints||(this.constraints=this.resolveRefConstraints()):e&&a?this.constraints=g2(a.layoutBox,e):this.constraints=!1,this.elastic=w2(s),l!==this.constraints&&!aa(e)&&a&&this.constraints&&!this.hasMutatedConstraints&&kn(c=>{this.constraints!==!1&&this.getAxisMotionValue(c)&&(this.constraints[c]=b2(a.layoutBox[c],this.constraints[c]))})}resolveRefConstraints(){const{dragConstraints:e,onMeasureDragConstraints:s}=this.getProps();if(!e||!aa(e))return!1;const a=e.current,{projection:l}=this.visualElement;if(!l||!l.layout)return!1;l.root&&(l.root.scroll=void 0,l.root.updateScroll());const u=TE(a,l.root,this.visualElement.getTransformPagePoint());let c=y2(l.layout.layoutBox,u);if(s){const d=s(_E(c));this.hasMutatedConstraints=!!d,d&&(c=iw(d))}return c}startAnimation(e){const{drag:s,dragMomentum:a,dragElastic:l,dragTransition:u,dragSnapToOrigin:c,onDragTransitionEnd:d}=this.getProps(),m=this.constraints||{},p=kn(g=>{if(!vl(g,s,this.currentDirection))return;let v=m&&m[g]||{};(c===!0||c===g)&&(v={min:0,max:0});const w=l?200:1e6,x=l?40:1e7,S={type:"inertia",velocity:a?e[g]:0,bounceStiffness:w,bounceDamping:x,timeConstant:750,restDelta:1,restSpeed:10,...u,...v};return this.startAxisValueAnimation(g,S)});return Promise.all(p).then(d)}startAxisValueAnimation(e,s){const a=this.getAxisMotionValue(e);return yd(this.visualElement,e),a.start(of(e,a,0,s,this.visualElement,!1))}stopAnimation(){kn(e=>this.getAxisMotionValue(e).stop())}getAxisMotionValue(e){const s=`_drag${e.toUpperCase()}`,l=this.visualElement.getProps()[s];return l||this.visualElement.getValue(e,this.visualElement.latestValues[e]??0)}snapToCursor(e){kn(s=>{const{drag:a}=this.getProps();if(!vl(s,a,this.currentDirection))return;const{projection:l}=this.visualElement,u=this.getAxisMotionValue(s);if(l&&l.layout){const{min:c,max:d}=l.layout.layoutBox[s],m=u.get()||0;u.set(e[s]-Ve(c,d,.5)+m)}})}scalePositionWithinConstraints(){if(!this.visualElement.current)return;const{drag:e,dragConstraints:s}=this.getProps(),{projection:a}=this.visualElement;if(!aa(s)||!a||!this.constraints)return;this.stopAnimation();const l={x:0,y:0};kn(c=>{const d=this.getAxisMotionValue(c);if(d&&this.constraints!==!1){const m=d.get();l[c]=v2({min:m,max:m},this.constraints[c])}});const{transformTemplate:u}=this.visualElement.getProps();this.visualElement.current.style.transform=u?u({},""):"none",a.root&&a.root.updateScroll(),a.updateLayout(),this.constraints=!1,this.resolveConstraints(),kn(c=>{if(!vl(c,e,null))return;const d=this.getAxisMotionValue(c),{min:m,max:p}=this.constraints[c];d.set(Ve(m,p,l[c]))}),this.visualElement.render()}addListeners(){if(!this.visualElement.current)return;_2.set(this.visualElement,this);const e=this.visualElement.current,s=Er(e,"pointerdown",p=>{const{drag:g,dragListener:v=!0}=this.getProps(),w=p.target,x=w!==e&&tE(w);g&&v&&!x&&this.start(p)});let a;const l=()=>{const{dragConstraints:p}=this.getProps();aa(p)&&p.current&&(this.constraints=this.resolveRefConstraints(),a||(a=S2(e,p.current,()=>this.scalePositionWithinConstraints())))},{projection:u}=this.visualElement,c=u.addEventListener("measure",l);u&&!u.layout&&(u.root&&u.root.updateScroll(),u.updateLayout()),Pe.read(l);const d=Dr(window,"resize",()=>this.scalePositionWithinConstraints()),m=u.addEventListener("didUpdate",(({delta:p,hasLayoutChanged:g})=>{this.isDragging&&g&&(kn(v=>{const w=this.getAxisMotionValue(v);w&&(this.originPoint[v]+=p[v].translate,w.set(w.get()+p[v].translate))}),this.visualElement.render())}));return()=>{d(),s(),c(),m&&m(),a&&a()}}getProps(){const e=this.visualElement.getProps(),{drag:s=!1,dragDirectionLock:a=!1,dragPropagation:l=!1,dragConstraints:u=!1,dragElastic:c=Rd,dragMomentum:d=!0}=e;return{...e,drag:s,dragDirectionLock:a,dragPropagation:l,dragConstraints:u,dragElastic:c,dragMomentum:d}}}function X0(n){let e=!0;return()=>{if(e){e=!1;return}n()}}function S2(n,e,s){const a=n0(n,X0(s)),l=n0(e,X0(s));return()=>{a(),l()}}function vl(n,e,s){return(e===!0||e===n)&&(s===null||s===n)}function T2(n,e=10){let s=null;return Math.abs(n.y)>e?s="y":Math.abs(n.x)>e&&(s="x"),s}class E2 extends Vi{constructor(e){super(e),this.removeGroupControls=ln,this.removeListeners=ln,this.controls=new x2(e)}mount(){const{dragControls:e}=this.node.getProps();e&&(this.removeGroupControls=e.subscribe(this.controls)),this.removeListeners=this.controls.addListeners()||ln}update(){const{dragControls:e}=this.node.getProps(),{dragControls:s}=this.node.prevProps||{};e!==s&&(this.removeGroupControls(),e&&(this.removeGroupControls=e.subscribe(this.controls)))}unmount(){this.removeGroupControls(),this.removeListeners(),this.controls.isDragging||this.controls.endPanSession()}}const Ih=n=>(e,s)=>{n&&Pe.update(()=>n(e,s),!1,!0)};class A2 extends Vi{constructor(){super(...arguments),this.removePointerDownListener=ln}onPointerDown(e){this.session=new Mw(e,this.createPanHandlers(),{transformPagePoint:this.node.getTransformPagePoint(),contextWindow:Dw(this.node)})}createPanHandlers(){const{onPanSessionStart:e,onPanStart:s,onPan:a,onPanEnd:l}=this.node.getProps();return{onSessionStart:Ih(e),onStart:Ih(s),onMove:Ih(a),onEnd:(u,c)=>{delete this.session,l&&Pe.postRender(()=>l(u,c))}}}mount(){this.removePointerDownListener=Er(this.node.current,"pointerdown",e=>this.onPointerDown(e))}update(){this.session&&this.session.updateHandlers(this.createPanHandlers())}unmount(){this.removePointerDownListener(),this.session&&this.session.end()}}let Gh=!1;class R2 extends V.Component{componentDidMount(){const{visualElement:e,layoutGroup:s,switchLayoutGroup:a,layoutId:l}=this.props,{projection:u}=e;u&&(s.group&&s.group.add(u),a&&a.register&&l&&a.register(u),Gh&&u.root.didUpdate(),u.addEventListener("animationComplete",()=>{this.safeToRemove()}),u.setOptions({...u.options,layoutDependency:this.props.layoutDependency,onExitComplete:()=>this.safeToRemove()})),Bl.hasEverUpdated=!0}getSnapshotBeforeUpdate(e){const{layoutDependency:s,visualElement:a,drag:l,isPresent:u}=this.props,{projection:c}=a;return c&&(c.isPresent=u,e.layoutDependency!==s&&c.setOptions({...c.options,layoutDependency:s}),Gh=!0,l||e.layoutDependency!==s||s===void 0||e.isPresent!==u?c.willUpdate():this.safeToRemove(),e.isPresent!==u&&(u?c.promote():c.relegate()||Pe.postRender(()=>{const d=c.getStack();(!d||!d.members.length)&&this.safeToRemove()}))),null}componentDidUpdate(){const{visualElement:e,layoutAnchor:s}=this.props,{projection:a}=e;a&&(a.options.layoutAnchor=s,a.root.didUpdate(),hf.postRender(()=>{!a.currentAnimation&&a.isLead()&&this.safeToRemove()}))}componentWillUnmount(){const{visualElement:e,layoutGroup:s,switchLayoutGroup:a}=this.props,{projection:l}=e;Gh=!0,l&&(l.scheduleCheckAfterUnmount(),s&&s.group&&s.group.remove(l),a&&a.deregister&&a.deregister(l))}safeToRemove(){const{safeToRemove:e}=this.props;e&&e()}render(){return null}}function Bw(n){const[e,s]=Tw(),a=V.useContext(Id);return b.jsx(R2,{...n,layoutGroup:a,switchLayoutGroup:V.useContext(Ow),isPresent:e,safeToRemove:s})}const C2={pan:{Feature:A2},drag:{Feature:E2,ProjectionNode:Sw,MeasureLayout:Bw}};function J0(n,e,s){const{props:a}=n;n.animationState&&a.whileHover&&n.animationState.setActive("whileHover",s==="Start");const l="onHover"+s,u=a[l];u&&Pe.postRender(()=>u(e,Kr(e)))}class k2 extends Vi{mount(){const{current:e}=this.node;e&&(this.unmount=QT(e,(s,a)=>(J0(this.node,a,"Start"),l=>J0(this.node,l,"End"))))}unmount(){}}class j2 extends Vi{constructor(){super(...arguments),this.isActive=!1}onFocus(){let e=!1;try{e=this.node.current.matches(":focus-visible")}catch{e=!0}!e||!this.node.animationState||(this.node.animationState.setActive("whileFocus",!0),this.isActive=!0)}onBlur(){!this.isActive||!this.node.animationState||(this.node.animationState.setActive("whileFocus",!1),this.isActive=!1)}mount(){this.unmount=qr(Dr(this.node.current,"focus",()=>this.onFocus()),Dr(this.node.current,"blur",()=>this.onBlur()))}unmount(){}}function Q0(n,e,s){const{props:a}=n;if(n.current instanceof HTMLButtonElement&&n.current.disabled)return;n.animationState&&a.whileTap&&n.animationState.setActive("whileTap",s==="Start");const l="onTap"+(s==="End"?"":s),u=a[l];u&&Pe.postRender(()=>u(e,Kr(e)))}class O2 extends Vi{mount(){const{current:e}=this.node;if(!e)return;const{globalTapTarget:s,propagate:a}=this.node.props;this.unmount=iE(e,(l,u)=>(Q0(this.node,u,"Start"),(c,{success:d})=>Q0(this.node,c,d?"End":"Cancel")),{useGlobalTarget:s,stopPropagation:(a==null?void 0:a.tap)===!1})}unmount(){}}const Cd=new WeakMap,Kh=new WeakMap,N2=n=>{const e=Cd.get(n.target);e&&e(n)},D2=n=>{n.forEach(N2)};function M2({root:n,...e}){const s=n||document;Kh.has(s)||Kh.set(s,{});const a=Kh.get(s),l=JSON.stringify(e);return a[l]||(a[l]=new IntersectionObserver(D2,{root:n,...e})),a[l]}function U2(n,e,s){const a=M2(e);return Cd.set(n,s),a.observe(n),()=>{Cd.delete(n),a.unobserve(n)}}const B2={some:0,all:1};class L2 extends Vi{constructor(){super(...arguments),this.hasEnteredView=!1,this.isInView=!1}startObserver(){var m;(m=this.stopObserver)==null||m.call(this);const{viewport:e={}}=this.node.getProps(),{root:s,margin:a,amount:l="some",once:u}=e,c={root:s?s.current:void 0,rootMargin:a,threshold:typeof l=="number"?l:B2[l]},d=p=>{const{isIntersecting:g}=p;if(this.isInView===g||(this.isInView=g,u&&!g&&this.hasEnteredView))return;g&&(this.hasEnteredView=!0),this.node.animationState&&this.node.animationState.setActive("whileInView",g);const{onViewportEnter:v,onViewportLeave:w}=this.node.getProps(),x=g?v:w;x&&x(p)};this.stopObserver=U2(this.node.current,c,d)}mount(){this.startObserver()}update(){if(typeof IntersectionObserver>"u")return;const{props:e,prevProps:s}=this.node;["amount","margin","root"].some(z2(e,s))&&this.startObserver()}unmount(){var e;(e=this.stopObserver)==null||e.call(this),this.hasEnteredView=!1,this.isInView=!1}}function z2({viewport:n={}},{viewport:e={}}={}){return s=>n[s]!==e[s]}const V2={inView:{Feature:L2},tap:{Feature:O2},focus:{Feature:j2},hover:{Feature:k2}},P2={layout:{ProjectionNode:Sw,MeasureLayout:Bw}},H2={...c2,...V2,...C2,...P2},$2=a2(H2,r2),bn=$2;/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const q2=n=>n.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),I2=n=>n.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,s,a)=>a?a.toUpperCase():s.toLowerCase()),Z0=n=>{const e=I2(n);return e.charAt(0).toUpperCase()+e.slice(1)},Lw=(...n)=>n.filter((e,s,a)=>!!e&&e.trim()!==""&&a.indexOf(e)===s).join(" ").trim(),G2=n=>{for(const e in n)if(e.startsWith("aria-")||e==="role"||e==="title")return!0};/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */var K2={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const F2=V.forwardRef(({color:n="currentColor",size:e=24,strokeWidth:s=2,absoluteStrokeWidth:a,className:l="",children:u,iconNode:c,...d},m)=>V.createElement("svg",{ref:m,...K2,width:e,height:e,stroke:n,strokeWidth:a?Number(s)*24/Number(e):s,className:Lw("lucide",l),...!u&&!G2(d)&&{"aria-hidden":"true"},...d},[...c.map(([p,g])=>V.createElement(p,g)),...Array.isArray(u)?u:[u]]));/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const De=(n,e)=>{const s=V.forwardRef(({className:a,...l},u)=>V.createElement(F2,{ref:u,iconNode:e,className:Lw(`lucide-${q2(Z0(n))}`,`lucide-${n}`,a),...l}));return s.displayName=Z0(n),s};/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Y2=[["path",{d:"M8 3 4 7l4 4",key:"9rb6wj"}],["path",{d:"M4 7h16",key:"6tx8e3"}],["path",{d:"m16 21 4-4-4-4",key:"siv7j2"}],["path",{d:"M20 17H4",key:"h6l3hr"}]],X2=De("arrow-left-right",Y2);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const J2=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]],Q2=De("arrow-right",J2);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Z2=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2",key:"9lu3g6"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}],["path",{d:"M6 12h.01M18 12h.01",key:"113zkx"}]],W0=De("banknote",Z2);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const W2=[["path",{d:"M8 2v4",key:"1cmpym"}],["path",{d:"M16 2v4",key:"4m81vk"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2",key:"1hopcy"}],["path",{d:"M3 10h18",key:"8toen8"}],["path",{d:"M8 14h.01",key:"6423bh"}],["path",{d:"M12 14h.01",key:"1etili"}],["path",{d:"M16 14h.01",key:"1gbofw"}],["path",{d:"M8 18h.01",key:"lrp35t"}],["path",{d:"M12 18h.01",key:"mhygvu"}],["path",{d:"M16 18h.01",key:"kzsmim"}]],eR=De("calendar-days",W2);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const tR=[["path",{d:"M8 2v4",key:"1cmpym"}],["path",{d:"M16 2v4",key:"4m81vk"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2",key:"1hopcy"}],["path",{d:"M3 10h18",key:"8toen8"}]],nR=De("calendar",tR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const iR=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],sR=De("chevron-down",iR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const aR=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]],rR=De("circle-alert",aR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const oR=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],_f=De("circle-check",oR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const lR=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"10",r:"3",key:"ilqhr7"}],["path",{d:"M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662",key:"154egf"}]],uR=De("circle-user",lR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const cR=[["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54",key:"1djwo0"}],["path",{d:"M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17",key:"1tzkfa"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05",key:"14pb5j"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],zw=De("earth",cR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const hR=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],dR=De("external-link",hR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const fR=[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],mR=De("eye",fR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const pR=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M8 13h2",key:"yr2amv"}],["path",{d:"M14 13h2",key:"un5t4a"}],["path",{d:"M8 17h2",key:"2yhykz"}],["path",{d:"M14 17h2",key:"10kma7"}]],Ll=De("file-spreadsheet",pR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const gR=[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]],Fh=De("file-text",gR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const yR=[["path",{d:"M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z",key:"sc7q7i"}]],vR=De("funnel",yR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const bR=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]],wR=De("history",bR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const _R=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]],Vw=De("info",_R);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const xR=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]],kd=De("loader-circle",xR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const SR=[["circle",{cx:"12",cy:"16",r:"1",key:"1au0dj"}],["rect",{x:"3",y:"10",width:"18",height:"12",rx:"2",key:"6s8ecr"}],["path",{d:"M7 10V7a5 5 0 0 1 10 0v3",key:"1pqi11"}]],TR=De("lock-keyhole",SR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const ER=[["path",{d:"m16 17 5-5-5-5",key:"1bji2h"}],["path",{d:"M21 12H9",key:"dn1m92"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4",key:"1uf3rs"}]],AR=De("log-out",ER);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const RR=[["path",{d:"M4 5h16",key:"1tepv9"}],["path",{d:"M4 12h16",key:"1lakjw"}],["path",{d:"M4 19h16",key:"1djgab"}]],Pw=De("menu",RR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const CR=[["path",{d:"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z",key:"10ikf1"}]],kR=De("play",CR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const jR=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]],OR=De("plus",jR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const NR=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],DR=De("refresh-cw",NR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const MR=[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]],Hw=De("search",MR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const UR=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],ev=De("trash-2",UR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const BR=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]],xf=De("triangle-alert",BR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const LR=[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m17 8-5-5-5 5",key:"7q97r8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}]],tv=De("upload",LR);/** - * @license lucide-react v0.546.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const zR=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],Sf=De("x",zR),Mr=[{code:"GT",name:"Guatemala",flag:"/cruce-cuentas/gt-flag.png",currency:"GTQ",locale:"es-GT",enabled:!0},{code:"TT",name:"Trinidad y Tobago",flag:"/cruce-cuentas/tt-flag.png",currency:"TTD",locale:"en-TT",enabled:!0}];function nv(n){return Mr.find(e=>e.code===n)??Mr[0]}const VR="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png";function PR({currentView:n,onViewChange:e,onNewCruce:s,isSidebarOpen:a=!1,onCloseSidebar:l,user:u,isAuthEnabled:c=!1,onLogout:d,country:m,onCountryChange:p,onChooseAnotherCountry:g}){const[v,w]=V.useState(!1),x=V.useRef(null);return V.useEffect(()=>{function S(A){x.current&&!x.current.contains(A.target)&&w(!1)}return document.addEventListener("mousedown",S),()=>document.removeEventListener("mousedown",S)},[]),b.jsxs("aside",{className:`bg-neutral-100 h-screen w-64 fixed left-0 top-0 flex flex-col z-50 pb-6 min-h-screen font-sans transition-transform duration-300 md:translate-x-0 ${a?"translate-x-0":"-translate-x-full"}`,children:[b.jsx("div",{className:"absolute right-0 top-0 h-[80px] w-px bg-[#D0D0D0] pointer-events-none z-10 hidden md:block"}),b.jsx("div",{className:"absolute right-0 top-[84px] bottom-0 w-px bg-[#D0D0D0] pointer-events-none z-10 hidden md:block"}),b.jsxs("div",{className:"h-[84px] px-4 flex justify-between md:justify-center items-center bg-white border-b-4 border-[#6CC24A] relative",children:[b.jsx("img",{alt:"GomezLee Marketing logo",className:"h-10 object-contain selection:bg-transparent",src:VR}),b.jsx("button",{onClick:l,className:"md:hidden p-2 text-neutral-500 hover:text-neutral-800 transition-colors","aria-label":"Cerrar menú",children:b.jsx(Sf,{className:"w-5 h-5"})})]}),b.jsxs("div",{className:"px-4 pt-5",children:[b.jsxs("label",{className:"block",children:[b.jsxs("span",{className:"text-[9px] uppercase tracking-wider font-black text-neutral-400 flex items-center gap-1.5 mb-1.5",children:[b.jsx(zw,{className:"w-3.5 h-3.5"}),"País"]}),b.jsxs("div",{className:"relative",ref:x,children:[b.jsxs("button",{type:"button",onClick:()=>w(!v),className:"w-full border border-neutral-300 bg-white px-3 py-2 text-xs font-bold text-[#4F758B] focus:outline-none focus:border-[#4F758B] flex items-center justify-between transition-colors hover:bg-neutral-50",children:[b.jsxs("div",{className:"flex items-center gap-2",children:[b.jsx("img",{src:m.flag,alt:"",className:"w-5 h-3.5 object-cover shadow-sm border border-neutral-200"}),b.jsx("span",{children:m.name})]}),b.jsx(sR,{className:`w-4 h-4 text-[#4F758B] transition-transform ${v?"rotate-180":""}`})]}),v&&b.jsx("ul",{className:"absolute left-0 right-0 top-full mt-1 bg-white border border-neutral-300 shadow-lg z-50 py-1",children:Mr.map(S=>b.jsx("li",{children:b.jsxs("button",{type:"button",disabled:!S.enabled,onClick:()=>{p(S.code),w(!1)},className:`w-full text-left px-3 py-2.5 text-xs font-bold flex items-center gap-2.5 transition-colors ${S.enabled?"hover:bg-neutral-100 text-[#4F758B]":"opacity-60 cursor-not-allowed text-neutral-500 bg-neutral-50"}`,children:[b.jsx("img",{src:S.flag,alt:"",className:"w-6 h-4 object-cover shadow-sm border border-neutral-200 bg-white flex-shrink-0"}),b.jsxs("span",{className:"min-w-0 text-left leading-tight",children:[b.jsx("span",{className:"block",children:S.name}),!S.enabled&&b.jsx("span",{className:"block mt-1 text-[10px] text-neutral-400 font-medium italic",children:"Próximamente"})]})]})},S.code))})]})]}),b.jsx("button",{type:"button",onClick:g,className:"mt-2 text-[9px] font-bold uppercase tracking-wider text-neutral-400 hover:text-[#4F758B]",children:"Ver países disponibles"})]}),b.jsx("nav",{className:"flex-1 flex flex-col mt-5",children:b.jsxs("ul",{className:"space-y-1",children:[b.jsx("li",{children:b.jsxs("button",{onClick:()=>e("cruce"),className:`w-full flex items-center gap-3 px-6 py-3.5 transition-all duration-150 text-left font-bold text-sm tracking-wide ${n==="cruce"?"bg-[#4F758B] text-white":"text-neutral-600 hover:bg-neutral-200 hover:text-neutral-800"}`,children:[b.jsx(X2,{className:`w-4 h-4 ${n==="cruce"?"text-[#6CC24A]":""}`}),b.jsx("span",{children:"Cruce de Cuentas"})]})}),b.jsx("li",{children:b.jsxs("button",{onClick:()=>e("historial"),className:`w-full flex items-center gap-3 px-6 py-3.5 transition-all duration-150 text-left font-bold text-sm tracking-wide ${n==="historial"?"bg-[#4F758B] text-white":"text-neutral-600 hover:bg-neutral-200 hover:text-neutral-800"}`,children:[b.jsx(wR,{className:`w-4 h-4 ${n==="historial"?"text-[#6CC24A]":""}`}),b.jsx("span",{children:"Reportes Históricos"})]})})]})}),b.jsxs("div",{className:"px-4 space-y-3 pt-4 border-t border-[#D0D0D0] bg-neutral-100",children:[u&&b.jsxs("div",{className:"border border-neutral-200 bg-white px-3 py-3 flex items-center gap-2",children:[b.jsx(uR,{className:"w-4 h-4 text-[#4F758B] flex-shrink-0"}),b.jsxs("div",{className:"min-w-0",children:[b.jsx("p",{className:"text-[11px] font-black text-neutral-700 truncate",title:u.name,children:u.name}),b.jsx("p",{className:"text-[9px] text-neutral-400 truncate",title:u.email,children:u.email})]})]}),b.jsxs("button",{onClick:s,className:"w-full bg-[#6CC24A] hover:bg-[#5bb03c] text-white py-3 px-4 font-bold text-xs uppercase tracking-wider flex items-center justify-center gap-2 transition-all duration-150 shadow-sm active:scale-98",children:[b.jsx(OR,{className:"w-4 h-4"}),b.jsx("span",{children:"Nuevo Cruce"})]}),c&&d&&b.jsxs("button",{onClick:d,className:"w-full border border-neutral-300 bg-white hover:bg-neutral-50 text-neutral-600 py-2.5 px-4 font-bold text-[11px] uppercase tracking-wider flex items-center justify-center gap-2 transition-all duration-150",children:[b.jsx(AR,{className:"w-4 h-4"}),b.jsx("span",{children:"Cerrar sesión"})]})]})]})}const $w=[{value:1,label:"Enero"},{value:2,label:"Febrero"},{value:3,label:"Marzo"},{value:4,label:"Abril"},{value:5,label:"Mayo"},{value:6,label:"Junio"},{value:7,label:"Julio"},{value:8,label:"Agosto"},{value:9,label:"Septiembre"},{value:10,label:"Octubre"},{value:11,label:"Noviembre"},{value:12,label:"Diciembre"}],iv=[{value:"quincena_15",label:"Quincena 15",description:"Del día 1 al 15 del mes."},{value:"quincena_30",label:"Quincena 30 / fin de mes",description:"Del día 16 al último día del mes."}],HR="https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-analizar-nomina";function $R(n){return n==="TT"?"https://agenteit.digitalcompass.agency/webhook/nominatt-bamboo-test":"https://agenteit.digitalcompass.agency/webhook/nominagt-bamboo-test"}function qR(){const n=new Date().getFullYear(),e=n-5,s=n+10;return Array.from({length:s-e+1},(a,l)=>e+l)}function IR(){const n=new Date,e=n.getDate();let s=n.getFullYear(),a=n.getMonth()+1,l="quincena_15";return e<=15&&(a-=1,l="quincena_30",a===0&&(a=12,s-=1)),{year:s,month:a,periodType:l}}function qw(n){return n<1024*1024?`${(n/1024).toFixed(1)} KB`:`${(n/(1024*1024)).toFixed(2)} MB`}function sv(n){const e=n.name.toLowerCase();return e.endsWith(".xlsx")||e.endsWith(".xls")}function av(n){return n.name.toLowerCase().endsWith(".csv")}function rv(n){return Array.from(n).map(e=>({name:e.name,size:qw(e.size),type:"bank",rawFile:e}))}function bl(n,e,s){return`${n}-${String(e).padStart(2,"0")}-${String(s).padStart(2,"0")}`}function ov(n,e,s){var u;const a=new Date(n,e,0).getDate(),l=((u=$w.find(c=>c.value===e))==null?void 0:u.label)??"Mes";return s==="quincena_15"?{start:bl(n,e,1),end:bl(n,e,15),label:`${l} ${n} · Quincena 15`}:{start:bl(n,e,16),end:bl(n,e,a),label:`${l} ${n} · Quincena 30 / fin de mes`}}function Yh(n,e){return n==null||Number.isNaN(n)?"—":new Intl.NumberFormat(e.locale,{style:"currency",currency:e.currency,minimumFractionDigits:2}).format(n)}function Cn(n,e=0){const s=Number(n);return Number.isFinite(s)?s:e}function At(n){return n.normalize("NFD").replace(/[\u0300-\u036f]/g,"").toLowerCase()}function GR(n,e){const s=At(String(n??e??""));return s.includes("coincid")?"Coincidencia":s.includes("resuelto")?"Resuelto":s.includes("no encontrado")?"No encontrado":s.includes("pendiente")?"Pendiente revisión":s.includes("riesgo")||s.includes("discrep")||s.includes("mal digitada")?"Riesgo":e==="coincidencia"?"Coincidencia":e==="posible_cuenta_mal_digitada"||e==="discrepancia"?"Riesgo":"Pendiente revisión"}function Xh(n,e){for(const s of e){if(typeof n[s]=="number")return n[s];if(typeof n[s]=="string"&&n[s]!==""){const a=n[s].replace(/[Q,$\s]/g,"").replace(/,/g,""),l=Number(a);if(Number.isFinite(l))return l}}return null}function lv(n){return Array.isArray(n)?n.map((e,s)=>{const a=e,l=String(a.category??a.categoria??a.tipo??a.source??a.fuente??""),u=Xh(a,["amountPayroll","monto_nomina","payroll_amount","amount_payroll","montoNomina"]),c=Xh(a,["amountBank","monto_banco","bank_amount","amount_bank","montoBanco"]),d=Xh(a,["difference","diferencia","diff"]);return{id:String(a.id??a.employeeNumber??a.employee_number??a.account??a.cuenta??`row-${s+1}`),employee:String(a.employee??a.empleado??a.name??a.nombre??a.employee_name??"Sin nombre"),employeeNumber:String(a.employeeNumber??a.employee_number??a.codigo??""),account:a.account||a.cuenta||a.bank_account?String(a.account??a.cuenta??a.bank_account):void 0,amountPayroll:u,amountBank:c,difference:d??(u!==null&&c!==null?u-c:null),status:GR(a.status??a.estado,l),category:l,observation:String(a.observation??a.observacion??a.note??a.nota??a.comentario??""),source:a.source??a.fuente??l}}):[]}function KR(n){const s=(Array.isArray(n)?n[0]:n)??{},a=s.originalResponse??s.original_response??s.response??s,l=a.summary??a.resumen??s.summary??s.resumen??{},u=lv(a.rows??s.rows??a.resultados??s.resultados??[]),c=lv(a.bankWithoutBamboo??s.bankWithoutBamboo??a.banco_sin_bamboo_rows??s.banco_sin_bamboo_rows??[]).map(C=>({...C,category:C.category||"banco_sin_bamboo",source:"banco_sin_bamboo",status:C.status==="Coincidencia"?"Pendiente revisión":C.status})),d=new Map;[...u,...c].forEach((C,N)=>{const O=C.id||`${C.source??C.category??"row"}-${N}`;d.set(O,C)});const m=Array.from(d.values()),p=Cn(l.matches??l.coincidencias,u.filter(C=>C.status==="Coincidencia").length),g=Cn(l.discrepancies??l.discrepancias,u.filter(C=>{const N=At(String(C.category??C.source??""));return N.includes("discrepancia")||N.includes("posible_cuenta_mal_digitada")}).length),v=Cn(l.bankWithoutPayroll??l.bancoSinNomina??l.banco_sin_nomina,u.filter(C=>vr(C,"banco_sin_nomina")).length),w=Cn(l.bankWithoutBamboo??l.bancoSinBamboo??l.banco_sin_bamboo,c.length),x=Cn(l.payrollWithoutAccount??l.nominaSinCuenta??l.nomina_sin_cuenta),S=Cn(l.nameDifferences??l.diferenciasNombreBanco??l.diferencias_nombre_banco,u.filter(C=>At(String(C.category??"")).includes("diferencia_nombre_banco")).length),A=Cn(l.possibleWrongAccounts??l.posiblesCuentasMalDigitadas??l.posibles_cuentas_mal_digitadas,u.filter(C=>At(String(C.category??"")).includes("posible_cuenta_mal_digitada")).length),k=g+v+w+x+S;return{ok:!!(a.ok??s.ok??!0),executionId:a.executionId||a.execution_id||s.executionId||s.execution_id?String(a.executionId??a.execution_id??s.executionId??s.execution_id):void 0,reportUrl:a.reportUrl||a.report_url||s.reportUrl||s.report_url||s.google_sheet_url?String(a.reportUrl??a.report_url??s.reportUrl??s.report_url??s.google_sheet_url):void 0,summary:{matches:p,discrepancies:g,bankWithoutPayroll:v,bankWithoutBamboo:w,payrollWithoutAccount:x,nameDifferences:S,possibleWrongAccounts:A,totalPending:k,totalRows:m.length,totalPayroll:Cn(l.totalNomina??l.total_nomina,void 0),totalBank:Cn(l.totalBanco??l.total_banco,void 0),totalDifference:Cn(l.diferenciaTotal??l.diferencia_total,void 0)},rows:m,message:a.message||s.message?String(a.message??s.message):void 0}}function FR(n){return n==="Coincidencia"?"bg-emerald-50 text-emerald-800 border-emerald-200":n==="Resuelto"?"bg-blue-50 text-blue-800 border-blue-200":n==="No encontrado"?"bg-amber-50 text-amber-800 border-amber-200":n==="Pendiente revisión"?"bg-neutral-100 text-neutral-700 border-neutral-300":"bg-red-50 text-[#ba1a1a] border-red-200"}function uv(n){const e=At(String(n.category??n.source??"")),s=At(n.observation??"");return n.status==="Coincidencia"||e.includes("coincidencia")?90:e.includes("posible_cuenta_mal_digitada")||s.includes("mal digitada")?1:e.includes("discrepancia")||n.status==="Riesgo"?2:e.includes("banco_sin_bamboo")?3:e.includes("banco_sin_nomina")?4:e.includes("diferencia_nombre_banco")?5:e.includes("nomina_sin_cuenta")?6:n.status==="Pendiente revisión"?7:n.status==="No encontrado"?10:20}function YR(n){return[...n].sort((e,s)=>{const a=uv(e)-uv(s);return a!==0?a:e.employee.localeCompare(s.employee,"es")})}function XR(n){const e=At(String(n.category??n.source??""));return e.includes("discrepancia")||e.includes("posible_cuenta_mal_digitada")}function vr(n,e){return n.source===e||At(String(n.category??"")).includes(e)}function JR(n){const e=At(String(n??""));return e?e.includes("posible_cuenta_mal_digitada")?"Posible cuenta mal digitada":e.includes("banco_sin_bamboo")?"Banco sin Bamboo":e.includes("banco_sin_nomina")?"Banco sin nómina":e.includes("diferencia_nombre_banco")?"Diferencia nombre banco":e.includes("nomina_sin_cuenta")?"Nómina sin cuenta":e.includes("discrepancia")?"Discrepancia":e.includes("coincidencia")?"Coincidencia":String(n).replace(/_/g," "):"—"}function QR(n){const e=n instanceof Error?n.message:"";return e.includes("planillas del banco repetidas")?e:"No se pudo procesar el cruce. Verifica los archivos e intenta nuevamente."}function ZR(n){try{return new TextDecoder("windows-1252").decode(n)}catch{return new TextDecoder("utf-8").decode(n)}}function cv(n,e){let s=0,a=!1;for(let l=0;lcv(n,a)>cv(n,s)?a:s)}function hv(n,e=Iw(n)){const s=[];let a="",l=!1;for(let u=0;uu.replace(/^\uFEFF/,"").trim())}function WR(n){const e=String(n??"").replace(/\D/g,"");return/^\d{1,20}$/.test(e)?e:null}function eC(n){const e=n.split(/\r?\n/).slice(0,120);for(const s of e.slice(0,25)){const l=At(s).match(/(?:consulta\s+(?:del\s+)?)?detalle\s+(?:del\s+)?envio\s+(\d{1,20})/);if(l!=null&&l[1])return l[1]}for(let s=0;sAt(m).trim()).findIndex(m=>m==="numero de envio"||m.includes("numero de envio"));if(!(d<0))for(let m=s+1;ms.length>1).map(([s,a])=>({shipmentNumber:s,fileNames:a}))}const iC={1:["enero","january","jan"],2:["febrero","february","feb"],3:["marzo","march","mar"],4:["abril","april","apr"],5:["mayo","may"],6:["junio","june","jun"],7:["julio","july","jul"],8:["agosto","august","aug"],9:["septiembre","setiembre","september","sep"],10:["octubre","october","oct"],11:["noviembre","november","nov"],12:["diciembre","december","dec"]};function sC(n){const e=At(n).replace(/[_\-.()]+/g," ").replace(/\s+/g," ").trim(),s=e.match(/\b(20\d{2})\b/),a=s?Number(s[1]):void 0;let l;for(const[c,d]of Object.entries(iC))if(d.some(m=>new RegExp(`\\b${m}\\b`,"i").test(e))){l=Number(c);break}let u;return/\b(?:1q|q1|quincena\s*15|primera\s+quincena)\b/.test(e)?u="quincena_15":/\b(?:2q|q2|quincena\s*30|segunda\s+quincena|fin\s+de\s+mes)\b/.test(e)?u="quincena_30":/\b15\b/.test(e)?u="quincena_15":/\b(?:30|31)\b/.test(e)&&(u="quincena_30"),{year:a,month:l,periodType:u}}function dv(n){const e=IR();try{const s=window.localStorage.getItem(`glm_cruce_period_${n}`);if(!s)return e;const a=JSON.parse(s);if(Number.isInteger(a.year)&&Number.isInteger(a.month)&&a.month>=1&&a.month<=12&&(a.periodType==="quincena_15"||a.periodType==="quincena_30"))return a}catch{}return e}async function aC(n,e){const s=sC(n);try{const a=await fetch(HR,{method:"POST",headers:{"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify({fileName:n,country:e.code,countryName:e.name})});if(!a.ok)return s;const l=await a.json().catch(()=>null),u=(l==null?void 0:l.detected)??(l==null?void 0:l.result)??(l==null?void 0:l.analysis)??l??{},c=String(u.periodType??u.period_type??""),d=Number(u.year)||s.year,m=Number(u.month)||s.month,p=c==="quincena_15"||c==="quincena_30"?c:s.periodType;return{year:d,month:m,periodType:p,confidence:Number.isFinite(Number(u.confidence))?Number(u.confidence):void 0,requiresManualReview:!!(u.requiresManualReview??u.requires_manual_review??!1),source:String(u.source??"gemini")}}catch{return{...s,source:"local_fallback",requiresManualReview:!(Number.isInteger(s.year)&&Number.isInteger(s.month)&&(s.periodType==="quincena_15"||s.periodType==="quincena_30"))}}}function rC({onShowToast:n,uploadedPayroll:e,setUploadedPayroll:s,uploadedBankFiles:a,setUploadedBankFiles:l,isCruceExecuted:u,setIsCruceExecuted:c,response:d,setResponse:m,onOpenSidebar:p,operatorName:g,operatorEmail:v,resetToken:w=0,country:x}){const S=V.useMemo(()=>dv(x.code),[x.code]),[A,k]=V.useState(!1),[C,N]=V.useState(""),[O,P]=V.useState("todos"),[J,X]=V.useState(S.year),[G,Z]=V.useState(S.month),[ee,ne]=V.useState(S.periodType),[le,de]=V.useState(1),[je,Re]=V.useState(""),ke=`glm_cruce_period_${x.code}`,L=V.useRef(null),Q=V.useRef(null),Y=V.useMemo(()=>qR(),[]),I=V.useMemo(()=>ov(J,G,ee),[J,G,ee]),E=V.useMemo(()=>YR((d==null?void 0:d.rows)??[]),[d==null?void 0:d.rows]).filter(F=>{const ge=At(je.trim());return At(`${F.employee} ${F.employeeNumber} ${F.account??""} ${F.observation} ${F.category??""} ${F.source??""}`).includes(ge)?O==="discrepancias"?XR(F):O==="coincidencias"?F.status==="Coincidencia":O==="banco_sin_nomina"?vr(F,"banco_sin_nomina"):O==="banco_sin_bamboo"?vr(F,"banco_sin_bamboo"):O==="diferencia_nombre_banco"?vr(F,"diferencia_nombre_banco"):O==="nomina_sin_cuenta"?vr(F,"nomina_sin_cuenta"):!0:!1}),B=10,K=Math.max(1,Math.ceil(E.length/B)),W=E.slice((le-1)*B,le*B);V.useEffect(()=>{le>K&&de(K)},[le,K]);const he=V.useMemo(()=>{var F;return((F=iv.find(ge=>ge.value===ee))==null?void 0:F.description)??""},[ee]);V.useEffect(()=>{const F=dv(x.code);X(F.year),Z(F.month),ne(F.periodType),de(1),Re(""),P("todos")},[x.code]),V.useEffect(()=>{window.localStorage.setItem(ke,JSON.stringify({year:J,month:G,periodType:ee}))},[ke,J,G,ee]),V.useEffect(()=>{Re(""),P("todos"),de(1),N(""),k(!1),L.current&&(L.current.value=""),Q.current&&(Q.current.value="")},[w]);const ue=()=>{c(!1),m(null),de(1),Re(""),P("todos")},_e=async F=>{s({name:F.name,size:qw(F.size),type:"payroll",rawFile:F}),ue();const ge=await aC(F.name,x);if(Number.isInteger(ge.year)&&Number.isInteger(ge.month)&&(ge.periodType==="quincena_15"||ge.periodType==="quincena_30")){X(ge.year),Z(ge.month),ne(ge.periodType);const Bt=ov(ge.year,ge.month,ge.periodType);ge.requiresManualReview?n(`Nómina cargada. Período sugerido: ${Bt.label}. Confirma los datos antes de ejecutar.`,"info"):n(`Nómina cargada. Período detectado: ${Bt.label}.`,"success");return}n("Archivo de nómina cargado con éxito.","success")},qe=F=>{F.preventDefault()},He=async F=>{F.preventDefault();const ge=F.dataTransfer.files[0];ge&&(sv(ge)?await _e(ge):n("Formato inválido. Por favor carga un archivo Excel (.xlsx, .xls).","error"))},_n=F=>{F.preventDefault();const Fe=Array.from(F.dataTransfer.files).filter(av);if(Fe.length===0){n("Formato inválido. Por favor carga uno o varios archivos CSV (.csv).","error");return}l([...a,...rv(Fe)]),ue(),n(`${Fe.length} planilla(s) del banco cargada(s) con éxito.`,"success")},wt=async F=>{var Fe;const ge=(Fe=F.target.files)==null?void 0:Fe[0];ge&&(sv(ge)?await _e(ge):n("Formato inválido. Por favor carga un archivo Excel (.xlsx, .xls).","error"))},Yt=F=>{const ge=F.target.files;if(!ge||ge.length===0)return;const Fe=Array.from(ge).filter(av);if(Fe.length===0){n("Formato inválido. Por favor carga uno o varios archivos CSV (.csv).","error");return}l([...a,...rv(Fe)]),ue(),n(`${Fe.length} planilla(s) del banco cargada(s) con éxito.`,"success")},oi=F=>{l(a.filter((ge,Fe)=>Fe!==F)),Q.current&&(Q.current.value=""),ue(),n("Planilla de banco removida.","info")},dt=async()=>{if(!A){if(!J||!G||!ee){n("Debes seleccionar año, mes y tipo de cruce antes de ejecutar.","error");return}if(!(e!=null&&e.rawFile)){n("Debes cargar la nómina antes de ejecutar el cruce.","error");return}if(a.length===0||a.some(F=>!F.rawFile)){n("Debes cargar al menos una planilla del banco antes de ejecutar el cruce.","error");return}k(!0),N("Validando archivos..."),de(1);try{const F=await nC(a);if(F.length>0){const Me=F.map(Sn=>`Número de envío ${Sn.shipmentNumber}: ${Sn.fileNames.join(", ")}`).join(` -`);throw new Error(`Hay planillas del banco repetidas. Revisa los archivos cargados antes de continuar. -${Me}`)}const ge=$R(x.code);if(!ge)throw new Error("No se pudo procesar el cruce. El servicio de conciliación no está disponible.");const Fe={country:x.code,country_name:x.name,year:J,month:G,period_type:ee,period_label:I.label,period_start:I.start,period_end:I.end,payroll_file_name:e.name,bank_file_names:a.map(Me=>Me.name),requested_by_name:g,requested_by_email:v,auth_mode:"supabase_google",source_app:"portal-cruce-cuentas-glm"};N("Procesando cruce de cuentas...");const Bt=new FormData;Bt.append("metadata",JSON.stringify(Fe)),Object.entries(Fe).forEach(([Me,Sn])=>{Bt.append(Me,Array.isArray(Sn)?JSON.stringify(Sn):String(Sn))}),Bt.append("payroll_file",e.rawFile,e.name),a.forEach((Me,Sn)=>{Me.rawFile&&(Bt.append("bank_files",Me.rawFile,Me.name),Bt.append(`bank_file_${Sn+1}`,Me.rawFile,Me.name))});const Ln=await fetch(ge,{method:"POST",body:Bt});N("Generando reporte...");const xn=(Ln.headers.get("content-type")??"").includes("application/json")?await Ln.json():{ok:Ln.ok,message:await Ln.text()};if(!Ln.ok||(xn==null?void 0:xn.ok)===!1)throw new Error(typeof(xn==null?void 0:xn.message)=="string"?xn.message:"No se pudo procesar el cruce. Verifica los archivos e intenta nuevamente.");const Xr=KR(xn);m(Xr),c(!0),n(Xr.message||"Cruce procesado correctamente.","success"),setTimeout(()=>{const Me=document.getElementById("resultados-seccion");Me&&Me.scrollIntoView({behavior:"smooth",block:"start"})},150)}catch(F){const ge=QR(F);c(!1),m(null),n(ge,"error")}finally{k(!1),N("")}}},Pi=()=>{if(d!=null&&d.reportUrl){window.open(d.reportUrl,"_blank","noopener,noreferrer");return}n("El reporte aún no tiene un enlace disponible.","info")};return b.jsxs("div",{className:"flex-1 flex flex-col min-h-screen bg-neutral-50 text-neutral-800 font-sans",children:[b.jsxs("header",{className:"bg-white border-b-4 border-[#6CC24A] flex justify-between items-center w-full px-4 sm:px-8 h-[84px] sticky top-0 z-40 shadow-sm gap-x-4",children:[b.jsxs("div",{className:"flex items-center gap-2 sm:gap-3 min-w-0",children:[b.jsx("button",{onClick:p,className:"md:hidden p-2 -ml-2 text-neutral-600 hover:text-neutral-800 hover:bg-neutral-100 transition-colors flex-shrink-0","aria-label":"Abrir menú",children:b.jsx(Pw,{className:"w-6 h-6"})}),b.jsx("h1",{className:"text-xs sm:text-sm md:text-lg lg:text-2xl font-black text-[#4F758B] tracking-tight uppercase flex items-center gap-1.5 sm:gap-2 min-w-0",children:b.jsxs("span",{className:"truncate",children:["Cruce de Cuentas GLM - ",x.name]})})]}),b.jsx("div",{className:"flex-shrink-0 flex items-center","aria-label":`Bandera de ${x.name}`,children:b.jsx("div",{className:"border border-neutral-200 bg-white px-2.5 py-2 shadow-sm",children:b.jsx("img",{src:x.flag,alt:`Bandera de ${x.name}`,className:"h-5 sm:h-6 w-auto object-contain"})})})]}),b.jsxs("main",{className:"flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 py-8 flex flex-col gap-8",children:[b.jsxs("section",{className:"bg-white border border-[#D0D0D0] p-5 shadow-sm",children:[b.jsx("div",{className:"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-5",children:b.jsxs("div",{className:"flex items-center gap-3",children:[b.jsx("div",{className:"bg-[#4F758B]/10 p-2 rounded-full",children:b.jsx(Vw,{className:"w-5 h-5 text-[#4F758B]"})}),b.jsx("div",{className:"pt-0.5",children:b.jsx("h2",{className:"text-sm font-black text-[#4F758B] uppercase tracking-wider leading-none",children:"Datos del cruce"})})]})}),b.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-4",children:[b.jsxs("label",{className:"flex flex-col gap-1.5",children:[b.jsx("span",{className:"text-[10px] font-black uppercase tracking-wider text-neutral-500",children:"Año"}),b.jsx("select",{value:J,onChange:F=>{X(Number(F.target.value)),ue()},className:"border border-[#D0D0D0] bg-white px-3 py-2.5 text-sm font-bold text-neutral-700 focus:outline-none focus:border-[#4F758B]",children:Y.map(F=>b.jsx("option",{value:F,children:F},F))})]}),b.jsxs("label",{className:"flex flex-col gap-1.5",children:[b.jsx("span",{className:"text-[10px] font-black uppercase tracking-wider text-neutral-500",children:"Mes"}),b.jsx("select",{value:G,onChange:F=>{Z(Number(F.target.value)),ue()},className:"border border-[#D0D0D0] bg-white px-3 py-2.5 text-sm font-bold text-neutral-700 focus:outline-none focus:border-[#4F758B]",children:$w.map(F=>b.jsx("option",{value:F.value,children:F.label},F.value))})]}),b.jsxs("label",{className:"flex flex-col gap-1.5",children:[b.jsx("span",{className:"text-[10px] font-black uppercase tracking-wider text-neutral-500",children:"Tipo de cruce"}),b.jsx("select",{value:ee,onChange:F=>{ne(F.target.value),ue()},className:"border border-[#D0D0D0] bg-white px-3 py-2.5 text-sm font-bold text-neutral-700 focus:outline-none focus:border-[#4F758B]",children:iv.map(F=>b.jsx("option",{value:F.value,children:F.label},F.value))})]})]}),b.jsxs("div",{className:"mt-4 grid grid-cols-1 md:grid-cols-[1fr_auto] gap-3 items-center bg-[#4F758B]/5 border border-[#4F758B]/10 px-4 py-3",children:[b.jsxs("div",{className:"flex items-start gap-2 text-xs text-neutral-600",children:[b.jsx(eR,{className:"w-4 h-4 text-[#4F758B] mt-0.5 flex-shrink-0"}),b.jsxs("div",{children:[b.jsx("span",{className:"font-black text-[#4F758B]",children:"Período calculado:"})," ",I.start," al ",I.end,b.jsx("p",{className:"text-[10px] text-neutral-500 mt-0.5",children:he})]})]}),b.jsx("div",{className:"text-[10px] font-bold text-neutral-500 uppercase tracking-wider",children:I.label})]})]}),b.jsxs("section",{className:"grid grid-cols-1 lg:grid-cols-2 gap-6",children:[b.jsxs("div",{className:"bg-white border border-[#D0D0D0] p-6 flex flex-col justify-between relative shadow-sm hover:shadow-md transition-shadow",children:[b.jsx("div",{className:"absolute top-0 left-0 w-full h-1 bg-[#4F758B]"}),b.jsxs("div",{children:[b.jsxs("div",{className:"flex items-center gap-2.5 mb-4",children:[b.jsx(Ll,{className:"w-5 h-5 text-[#4F758B]"}),b.jsx("h2",{className:"text-sm font-black text-neutral-800 uppercase tracking-wider",children:"1. Carga de Nómina"})]}),b.jsx("input",{type:"file",ref:L,onChange:wt,accept:".xlsx,.xls",className:"hidden"}),e?b.jsxs("div",{className:"border border-[#6CC24A]/30 bg-[#6CC24A]/5 p-5 flex items-center justify-between gap-3",children:[b.jsxs("div",{className:"flex items-center gap-3 min-w-0",children:[b.jsx(Ll,{className:"w-8 h-8 text-[#6CC24A] flex-shrink-0"}),b.jsxs("div",{className:"overflow-hidden",children:[b.jsx("p",{className:"text-xs font-bold text-neutral-700 truncate max-w-[300px]",title:e.name,children:e.name}),b.jsxs("p",{className:"text-[10px] text-neutral-400 font-mono mt-0.5",children:[e.size," • Excel"]})]})]}),b.jsx("button",{onClick:()=>{s(null),L.current&&(L.current.value=""),ue(),n("Nómina removida.","info")},className:"p-1.5 text-neutral-400 hover:text-red-500 hover:bg-neutral-100 transition-all rounded","aria-label":"Remover nómina",children:b.jsx(ev,{className:"w-4.5 h-4.5"})})]}):b.jsxs("div",{onDragOver:qe,onDrop:He,onClick:()=>{var F;return(F=L.current)==null?void 0:F.click()},className:"border-2 border-dashed border-[#D0D0D0] bg-neutral-50 hover:bg-[#4F758B]/5 hover:border-[#4F758B]/40 transition-colors flex flex-col items-center justify-center py-10 px-4 text-center cursor-pointer min-h-[190px]",children:[b.jsx(tv,{className:"w-8 h-8 text-neutral-300 mb-2"}),b.jsx("p",{className:"text-xs font-bold text-neutral-700",children:"Arrastre y suelte archivo Excel"}),b.jsx("p",{className:"text-[10px] text-neutral-400 mt-1 font-medium",children:"Formato .xlsx, .xls"}),b.jsx("button",{type:"button",className:"mt-4 border border-[#D0D0D0] bg-white px-4 py-1.5 text-[10px] font-bold text-neutral-600 hover:border-[#4F758B] hover:text-[#4F758B] transition-colors shadow-sm uppercase tracking-wide",children:"Seleccionar Archivo"})]})]})]}),b.jsxs("div",{className:"bg-white border border-[#D0D0D0] p-6 flex flex-col justify-between relative shadow-sm hover:shadow-md transition-shadow",children:[b.jsx("div",{className:"absolute top-0 left-0 w-full h-1 bg-[#4F758B]"}),b.jsxs("div",{children:[b.jsxs("div",{className:"flex items-center gap-2.5 mb-2",children:[b.jsx(Fh,{className:"w-5 h-5 text-[#4F758B]"}),b.jsx("h2",{className:"text-sm font-black text-neutral-800 uppercase tracking-wider",children:"2. Carga de Planillas del Banco"})]}),b.jsx("p",{className:"text-[11px] text-neutral-500 mb-4 leading-relaxed",children:"Puede subir múltiples archivos separados o un archivo consolidado del período."}),b.jsx("input",{type:"file",ref:Q,onChange:Yt,accept:".csv",multiple:!0,className:"hidden"}),b.jsxs("div",{onDragOver:qe,onDrop:_n,onClick:()=>{var F;return(F=Q.current)==null?void 0:F.click()},className:"border-2 border-dashed border-[#D0D0D0] bg-neutral-50 hover:bg-[#4F758B]/5 hover:border-[#4F758B]/40 transition-colors flex flex-col items-center justify-center py-8 px-4 text-center cursor-pointer min-h-[130px]",children:[b.jsx(tv,{className:"w-7 h-7 text-neutral-300 mb-2"}),b.jsx("p",{className:"text-xs font-bold text-neutral-700",children:"Arrastre y suelte archivo CSV"}),b.jsx("p",{className:"text-[10px] text-neutral-400 mt-1 font-medium",children:"Formato .csv"}),b.jsx("button",{type:"button",className:"mt-4 border border-[#D0D0D0] bg-white px-4 py-1.5 text-[10px] font-bold text-neutral-600 hover:border-[#4F758B] hover:text-[#4F758B] transition-colors shadow-sm uppercase tracking-wide",children:"Seleccionar Archivos"})]}),a.length>0&&b.jsx("div",{className:"mt-4 space-y-2 max-h-[140px] overflow-y-auto pr-1",children:a.map((F,ge)=>b.jsxs("div",{className:"border border-neutral-200 bg-neutral-50 px-3 py-2 flex items-center justify-between gap-3",children:[b.jsxs("div",{className:"flex items-center gap-2 min-w-0",children:[b.jsx(Fh,{className:"w-4 h-4 text-[#4F758B] flex-shrink-0"}),b.jsxs("div",{className:"min-w-0",children:[b.jsx("p",{className:"text-[11px] font-bold text-neutral-700 truncate max-w-[260px]",title:F.name,children:F.name}),b.jsxs("p",{className:"text-[9px] text-neutral-400 font-mono",children:[F.size," • CSV"]})]})]}),b.jsx("button",{onClick:Fe=>{Fe.stopPropagation(),oi(ge)},className:"p-1 text-neutral-400 hover:text-red-500 hover:bg-white rounded transition-colors","aria-label":"Remover planilla de banco",children:b.jsx(ev,{className:"w-3.5 h-3.5"})})]},`${F.name}-${ge}`))})]})]})]}),b.jsx("section",{className:"flex flex-col items-center gap-3",children:b.jsx("button",{onClick:dt,disabled:A,className:"w-full sm:w-auto min-w-[280px] bg-[#4F758B] hover:bg-[#41677b] disabled:bg-neutral-300 disabled:cursor-not-allowed text-white px-8 py-4 font-black text-sm uppercase tracking-wider shadow-md flex items-center justify-center gap-3 transition-all duration-150 active:scale-[0.99] border-l-4 border-[#6CC24A]",children:A?b.jsxs(b.Fragment,{children:[b.jsx(kd,{className:"w-5 h-5 animate-spin"}),b.jsx("span",{children:C||"Procesando cruce de cuentas..."})]}):b.jsxs(b.Fragment,{children:[b.jsx(kR,{className:"w-5 h-5"}),b.jsx("span",{children:"Ejecutar Cruce de Cuentas"})]})})}),b.jsx(cs,{children:u&&d&&b.jsxs(bn.section,{id:"resultados-seccion",initial:{opacity:0,y:20},animate:{opacity:1,y:0},exit:{opacity:0,y:10},className:"flex flex-col gap-5 scroll-mt-28",children:[b.jsxs("div",{className:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4",children:[b.jsxs("div",{className:"bg-white border border-emerald-200 p-4 shadow-sm",children:[b.jsxs("div",{className:"flex items-center gap-2 text-emerald-700",children:[b.jsx(_f,{className:"w-5 h-5"}),b.jsx("span",{className:"text-[10px] uppercase tracking-wider font-black",children:"Coincidencias"})]}),b.jsx("p",{className:"text-3xl font-black text-emerald-800 mt-2",children:d.summary.matches})]}),b.jsxs("div",{className:"bg-white border border-red-200 p-4 shadow-sm",children:[b.jsxs("div",{className:"flex items-center gap-2 text-[#ba1a1a]",children:[b.jsx(xf,{className:"w-5 h-5"}),b.jsx("span",{className:"text-[10px] uppercase tracking-wider font-black",children:"Discrepancias"})]}),b.jsx("p",{className:"text-3xl font-black text-[#ba1a1a] mt-2",children:d.summary.discrepancies})]}),b.jsxs("div",{className:"bg-white border border-amber-200 p-4 shadow-sm",children:[b.jsxs("div",{className:"flex items-center gap-2 text-amber-800",children:[b.jsx(W0,{className:"w-5 h-5"}),b.jsx("span",{className:"text-[10px] uppercase tracking-wider font-black",children:"Banco sin nómina"})]}),b.jsx("p",{className:"text-3xl font-black text-amber-800 mt-2",children:d.summary.bankWithoutPayroll})]}),b.jsxs("div",{className:"bg-white border border-orange-200 p-4 shadow-sm",children:[b.jsxs("div",{className:"flex items-center gap-2 text-orange-800",children:[b.jsx(W0,{className:"w-5 h-5"}),b.jsx("span",{className:"text-[10px] uppercase tracking-wider font-black",children:"Banco sin Bamboo"})]}),b.jsx("p",{className:"text-3xl font-black text-orange-800 mt-2",children:d.summary.bankWithoutBamboo})]}),b.jsxs("div",{className:"bg-white border border-violet-200 p-4 shadow-sm",children:[b.jsxs("div",{className:"flex items-center gap-2 text-violet-800",children:[b.jsx(Fh,{className:"w-5 h-5"}),b.jsx("span",{className:"text-[10px] uppercase tracking-wider font-black",children:"Diferencias nombre banco"})]}),b.jsx("p",{className:"text-3xl font-black text-violet-800 mt-2",children:d.summary.nameDifferences})]}),b.jsxs("div",{className:"bg-white border border-blue-200 p-4 shadow-sm",children:[b.jsxs("div",{className:"flex items-center gap-2 text-blue-800",children:[b.jsx(Ll,{className:"w-5 h-5"}),b.jsx("span",{className:"text-[10px] uppercase tracking-wider font-black",children:"Nómina sin cuenta"})]}),b.jsx("p",{className:"text-3xl font-black text-blue-800 mt-2",children:d.summary.payrollWithoutAccount})]})]}),b.jsxs("div",{className:"bg-white border border-[#D0D0D0] shadow-sm overflow-hidden",children:[b.jsxs("div",{className:"p-5 border-b border-neutral-200 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4",children:[b.jsxs("div",{children:[b.jsx("h2",{className:"text-lg font-black text-[#4F758B] uppercase tracking-tight",children:"Resultados del Cruce"}),b.jsx("p",{className:"text-[11px] text-neutral-500 mt-1",children:I.label})]}),b.jsx("div",{className:"flex flex-col sm:flex-row gap-2",children:b.jsxs("button",{onClick:Pi,disabled:!d.reportUrl,className:"inline-flex items-center justify-center gap-2 border border-[#4F758B] text-[#4F758B] hover:bg-[#4F758B] hover:text-white px-4 py-2 text-xs font-bold transition-colors disabled:opacity-50 disabled:cursor-not-allowed",children:[b.jsx(dR,{className:"w-4 h-4"}),"Abrir Google Sheet"]})})]}),b.jsxs("div",{className:"p-4 border-b border-neutral-200 bg-neutral-50 flex flex-col lg:flex-row gap-3 lg:items-center lg:justify-between",children:[b.jsxs("div",{className:"relative w-full lg:max-w-sm",children:[b.jsx(Hw,{className:"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-400"}),b.jsx("input",{type:"text",value:je,onChange:F=>{Re(F.target.value),de(1)},placeholder:"Buscar empleado, Employee Number o cuenta...",className:"w-full pl-9 pr-3 py-2 border border-[#D0D0D0] text-xs font-medium focus:outline-none focus:border-[#4F758B]"})]}),b.jsx("div",{className:"flex flex-wrap gap-2",children:[["todos","Todos"],["discrepancias","Discrepancias"],["coincidencias","Coincidencias"],["banco_sin_nomina","Banco sin nómina"],["banco_sin_bamboo","Banco sin Bamboo"],["diferencia_nombre_banco","Diferencias nombre banco"],["nomina_sin_cuenta","Nómina sin cuenta"]].map(([F,ge])=>b.jsx("button",{onClick:()=>{P(F),de(1)},className:`px-3 py-1.5 text-[10px] uppercase tracking-wider font-black border transition-colors ${O===F?"bg-[#4F758B] text-white border-[#4F758B]":"bg-white text-neutral-500 border-neutral-300 hover:border-[#4F758B] hover:text-[#4F758B]"}`,children:ge},F))})]}),b.jsx("div",{className:"overflow-x-auto",children:b.jsxs("table",{className:"w-full text-left border-collapse min-w-[960px] xl:min-w-full",children:[b.jsx("thead",{children:b.jsxs("tr",{className:"bg-[#4F758B] text-white text-xs font-bold uppercase tracking-wider",children:[b.jsx("th",{className:"py-3 px-3 xl:px-4",children:"Empleado"}),b.jsx("th",{className:"py-3 px-3 xl:px-4",children:"Employee Number"}),b.jsx("th",{className:"py-3 px-3 xl:px-4",children:"Cuenta"}),b.jsx("th",{className:"py-3 px-3 xl:px-4 text-right",children:"Monto Nómina"}),b.jsx("th",{className:"py-3 px-3 xl:px-4 text-right",children:"Monto Banco"}),b.jsx("th",{className:"py-3 px-3 xl:px-4 text-right",children:"Diferencia"}),b.jsx("th",{className:"py-3 px-3 xl:px-4 text-center",children:"Estado"}),b.jsx("th",{className:"py-3 px-3 xl:px-4",children:"Categoría"}),b.jsx("th",{className:"py-3 px-3 xl:px-4",children:"Observación"})]})}),b.jsx("tbody",{className:"divide-y divide-neutral-200 text-xs",children:W.length>0?W.map(F=>b.jsxs("tr",{className:"hover:bg-neutral-50 transition-colors",children:[b.jsx("td",{className:"py-3 px-3 xl:px-4 font-bold text-neutral-800",children:F.employee}),b.jsx("td",{className:"py-3 px-3 xl:px-4 font-mono text-neutral-600",children:F.employeeNumber||"—"}),b.jsx("td",{className:"py-3 px-3 xl:px-4 font-mono text-neutral-500",children:F.account??"—"}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-right font-mono text-neutral-700",children:Yh(F.amountPayroll,x)}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-right font-mono text-neutral-700",children:Yh(F.amountBank,x)}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-right font-mono text-neutral-700",children:Yh(F.difference,x)}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-center",children:b.jsx("span",{className:`inline-block px-2.5 py-1 text-[9px] font-black uppercase tracking-wider border ${FR(F.status)}`,children:F.status})}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-neutral-600 max-w-[180px] capitalize",children:JR(F.category??F.source)}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-neutral-600 max-w-[300px]",children:F.observation})]},F.id)):b.jsx("tr",{children:b.jsx("td",{colSpan:9,className:"py-12 text-center text-neutral-400 font-medium",children:"No hay resultados para el filtro seleccionado."})})})]})}),b.jsxs("div",{className:"p-4 border-t border-neutral-200 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-xs text-neutral-500",children:[b.jsxs("span",{children:["Mostrando ",W.length," de ",E.length," resultado(s)."]}),b.jsxs("div",{className:"flex items-center gap-2",children:[b.jsx("button",{onClick:()=>de(F=>Math.max(1,F-1)),disabled:le===1,className:"px-3 py-1.5 border border-neutral-300 bg-white disabled:opacity-40 disabled:cursor-not-allowed hover:border-[#4F758B] font-bold",children:"Anterior"}),b.jsxs("span",{className:"font-bold text-neutral-700",children:["Página ",le," de ",K]}),b.jsx("button",{onClick:()=>de(F=>Math.min(K,F+1)),disabled:le===K,className:"px-3 py-1.5 border border-neutral-300 bg-white disabled:opacity-40 disabled:cursor-not-allowed hover:border-[#4F758B] font-bold",children:"Siguiente"})]})]})]})]})})]})]})}function lu(n,e){var s={};for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&e.indexOf(a)<0&&(s[a]=n[a]);if(n!=null&&typeof Object.getOwnPropertySymbols=="function")for(var l=0,a=Object.getOwnPropertySymbols(n);ln?(...e)=>n(...e):(...e)=>fetch(...e);class Tf extends Error{constructor(e,s="FunctionsError",a){super(e),this.name=s,this.context=a}toJSON(){return{name:this.name,message:this.message,context:this.context}}}class uC extends Tf{constructor(e){super("Failed to send a request to the Edge Function","FunctionsFetchError",e)}}class fv extends Tf{constructor(e){super("Relay Error invoking the Edge Function","FunctionsRelayError",e)}}class mv extends Tf{constructor(e){super("Edge Function returned a non-2xx status code","FunctionsHttpError",e)}}var jd;(function(n){n.Any="any",n.ApNortheast1="ap-northeast-1",n.ApNortheast2="ap-northeast-2",n.ApSouth1="ap-south-1",n.ApSoutheast1="ap-southeast-1",n.ApSoutheast2="ap-southeast-2",n.CaCentral1="ca-central-1",n.EuCentral1="eu-central-1",n.EuWest1="eu-west-1",n.EuWest2="eu-west-2",n.EuWest3="eu-west-3",n.SaEast1="sa-east-1",n.UsEast1="us-east-1",n.UsWest1="us-west-1",n.UsWest2="us-west-2"})(jd||(jd={}));class cC{constructor(e,{headers:s={},customFetch:a,region:l=jd.Any}={}){this.url=e,this.headers=s,this.region=l,this.fetch=lC(a)}setAuth(e){this.headers.Authorization=`Bearer ${e}`}invoke(e){return oC(this,arguments,void 0,function*(s,a={}){var l;let u,c;try{const{headers:d,method:m,body:p,signal:g,timeout:v}=a;let w={},{region:x}=a;x||(x=this.region);const S=new URL(`${this.url}/${s}`);x&&x!=="any"&&(w["x-region"]=x,S.searchParams.set("forceFunctionRegion",x));let A;const k=!!d&&Object.keys(d).some(X=>X.toLowerCase()==="content-type");p&&!k?typeof Blob<"u"&&p instanceof Blob||p instanceof ArrayBuffer?(w["Content-Type"]="application/octet-stream",A=p):typeof p=="string"?(w["Content-Type"]="text/plain",A=p):typeof FormData<"u"&&p instanceof FormData?A=p:(w["Content-Type"]="application/json",A=JSON.stringify(p)):p&&typeof p!="string"&&!(typeof Blob<"u"&&p instanceof Blob)&&!(p instanceof ArrayBuffer)&&!(typeof FormData<"u"&&p instanceof FormData)?A=JSON.stringify(p):A=p;let C=g;v&&(c=new AbortController,u=setTimeout(()=>c.abort(),v),g?(C=c.signal,g.addEventListener("abort",()=>c.abort())):C=c.signal);const N=yield this.fetch(S.toString(),{method:m||"POST",headers:Object.assign(Object.assign(Object.assign({},w),this.headers),d),body:A,signal:C}).catch(X=>{throw new uC(X)}),O=N.headers.get("x-relay-error");if(O&&O==="true")throw new fv(N);if(!N.ok)throw new mv(N);let P=((l=N.headers.get("Content-Type"))!==null&&l!==void 0?l:"text/plain").split(";")[0].trim(),J;return P==="application/json"?J=yield N.json():P==="application/octet-stream"||P==="application/pdf"?J=yield N.blob():P==="text/event-stream"?J=N:P==="multipart/form-data"?J=yield N.formData():J=yield N.text(),{data:J,error:null,response:N}}catch(d){return{data:null,error:d,response:d instanceof mv||d instanceof fv?d.context:void 0}}finally{u&&clearTimeout(u)}})}}const Gw=3,pv=n=>Math.min(1e3*2**n,3e4),hC=[520,503],Kw=["GET","HEAD","OPTIONS"];var gv=class extends Error{constructor(n){super(n.message),this.name="PostgrestError",this.details=n.details,this.hint=n.hint,this.code=n.code}toJSON(){return{name:this.name,message:this.message,details:this.details,hint:this.hint,code:this.code}}};function yv(n,e){return new Promise(s=>{if(e!=null&&e.aborted){s();return}const a=setTimeout(()=>{e==null||e.removeEventListener("abort",l),s()},n);function l(){clearTimeout(a),s()}e==null||e.addEventListener("abort",l)})}function dC(n,e,s,a){return!(!a||s>=Gw||!Kw.includes(n)||!hC.includes(e))}var fC=class{constructor(n){var e,s,a,l,u;this.shouldThrowOnError=!1,this.retryEnabled=!0,this.method=n.method,this.url=n.url,this.headers=new Headers(n.headers),this.schema=n.schema,this.body=n.body,this.shouldThrowOnError=(e=n.shouldThrowOnError)!==null&&e!==void 0?e:!1,this.signal=n.signal,this.isMaybeSingle=(s=n.isMaybeSingle)!==null&&s!==void 0?s:!1,this.shouldStripNulls=(a=n.shouldStripNulls)!==null&&a!==void 0?a:!1,this.urlLengthLimit=(l=n.urlLengthLimit)!==null&&l!==void 0?l:8e3,this.retryEnabled=(u=n.retry)!==null&&u!==void 0?u:!0,n.fetch?this.fetch=n.fetch:this.fetch=fetch}throwOnError(){return this.shouldThrowOnError=!0,this}stripNulls(){if(this.headers.get("Accept")==="text/csv")throw new Error("stripNulls() cannot be used with csv()");return this.shouldStripNulls=!0,this}setHeader(n,e){return this.headers=new Headers(this.headers),this.headers.set(n,e),this}retry(n){return this.retryEnabled=n,this}then(n,e){var s=this;if(this.schema===void 0||(["GET","HEAD"].includes(this.method)?this.headers.set("Accept-Profile",this.schema):this.headers.set("Content-Profile",this.schema)),this.method!=="GET"&&this.method!=="HEAD"&&this.headers.set("Content-Type","application/json"),this.shouldStripNulls){const c=this.headers.get("Accept");c==="application/vnd.pgrst.object+json"?this.headers.set("Accept","application/vnd.pgrst.object+json;nulls=stripped"):(!c||c==="application/json")&&this.headers.set("Accept","application/vnd.pgrst.array+json;nulls=stripped")}const a=this.fetch;let u=(async()=>{let c=0;for(;;){const p={};s.headers.forEach((v,w)=>{p[w]=v}),c>0&&(p["X-Retry-Count"]=String(c));let g;try{g=await a(s.url.toString(),{method:s.method,headers:p,body:JSON.stringify(s.body,(v,w)=>typeof w=="bigint"?w.toString():w),signal:s.signal})}catch(v){if((v==null?void 0:v.name)==="AbortError"||(v==null?void 0:v.code)==="ABORT_ERR"||!Kw.includes(s.method))throw v;if(s.retryEnabled&&c{var d;let m="",p="",g="";const v=c==null?void 0:c.cause;if(v){var w,x,S,A;const N=(w=v==null?void 0:v.message)!==null&&w!==void 0?w:"",O=(x=v==null?void 0:v.code)!==null&&x!==void 0?x:"";m=`${(S=c==null?void 0:c.name)!==null&&S!==void 0?S:"FetchError"}: ${c==null?void 0:c.message}`,m+=` - -Caused by: ${(A=v==null?void 0:v.name)!==null&&A!==void 0?A:"Error"}: ${N}`,O&&(m+=` (${O})`),v!=null&&v.stack&&(m+=` -${v.stack}`)}else{var k;m=(k=c==null?void 0:c.stack)!==null&&k!==void 0?k:""}const C=this.url.toString().length;return(c==null?void 0:c.name)==="AbortError"||(c==null?void 0:c.code)==="ABORT_ERR"?(g="",p="Request was aborted (timeout or manual cancellation)",C>this.urlLengthLimit&&(p+=`. Note: Your request URL is ${C} characters, which may exceed server limits. If selecting many fields, consider using views. If filtering with large arrays (e.g., .in('id', [many IDs])), consider using an RPC function to pass values server-side.`)):((v==null?void 0:v.name)==="HeadersOverflowError"||(v==null?void 0:v.code)==="UND_ERR_HEADERS_OVERFLOW")&&(g="",p="HTTP headers exceeded server limits (typically 16KB)",C>this.urlLengthLimit&&(p+=`. Your request URL is ${C} characters. If selecting many fields, consider using views. If filtering with large arrays (e.g., .in('id', [200+ IDs])), consider using an RPC function instead.`)),{success:!1,error:{message:`${(d=c==null?void 0:c.name)!==null&&d!==void 0?d:"FetchError"}: ${c==null?void 0:c.message}`,details:m,hint:p,code:g},data:null,count:null,status:0,statusText:""}})),u.then(n,e)}async processResponse(n){var e=this;let s=null,a=null,l=null,u=n.status,c=n.statusText;if(n.ok){var d,m;if(e.method!=="HEAD"){var p;const w=await n.text();if(w!=="")if(e.headers.get("Accept")==="text/csv")a=w;else if(e.headers.get("Accept")&&(!((p=e.headers.get("Accept"))===null||p===void 0)&&p.includes("application/vnd.pgrst.plan+text")))a=w;else try{a=JSON.parse(w)}catch{if(s={message:w},a=null,e.shouldThrowOnError)throw new gv({message:w,details:"",hint:"",code:""})}}const g=(d=e.headers.get("Prefer"))===null||d===void 0?void 0:d.match(/count=(exact|planned|estimated)/),v=(m=n.headers.get("content-range"))===null||m===void 0?void 0:m.split("/");g&&v&&v.length>1&&(l=parseInt(v[1])),e.isMaybeSingle&&Array.isArray(a)&&(a.length>1?(s={code:"PGRST116",details:`Results contain ${a.length} rows, application/vnd.pgrst.object+json requires 1 row`,hint:null,message:"JSON object requested, multiple (or no) rows returned"},a=null,l=null,u=406,c="Not Acceptable"):a.length===1?a=a[0]:a=null)}else{const g=await n.text();try{s=JSON.parse(g),Array.isArray(s)&&n.status===404&&(a=[],s=null,u=200,c="OK")}catch{n.status===404&&g===""?(u=204,c="No Content"):s={message:g}}if(s&&e.shouldThrowOnError)throw new gv(s)}return{success:s===null,error:s,data:a,count:l,status:u,statusText:c}}returns(){return this}overrideTypes(){return this}},mC=class extends fC{throwOnError(){return super.throwOnError()}select(n){let e=!1;const s=(n??"*").split("").map(a=>/\s/.test(a)&&!e?"":(a==='"'&&(e=!e),a)).join("");return this.url.searchParams.set("select",s),this.headers.append("Prefer","return=representation"),this}order(n,{ascending:e=!0,nullsFirst:s,foreignTable:a,referencedTable:l=a}={}){const u=l?`${l}.order`:"order",c=this.url.searchParams.get(u);return this.url.searchParams.set(u,`${c?`${c},`:""}${n}.${e?"asc":"desc"}${s===void 0?"":s?".nullsfirst":".nullslast"}`),this}limit(n,{foreignTable:e,referencedTable:s=e}={}){const a=typeof s>"u"?"limit":`${s}.limit`;return this.url.searchParams.set(a,`${n}`),this}range(n,e,{foreignTable:s,referencedTable:a=s}={}){const l=typeof a>"u"?"offset":`${a}.offset`,u=typeof a>"u"?"limit":`${a}.limit`;return this.url.searchParams.set(l,`${n}`),this.url.searchParams.set(u,`${e-n+1}`),this}abortSignal(n){return this.signal=n,this}single(){return this.headers.set("Accept","application/vnd.pgrst.object+json"),this}maybeSingle(){return this.isMaybeSingle=!0,this}csv(){return this.headers.set("Accept","text/csv"),this}geojson(){return this.headers.set("Accept","application/geo+json"),this}explain({analyze:n=!1,verbose:e=!1,settings:s=!1,buffers:a=!1,wal:l=!1,format:u="text"}={}){var c;const d=[n?"analyze":null,e?"verbose":null,s?"settings":null,a?"buffers":null,l?"wal":null].filter(Boolean).join("|"),m=(c=this.headers.get("Accept"))!==null&&c!==void 0?c:"application/json";return this.headers.set("Accept",`application/vnd.pgrst.plan+${u}; for="${m}"; options=${d};`),u==="json"?this:this}rollback(){return this.headers.append("Prefer","tx=rollback"),this}returns(){return this}maxAffected(n){return this.headers.append("Prefer","handling=strict"),this.headers.append("Prefer",`max-affected=${n}`),this}};const vv=new RegExp("[,()]");var ra=class extends mC{throwOnError(){return super.throwOnError()}eq(n,e){return this.url.searchParams.append(n,`eq.${e}`),this}neq(n,e){return this.url.searchParams.append(n,`neq.${e}`),this}gt(n,e){return this.url.searchParams.append(n,`gt.${e}`),this}gte(n,e){return this.url.searchParams.append(n,`gte.${e}`),this}lt(n,e){return this.url.searchParams.append(n,`lt.${e}`),this}lte(n,e){return this.url.searchParams.append(n,`lte.${e}`),this}like(n,e){return this.url.searchParams.append(n,`like.${e}`),this}likeAllOf(n,e){return this.url.searchParams.append(n,`like(all).{${e.join(",")}}`),this}likeAnyOf(n,e){return this.url.searchParams.append(n,`like(any).{${e.join(",")}}`),this}ilike(n,e){return this.url.searchParams.append(n,`ilike.${e}`),this}ilikeAllOf(n,e){return this.url.searchParams.append(n,`ilike(all).{${e.join(",")}}`),this}ilikeAnyOf(n,e){return this.url.searchParams.append(n,`ilike(any).{${e.join(",")}}`),this}regexMatch(n,e){return this.url.searchParams.append(n,`match.${e}`),this}regexIMatch(n,e){return this.url.searchParams.append(n,`imatch.${e}`),this}is(n,e){return this.url.searchParams.append(n,`is.${e}`),this}isDistinct(n,e){return this.url.searchParams.append(n,`isdistinct.${e}`),this}in(n,e){const s=Array.from(new Set(e)).map(a=>typeof a=="string"&&vv.test(a)?`"${a}"`:`${a}`).join(",");return this.url.searchParams.append(n,`in.(${s})`),this}notIn(n,e){const s=Array.from(new Set(e)).map(a=>typeof a=="string"&&vv.test(a)?`"${a}"`:`${a}`).join(",");return this.url.searchParams.append(n,`not.in.(${s})`),this}contains(n,e){return typeof e=="string"?this.url.searchParams.append(n,`cs.${e}`):Array.isArray(e)?this.url.searchParams.append(n,`cs.{${e.join(",")}}`):this.url.searchParams.append(n,`cs.${JSON.stringify(e)}`),this}containedBy(n,e){return typeof e=="string"?this.url.searchParams.append(n,`cd.${e}`):Array.isArray(e)?this.url.searchParams.append(n,`cd.{${e.join(",")}}`):this.url.searchParams.append(n,`cd.${JSON.stringify(e)}`),this}rangeGt(n,e){return this.url.searchParams.append(n,`sr.${e}`),this}rangeGte(n,e){return this.url.searchParams.append(n,`nxl.${e}`),this}rangeLt(n,e){return this.url.searchParams.append(n,`sl.${e}`),this}rangeLte(n,e){return this.url.searchParams.append(n,`nxr.${e}`),this}rangeAdjacent(n,e){return this.url.searchParams.append(n,`adj.${e}`),this}overlaps(n,e){return typeof e=="string"?this.url.searchParams.append(n,`ov.${e}`):this.url.searchParams.append(n,`ov.{${e.join(",")}}`),this}textSearch(n,e,{config:s,type:a}={}){let l="";a==="plain"?l="pl":a==="phrase"?l="ph":a==="websearch"&&(l="w");const u=s===void 0?"":`(${s})`;return this.url.searchParams.append(n,`${l}fts${u}.${e}`),this}match(n){return Object.entries(n).filter(([e,s])=>s!==void 0).forEach(([e,s])=>{this.url.searchParams.append(e,`eq.${s}`)}),this}not(n,e,s){return this.url.searchParams.append(n,`not.${e}.${s}`),this}or(n,{foreignTable:e,referencedTable:s=e}={}){const a=s?`${s}.or`:"or";return this.url.searchParams.append(a,`(${n})`),this}filter(n,e,s){return this.url.searchParams.append(n,`${e}.${s}`),this}},pC=class{constructor(n,{headers:e={},schema:s,fetch:a,urlLengthLimit:l=8e3,retry:u}){this.url=n,this.headers=new Headers(e),this.schema=s,this.fetch=a,this.urlLengthLimit=l,this.retry=u}cloneRequestState(){return{url:new URL(this.url.toString()),headers:new Headers(this.headers)}}select(n,e){const{head:s=!1,count:a}=e??{},l=s?"HEAD":"GET";let u=!1;const c=(n??"*").split("").map(p=>/\s/.test(p)&&!u?"":(p==='"'&&(u=!u),p)).join(""),{url:d,headers:m}=this.cloneRequestState();return d.searchParams.set("select",c),a&&m.append("Prefer",`count=${a}`),new ra({method:l,url:d,headers:m,schema:this.schema,fetch:this.fetch,urlLengthLimit:this.urlLengthLimit,retry:this.retry})}insert(n,{count:e,defaultToNull:s=!0}={}){var a;const l="POST",{url:u,headers:c}=this.cloneRequestState();if(e&&c.append("Prefer",`count=${e}`),s||c.append("Prefer","missing=default"),Array.isArray(n)){const d=n.reduce((m,p)=>m.concat(Object.keys(p)),[]);if(d.length>0){const m=[...new Set(d)].map(p=>`"${p}"`);u.searchParams.set("columns",m.join(","))}}return new ra({method:l,url:u,headers:c,schema:this.schema,body:n,fetch:(a=this.fetch)!==null&&a!==void 0?a:fetch,urlLengthLimit:this.urlLengthLimit,retry:this.retry})}upsert(n,{onConflict:e,ignoreDuplicates:s=!1,count:a,defaultToNull:l=!0}={}){var u;const c="POST",{url:d,headers:m}=this.cloneRequestState();if(m.append("Prefer",`resolution=${s?"ignore":"merge"}-duplicates`),e!==void 0&&d.searchParams.set("on_conflict",e),a&&m.append("Prefer",`count=${a}`),l||m.append("Prefer","missing=default"),Array.isArray(n)){const p=n.reduce((g,v)=>g.concat(Object.keys(v)),[]);if(p.length>0){const g=[...new Set(p)].map(v=>`"${v}"`);d.searchParams.set("columns",g.join(","))}}return new ra({method:c,url:d,headers:m,schema:this.schema,body:n,fetch:(u=this.fetch)!==null&&u!==void 0?u:fetch,urlLengthLimit:this.urlLengthLimit,retry:this.retry})}update(n,{count:e}={}){var s;const a="PATCH",{url:l,headers:u}=this.cloneRequestState();return e&&u.append("Prefer",`count=${e}`),new ra({method:a,url:l,headers:u,schema:this.schema,body:n,fetch:(s=this.fetch)!==null&&s!==void 0?s:fetch,urlLengthLimit:this.urlLengthLimit,retry:this.retry})}delete({count:n}={}){var e;const s="DELETE",{url:a,headers:l}=this.cloneRequestState();return n&&l.append("Prefer",`count=${n}`),new ra({method:s,url:a,headers:l,schema:this.schema,fetch:(e=this.fetch)!==null&&e!==void 0?e:fetch,urlLengthLimit:this.urlLengthLimit,retry:this.retry})}};function Ur(n){"@babel/helpers - typeof";return Ur=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Ur(n)}function gC(n,e){if(Ur(n)!="object"||!n)return n;var s=n[Symbol.toPrimitive];if(s!==void 0){var a=s.call(n,e);if(Ur(a)!="object")return a;throw new TypeError("@@toPrimitive must return a primitive value.")}return(e==="string"?String:Number)(n)}function yC(n){var e=gC(n,"string");return Ur(e)=="symbol"?e:e+""}function vC(n,e,s){return(e=yC(e))in n?Object.defineProperty(n,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):n[e]=s,n}function bv(n,e){var s=Object.keys(n);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(n);e&&(a=a.filter(function(l){return Object.getOwnPropertyDescriptor(n,l).enumerable})),s.push.apply(s,a)}return s}function wl(n){for(var e=1;e0?this.fetch=(p,g)=>{const v=new AbortController,w=setTimeout(()=>v.abort(),u),x=g==null?void 0:g.signal;if(x){if(x.aborted)return clearTimeout(w),m(p,g);const S=()=>{clearTimeout(w),v.abort()};return x.addEventListener("abort",S,{once:!0}),m(p,wl(wl({},g),{},{signal:v.signal})).finally(()=>{clearTimeout(w),x.removeEventListener("abort",S)})}return m(p,wl(wl({},g),{},{signal:v.signal})).finally(()=>clearTimeout(w))}:this.fetch=m,this.retry=d}from(e){if(!e||typeof e!="string"||e.trim()==="")throw new Error("Invalid relation name: relation must be a non-empty string.");return new pC(new URL(`${this.url}/${e}`),{headers:new Headers(this.headers),schema:this.schemaName,fetch:this.fetch,urlLengthLimit:this.urlLengthLimit,retry:this.retry})}schema(e){return new Fw(this.url,{headers:this.headers,schema:e,fetch:this.fetch,urlLengthLimit:this.urlLengthLimit,retry:this.retry})}rpc(e,s={},{head:a=!1,get:l=!1,count:u}={}){var c;let d;const m=new URL(`${this.url}/rpc/${e}`);let p;const g=x=>x!==null&&typeof x=="object"&&(!Array.isArray(x)||x.some(g)),v=a&&Object.values(s).some(g);v?(d="POST",p=s):a||l?(d=a?"HEAD":"GET",Object.entries(s).filter(([x,S])=>S!==void 0).map(([x,S])=>[x,Array.isArray(S)?`{${S.join(",")}}`:`${S}`]).forEach(([x,S])=>{m.searchParams.append(x,S)})):(d="POST",p=s);const w=new Headers(this.headers);return v?w.set("Prefer",u?`count=${u},return=minimal`:"return=minimal"):u&&w.set("Prefer",`count=${u}`),new ra({method:d,url:m,headers:w,schema:this.schemaName,body:p,fetch:(c=this.fetch)!==null&&c!==void 0?c:fetch,urlLengthLimit:this.urlLengthLimit,retry:this.retry})}};class wC{constructor(){}static detectEnvironment(){var e;if(typeof WebSocket<"u")return{type:"native",wsConstructor:WebSocket};const s=globalThis;if(typeof globalThis<"u"&&typeof s.WebSocket<"u")return{type:"native",wsConstructor:s.WebSocket};const a=typeof global<"u"?global:void 0;if(a&&typeof a.WebSocket<"u")return{type:"native",wsConstructor:a.WebSocket};if(typeof globalThis<"u"&&typeof s.WebSocketPair<"u"&&typeof globalThis.WebSocket>"u")return{type:"cloudflare",error:"Cloudflare Workers detected. WebSocket clients are not supported in Cloudflare Workers.",workaround:"Use Cloudflare Workers WebSocket API for server-side WebSocket handling, or deploy to a different runtime."};if(typeof globalThis<"u"&&s.EdgeRuntime||typeof navigator<"u"&&(!((e=navigator.userAgent)===null||e===void 0)&&e.includes("Vercel-Edge")))return{type:"unsupported",error:"Edge runtime detected (Vercel Edge/Netlify Edge). WebSockets are not supported in edge functions.",workaround:"Use serverless functions or a different deployment target for WebSocket functionality."};const l=globalThis.process;if(l){const u=l.versions;if(u&&u.node)return{type:"unsupported",error:"Node.js detected but native WebSocket not found.",workaround:"Ensure you are running Node.js 22+ or provide a WebSocket implementation via the transport option."}}return{type:"unsupported",error:"Unknown JavaScript runtime without WebSocket support.",workaround:"Ensure you're running in a supported environment (browser, Node.js, Deno) or provide a custom WebSocket implementation."}}static getWebSocketConstructor(){const e=this.detectEnvironment();if(e.wsConstructor)return e.wsConstructor;let s=e.error||"WebSocket not supported in this environment.";throw e.workaround&&(s+=` - -Suggested solution: ${e.workaround}`),new Error(s)}static isWebSocketSupported(){try{return this.detectEnvironment().type==="native"}catch{return!1}}}const _C="2.110.2",xC=`realtime-js/${_C}`,SC="1.0.0",Yw="2.0.0",TC=Yw,EC=1e4,AC=100,Mi={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},Xw={close:"phx_close",error:"phx_error",join:"phx_join",leave:"phx_leave",access_token:"access_token"},Od={connecting:"connecting",closing:"closing",closed:"closed"};class RC{constructor(e){this.HEADER_LENGTH=1,this.USER_BROADCAST_PUSH_META_LENGTH=6,this.KINDS={userBroadcastPush:3,userBroadcast:4},this.BINARY_ENCODING=0,this.JSON_ENCODING=1,this.BROADCAST_EVENT="broadcast",this.allowedMetadataKeys=[],this.allowedMetadataKeys=e??[]}encode(e,s){if(e.event===this.BROADCAST_EVENT&&!(e.payload instanceof ArrayBuffer)&&typeof e.payload.event=="string")return s(this._binaryEncodeUserBroadcastPush(e));let a=[e.join_ref,e.ref,e.topic,e.event,e.payload];return s(JSON.stringify(a))}_binaryEncodeUserBroadcastPush(e){var s;return this._isArrayBuffer((s=e.payload)===null||s===void 0?void 0:s.payload)?this._encodeBinaryUserBroadcastPush(e):this._encodeJsonUserBroadcastPush(e)}_encodeBinaryUserBroadcastPush(e){var s,a;const l=(a=(s=e.payload)===null||s===void 0?void 0:s.payload)!==null&&a!==void 0?a:new ArrayBuffer(0);return this._encodeUserBroadcastPush(e,this.BINARY_ENCODING,l)}_encodeJsonUserBroadcastPush(e){var s,a;const l=(a=(s=e.payload)===null||s===void 0?void 0:s.payload)!==null&&a!==void 0?a:{},c=new TextEncoder().encode(JSON.stringify(l)).buffer;return this._encodeUserBroadcastPush(e,this.JSON_ENCODING,c)}_encodeUserBroadcastPush(e,s,a){var l,u;const c=e.topic,d=(l=e.ref)!==null&&l!==void 0?l:"",m=(u=e.join_ref)!==null&&u!==void 0?u:"",p=e.payload.event,g=this.allowedMetadataKeys?this._pick(e.payload,this.allowedMetadataKeys):{},v=Object.keys(g).length===0?"":JSON.stringify(g);if(m.length>255)throw new Error(`joinRef length ${m.length} exceeds maximum of 255`);if(d.length>255)throw new Error(`ref length ${d.length} exceeds maximum of 255`);if(c.length>255)throw new Error(`topic length ${c.length} exceeds maximum of 255`);if(p.length>255)throw new Error(`userEvent length ${p.length} exceeds maximum of 255`);if(v.length>255)throw new Error(`metadata length ${v.length} exceeds maximum of 255`);const w=this.USER_BROADCAST_PUSH_META_LENGTH+m.length+d.length+c.length+p.length+v.length,x=new ArrayBuffer(this.HEADER_LENGTH+w);let S=new DataView(x),A=0;S.setUint8(A++,this.KINDS.userBroadcastPush),S.setUint8(A++,m.length),S.setUint8(A++,d.length),S.setUint8(A++,c.length),S.setUint8(A++,p.length),S.setUint8(A++,v.length),S.setUint8(A++,s),Array.from(m,C=>S.setUint8(A++,C.charCodeAt(0))),Array.from(d,C=>S.setUint8(A++,C.charCodeAt(0))),Array.from(c,C=>S.setUint8(A++,C.charCodeAt(0))),Array.from(p,C=>S.setUint8(A++,C.charCodeAt(0))),Array.from(v,C=>S.setUint8(A++,C.charCodeAt(0)));var k=new Uint8Array(x.byteLength+a.byteLength);return k.set(new Uint8Array(x),0),k.set(new Uint8Array(a),x.byteLength),k.buffer}decode(e,s){if(this._isArrayBuffer(e)){let a=this._binaryDecode(e);return s(a)}if(typeof e=="string"){const a=JSON.parse(e),[l,u,c,d,m]=a;return s({join_ref:l,ref:u,topic:c,event:d,payload:m})}return s({})}_binaryDecode(e){const s=new DataView(e),a=s.getUint8(0),l=new TextDecoder;switch(a){case this.KINDS.userBroadcast:return this._decodeUserBroadcast(e,s,l)}}_decodeUserBroadcast(e,s,a){const l=s.getUint8(1),u=s.getUint8(2),c=s.getUint8(3),d=s.getUint8(4);let m=this.HEADER_LENGTH+4;const p=a.decode(e.slice(m,m+l));m=m+l;const g=a.decode(e.slice(m,m+u));m=m+u;const v=a.decode(e.slice(m,m+c));m=m+c;const w=e.slice(m,e.byteLength),x=d===this.JSON_ENCODING?JSON.parse(a.decode(w)):w,S={type:this.BROADCAST_EVENT,event:g,payload:x};return c>0&&(S.meta=JSON.parse(v)),{join_ref:null,ref:null,topic:p,event:this.BROADCAST_EVENT,payload:S}}_isArrayBuffer(e){var s;return e instanceof ArrayBuffer||((s=e==null?void 0:e.constructor)===null||s===void 0?void 0:s.name)==="ArrayBuffer"}_pick(e,s){return!e||typeof e!="object"?{}:Object.fromEntries(Object.entries(e).filter(([a])=>s.includes(a)))}}var $e;(function(n){n.abstime="abstime",n.bool="bool",n.date="date",n.daterange="daterange",n.float4="float4",n.float8="float8",n.int2="int2",n.int4="int4",n.int4range="int4range",n.int8="int8",n.int8range="int8range",n.json="json",n.jsonb="jsonb",n.money="money",n.numeric="numeric",n.oid="oid",n.reltime="reltime",n.text="text",n.time="time",n.timestamp="timestamp",n.timestamptz="timestamptz",n.timetz="timetz",n.tsrange="tsrange",n.tstzrange="tstzrange"})($e||($e={}));const wv=(n,e,s={})=>{var a;const l=(a=s.skipTypes)!==null&&a!==void 0?a:[];return e?Object.keys(e).reduce((u,c)=>(u[c]=CC(c,n,e,l),u),{}):{}},CC=(n,e,s,a)=>{const l=e.find(d=>d.name===n),u=l==null?void 0:l.type,c=s[n];return u&&!a.includes(u)?Jw(u,c):Nd(c)},Jw=(n,e)=>{if(n.charAt(0)==="_"){const s=n.slice(1,n.length);return NC(e,s)}switch(n){case $e.bool:return kC(e);case $e.float4:case $e.float8:case $e.int2:case $e.int4:case $e.int8:case $e.numeric:case $e.oid:return jC(e);case $e.json:case $e.jsonb:return OC(e);case $e.timestamp:return DC(e);case $e.abstime:case $e.date:case $e.daterange:case $e.int4range:case $e.int8range:case $e.money:case $e.reltime:case $e.text:case $e.time:case $e.timestamptz:case $e.timetz:case $e.tsrange:case $e.tstzrange:return Nd(e);default:return Nd(e)}},Nd=n=>n,kC=n=>{switch(n){case"t":return!0;case"f":return!1;default:return n}},jC=n=>{if(typeof n=="string"){const e=parseFloat(n);if(!Number.isNaN(e))return e}return n},OC=n=>{if(typeof n=="string")try{return JSON.parse(n)}catch{return n}return n},NC=(n,e)=>{if(typeof n!="string")return n;const s=n.length-1,a=n[s];if(n[0]==="{"&&a==="}"){let u;const c=n.slice(1,s);try{u=JSON.parse("["+c+"]")}catch{u=c?c.split(","):[]}return u.map(d=>Jw(e,d))}return n},DC=n=>typeof n=="string"?n.replace(" ","T"):n,Qw=n=>{const e=new URL(n);return e.protocol=e.protocol.replace(/^ws/i,"http"),e.pathname=e.pathname.replace(/\/+$/,"").replace(/\/socket\/websocket$/i,"").replace(/\/socket$/i,"").replace(/\/websocket$/i,""),e.pathname===""||e.pathname==="/"?e.pathname="/api/broadcast":e.pathname=e.pathname+"/api/broadcast",e.href};var Ar=n=>typeof n=="function"?n:function(){return n},MC=typeof self<"u"?self:null,oa=typeof window<"u"?window:null,jn=MC||oa||globalThis,UC="2.0.0",BC=1e4,LC=1e3,Nn={connecting:0,open:1,closing:2,closed:3},Mt={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},si={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},Dd={longpoll:"longpoll",websocket:"websocket"},zC={complete:4},Md="base64url.bearer.phx.",_l=class{constructor(n,e,s,a){this.channel=n,this.event=e,this.payload=s||function(){return{}},this.receivedResp=null,this.timeout=a,this.timeoutTimer=null,this.recHooks=[],this.sent=!1,this.ref=void 0}resend(n){this.timeout=n,this.reset(),this.send()}send(){this.hasReceived("timeout")||(this.startTimeout(),this.sent=!0,this.channel.socket.push({topic:this.channel.topic,event:this.event,payload:this.payload(),ref:this.ref,join_ref:this.channel.joinRef()}))}receive(n,e){return this.hasReceived(n)&&e(this.receivedResp.response),this.recHooks.push({status:n,callback:e}),this}reset(){this.cancelRefEvent(),this.ref=null,this.refEvent=null,this.receivedResp=null,this.sent=!1}destroy(){this.cancelRefEvent(),this.cancelTimeout()}matchReceive({status:n,response:e,_ref:s}){this.recHooks.filter(a=>a.status===n).forEach(a=>a.callback(e))}cancelRefEvent(){this.refEvent&&this.channel.off(this.refEvent)}cancelTimeout(){clearTimeout(this.timeoutTimer),this.timeoutTimer=null}startTimeout(){this.timeoutTimer&&this.cancelTimeout(),this.ref=this.channel.socket.makeRef(),this.refEvent=this.channel.replyEventName(this.ref),this.channel.on(this.refEvent,n=>{this.cancelRefEvent(),this.cancelTimeout(),this.receivedResp=n,this.matchReceive(n)}),this.timeoutTimer=setTimeout(()=>{this.trigger("timeout",{})},this.timeout)}hasReceived(n){return this.receivedResp&&this.receivedResp.status===n}trigger(n,e){this.channel.trigger(this.refEvent,{status:n,response:e})}},Zw=class{constructor(n,e){this.callback=n,this.timerCalc=e,this.timer=void 0,this.tries=0}reset(){this.tries=0,clearTimeout(this.timer)}scheduleTimeout(){clearTimeout(this.timer),this.timer=setTimeout(()=>{this.tries=this.tries+1,this.callback()},this.timerCalc(this.tries+1))}},VC=class{constructor(n,e,s){this.state=Mt.closed,this.topic=n,this.params=Ar(e||{}),this.socket=s,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new _l(this,si.join,this.params,this.timeout),this.pushBuffer=[],this.stateChangeRefs=[],this.rejoinTimer=new Zw(()=>{this.socket.isConnected()&&this.rejoin()},this.socket.rejoinAfterMs),this.stateChangeRefs.push(this.socket.onError(()=>this.rejoinTimer.reset())),this.stateChangeRefs.push(this.socket.onOpen(()=>{this.rejoinTimer.reset(),this.isErrored()&&this.rejoin()})),this.joinPush.receive("ok",()=>{this.state=Mt.joined,this.rejoinTimer.reset(),this.pushBuffer.forEach(a=>a.send()),this.pushBuffer=[]}),this.joinPush.receive("error",a=>{this.state=Mt.errored,this.socket.hasLogger()&&this.socket.log("channel",`error ${this.topic}`,a),this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.onClose(()=>{this.rejoinTimer.reset(),this.socket.hasLogger()&&this.socket.log("channel",`close ${this.topic}`),this.state=Mt.closed,this.socket.remove(this)}),this.onError(a=>{this.socket.hasLogger()&&this.socket.log("channel",`error ${this.topic}`,a),this.isJoining()&&this.joinPush.reset(),this.state=Mt.errored,this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.joinPush.receive("timeout",()=>{this.socket.hasLogger()&&this.socket.log("channel",`timeout ${this.topic}`,this.joinPush.timeout),new _l(this,si.leave,Ar({}),this.timeout).send(),this.state=Mt.errored,this.joinPush.reset(),this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.on(si.reply,(a,l)=>{this.trigger(this.replyEventName(l),a)})}join(n=this.timeout){if(this.joinedOnce)throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");return this.timeout=n,this.joinedOnce=!0,this.rejoin(),this.joinPush}teardown(){this.pushBuffer.forEach(n=>n.destroy()),this.pushBuffer=[],this.rejoinTimer.reset(),this.joinPush.destroy(),this.state=Mt.closed,this.bindings=[]}onClose(n){this.on(si.close,n)}onError(n){return this.on(si.error,e=>n(e))}on(n,e){let s=this.bindingRef++;return this.bindings.push({event:n,ref:s,callback:e}),s}off(n,e){this.bindings=this.bindings.filter(s=>!(s.event===n&&(typeof e>"u"||e===s.ref)))}canPush(){return this.socket.isConnected()&&this.isJoined()}push(n,e,s=this.timeout){if(e=e||{},!this.joinedOnce)throw new Error(`tried to push '${n}' to '${this.topic}' before joining. Use channel.join() before pushing events`);let a=new _l(this,n,function(){return e},s);return this.canPush()?a.send():(a.startTimeout(),this.pushBuffer.push(a)),a}leave(n=this.timeout){this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=Mt.leaving;let e=()=>{this.socket.hasLogger()&&this.socket.log("channel",`leave ${this.topic}`),this.trigger(si.close,"leave")},s=new _l(this,si.leave,Ar({}),n);return s.receive("ok",()=>e()).receive("timeout",()=>e()),s.send(),this.canPush()||s.trigger("ok",{}),s}onMessage(n,e,s){return e}filterBindings(n,e,s){return!0}isMember(n,e,s,a){return this.topic!==n?!1:a&&a!==this.joinRef()?(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:n,event:e,payload:s,joinRef:a}),!1):!0}joinRef(){return this.joinPush.ref}rejoin(n=this.timeout){this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=Mt.joining,this.joinPush.resend(n))}trigger(n,e,s,a){let l=this.onMessage(n,e,s,a);if(e&&!l)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");let u=this.bindings.filter(c=>c.event===n&&this.filterBindings(c,e,s));for(let c=0;cm.abort(),l),d.signal=m.signal),jn.fetch(e,d).then(p=>p.text()).then(p=>this.parseJSON(p)).then(p=>c&&c(p)).catch(p=>{p.name==="AbortError"&&u?u():c&&c(null)}),m}static xdomainRequest(n,e,s,a,l,u,c){return n.timeout=l,n.open(e,s),n.onload=()=>{let d=this.parseJSON(n.responseText);c&&c(d)},u&&(n.ontimeout=u),n.onprogress=()=>{},n.send(a),n}static xhrRequest(n,e,s,a,l,u,c,d){n.open(e,s,!0),n.timeout=u;for(let[m,p]of Object.entries(a))n.setRequestHeader(m,p);return n.onerror=()=>d&&d(null),n.onreadystatechange=()=>{if(n.readyState===zC.complete&&d){let m=this.parseJSON(n.responseText);d(m)}},c&&(n.ontimeout=c),n.send(l),n}static parseJSON(n){if(!n||n==="")return null;try{return JSON.parse(n)}catch{return console&&console.log("failed to parse JSON response",n),null}}static serialize(n,e){let s=[];for(var a in n){if(!Object.prototype.hasOwnProperty.call(n,a))continue;let l=e?`${e}[${a}]`:a,u=n[a];typeof u=="object"?s.push(this.serialize(u,l)):s.push(encodeURIComponent(l)+"="+encodeURIComponent(u))}return s.join("&")}static appendParams(n,e){if(Object.keys(e).length===0)return n;let s=n.match(/\?/)?"&":"?";return`${n}${s}${this.serialize(e)}`}},PC=n=>{let e="",s=new Uint8Array(n),a=s.byteLength;for(let l=0;lthis.poll(),0)}normalizeEndpoint(n){return n.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+Dd.websocket),"$1/"+Dd.longpoll)}endpointURL(){return Xl.appendParams(this.pollEndpoint,{token:this.token})}closeAndRetry(n,e,s){this.close(n,e,s),this.readyState=Nn.connecting}ontimeout(){this.onerror("timeout"),this.closeAndRetry(1005,"timeout",!1)}isActive(){return this.readyState===Nn.open||this.readyState===Nn.connecting}poll(){const n={Accept:"application/json"};this.authToken&&(n["X-Phoenix-AuthToken"]=this.authToken),this.ajax("GET",n,null,()=>this.ontimeout(),e=>{if(e){var{status:s,token:a,messages:l}=e;if(s===410&&this.token!==null){this.onerror(410),this.closeAndRetry(3410,"session_gone",!1);return}this.token=a}else s=0;switch(s){case 200:l.forEach(u=>{setTimeout(()=>this.onmessage({data:u}),0)}),this.poll();break;case 204:this.poll();break;case 410:this.readyState=Nn.open,this.onopen({}),this.poll();break;case 403:this.onerror(403),this.close(1008,"forbidden",!1);break;case 0:case 500:this.onerror(500),this.closeAndRetry(1011,"internal server error",500);break;default:throw new Error(`unhandled poll status ${s}`)}})}send(n){typeof n!="string"&&(n=PC(n)),this.currentBatch?this.currentBatch.push(n):this.awaitingBatchAck?this.batchBuffer.push(n):(this.currentBatch=[n],this.currentBatchTimer=setTimeout(()=>{this.batchSend(this.currentBatch),this.currentBatch=null},0))}batchSend(n){this.awaitingBatchAck=!0,this.ajax("POST",{"Content-Type":"application/x-ndjson"},n.join(` -`),()=>this.onerror("timeout"),e=>{this.awaitingBatchAck=!1,!e||e.status!==200?(this.onerror(e&&e.status),this.closeAndRetry(1011,"internal server error",!1)):this.batchBuffer.length>0&&(this.batchSend(this.batchBuffer),this.batchBuffer=[])})}close(n,e,s){for(let l of this.reqs)l.abort();this.readyState=Nn.closed;let a=Object.assign({code:1e3,reason:void 0,wasClean:!0},{code:n,reason:e,wasClean:s});this.batchBuffer=[],clearTimeout(this.currentBatchTimer),this.currentBatchTimer=null,typeof CloseEvent<"u"?this.onclose(new CloseEvent("close",a)):this.onclose(a)}ajax(n,e,s,a,l){let u,c=()=>{this.reqs.delete(u),a()};u=Xl.request(n,this.endpointURL(),e,s,this.timeout,c,d=>{this.reqs.delete(u),this.isActive()&&l(d)}),this.reqs.add(u)}},HC=class br{constructor(e,s={}){let a=s.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=e,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(a.state,l=>{let{onJoin:u,onLeave:c,onSync:d}=this.caller;this.joinRef=this.channel.joinRef(),this.state=br.syncState(this.state,l,u,c),this.pendingDiffs.forEach(m=>{this.state=br.syncDiff(this.state,m,u,c)}),this.pendingDiffs=[],d()}),this.channel.on(a.diff,l=>{let{onJoin:u,onLeave:c,onSync:d}=this.caller;this.inPendingSyncState()?this.pendingDiffs.push(l):(this.state=br.syncDiff(this.state,l,u,c),d())})}onJoin(e){this.caller.onJoin=e}onLeave(e){this.caller.onLeave=e}onSync(e){this.caller.onSync=e}list(e){return br.list(this.state,e)}inPendingSyncState(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}static syncState(e,s,a,l){let u=this.clone(e),c={},d={};return this.map(u,(m,p)=>{s[m]||(d[m]=p)}),this.map(s,(m,p)=>{let g=u[m];if(g){let v=p.metas.map(A=>A.phx_ref),w=g.metas.map(A=>A.phx_ref),x=p.metas.filter(A=>w.indexOf(A.phx_ref)<0),S=g.metas.filter(A=>v.indexOf(A.phx_ref)<0);x.length>0&&(c[m]=p,c[m].metas=x),S.length>0&&(d[m]=this.clone(g),d[m].metas=S)}else c[m]=p}),this.syncDiff(u,{joins:c,leaves:d},a,l)}static syncDiff(e,s,a,l){let{joins:u,leaves:c}=this.clone(s);return a||(a=function(){}),l||(l=function(){}),this.map(u,(d,m)=>{let p=e[d];if(e[d]=this.clone(m),p){let g=e[d].metas.map(w=>w.phx_ref),v=p.metas.filter(w=>g.indexOf(w.phx_ref)<0);e[d].metas.unshift(...v)}a(d,p,m)}),this.map(c,(d,m)=>{let p=e[d];if(!p)return;let g=m.metas.map(v=>v.phx_ref);p.metas=p.metas.filter(v=>g.indexOf(v.phx_ref)<0),l(d,p,m),p.metas.length===0&&delete e[d]}),e}static list(e,s){return s||(s=function(a,l){return l}),this.map(e,(a,l)=>s(a,l))}static map(e,s){return Object.getOwnPropertyNames(e).map(a=>s(a,e[a]))}static clone(e){return JSON.parse(JSON.stringify(e))}},xl={HEADER_LENGTH:1,META_LENGTH:4,KINDS:{push:0,reply:1,broadcast:2},encode(n,e){if(n.payload.constructor===ArrayBuffer)return e(this.binaryEncode(n));{let s=[n.join_ref,n.ref,n.topic,n.event,n.payload];return e(JSON.stringify(s))}},decode(n,e){if(n.constructor===ArrayBuffer)return e(this.binaryDecode(n));{let[s,a,l,u,c]=JSON.parse(n);return e({join_ref:s,ref:a,topic:l,event:u,payload:c})}},binaryEncode(n){let{join_ref:e,ref:s,event:a,topic:l,payload:u}=n,c=this.META_LENGTH+e.length+s.length+l.length+a.length,d=new ArrayBuffer(this.HEADER_LENGTH+c),m=new DataView(d),p=0;m.setUint8(p++,this.KINDS.push),m.setUint8(p++,e.length),m.setUint8(p++,s.length),m.setUint8(p++,l.length),m.setUint8(p++,a.length),Array.from(e,v=>m.setUint8(p++,v.charCodeAt(0))),Array.from(s,v=>m.setUint8(p++,v.charCodeAt(0))),Array.from(l,v=>m.setUint8(p++,v.charCodeAt(0))),Array.from(a,v=>m.setUint8(p++,v.charCodeAt(0)));var g=new Uint8Array(d.byteLength+u.byteLength);return g.set(new Uint8Array(d),0),g.set(new Uint8Array(u),d.byteLength),g.buffer},binaryDecode(n){let e=new DataView(n),s=e.getUint8(0),a=new TextDecoder;switch(s){case this.KINDS.push:return this.decodePush(n,e,a);case this.KINDS.reply:return this.decodeReply(n,e,a);case this.KINDS.broadcast:return this.decodeBroadcast(n,e,a)}},decodePush(n,e,s){let a=e.getUint8(1),l=e.getUint8(2),u=e.getUint8(3),c=this.HEADER_LENGTH+this.META_LENGTH-1,d=s.decode(n.slice(c,c+a));c=c+a;let m=s.decode(n.slice(c,c+l));c=c+l;let p=s.decode(n.slice(c,c+u));c=c+u;let g=n.slice(c,n.byteLength);return{join_ref:d,ref:null,topic:m,event:p,payload:g}},decodeReply(n,e,s){let a=e.getUint8(1),l=e.getUint8(2),u=e.getUint8(3),c=e.getUint8(4),d=this.HEADER_LENGTH+this.META_LENGTH,m=s.decode(n.slice(d,d+a));d=d+a;let p=s.decode(n.slice(d,d+l));d=d+l;let g=s.decode(n.slice(d,d+u));d=d+u;let v=s.decode(n.slice(d,d+c));d=d+c;let w=n.slice(d,n.byteLength),x={status:v,response:w};return{join_ref:m,ref:p,topic:g,event:si.reply,payload:x}},decodeBroadcast(n,e,s){let a=e.getUint8(1),l=e.getUint8(2),u=this.HEADER_LENGTH+2,c=s.decode(n.slice(u,u+a));u=u+a;let d=s.decode(n.slice(u,u+l));u=u+l;let m=n.slice(u,n.byteLength);return{join_ref:null,ref:null,topic:c,event:d,payload:m}}},$C=class{constructor(n,e={}){this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.fallbackRef=null,this.timeout=e.timeout||BC,this.transport=e.transport||jn.WebSocket||ea,this.conn=void 0,this.primaryPassedHealthCheck=!1,this.longPollFallbackMs=e.longPollFallbackMs,this.fallbackTimer=null;let s=null;try{s=jn&&jn.sessionStorage}catch{}this.sessionStore=e.sessionStorage||s,this.establishedConnections=0,this.defaultEncoder=xl.encode.bind(xl),this.defaultDecoder=xl.decode.bind(xl),this.closeWasClean=!0,this.disconnecting=!1,this.binaryType=e.binaryType||"arraybuffer",this.connectClock=1,this.pageHidden=!1,this.encode=void 0,this.decode=void 0,this.transport!==ea?(this.encode=e.encode||this.defaultEncoder,this.decode=e.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder);let a=null;oa&&oa.addEventListener&&(oa.addEventListener("pagehide",l=>{this.conn&&(this.disconnect(),a=this.connectClock)}),oa.addEventListener("pageshow",l=>{a===this.connectClock&&(a=null,this.connect())}),oa.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?this.pageHidden=!0:(this.pageHidden=!1,!this.isConnected()&&!this.closeWasClean&&this.teardown(()=>this.connect()))})),this.heartbeatIntervalMs=e.heartbeatIntervalMs||3e4,this.autoSendHeartbeat=e.autoSendHeartbeat??!0,this.heartbeatCallback=e.heartbeatCallback??(()=>{}),this.rejoinAfterMs=l=>e.rejoinAfterMs?e.rejoinAfterMs(l):[1e3,2e3,5e3][l-1]||1e4,this.reconnectAfterMs=l=>e.reconnectAfterMs?e.reconnectAfterMs(l):[10,50,100,150,200,250,500,1e3,2e3][l-1]||5e3,this.logger=e.logger||null,!this.logger&&e.debug&&(this.logger=(l,u,c)=>{console.log(`${l}: ${u}`,c)}),this.longpollerTimeout=e.longpollerTimeout||2e4,this.params=Ar(e.params||{}),this.endPoint=`${n}/${Dd.websocket}`,this.vsn=e.vsn||UC,this.heartbeatTimeoutTimer=null,this.heartbeatTimer=null,this.heartbeatSentAt=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new Zw(()=>{if(this.pageHidden){this.log("Not reconnecting as page is hidden!"),this.teardown();return}this.teardown(async()=>{e.beforeReconnect&&await e.beforeReconnect(),this.connect()})},this.reconnectAfterMs),this.authToken=e.authToken}getLongPollTransport(){return ea}replaceTransport(n){this.connectClock++,this.closeWasClean=!0,clearTimeout(this.fallbackTimer),this.reconnectTimer.reset(),this.conn&&(this.conn.close(),this.conn=null),this.transport=n}protocol(){return location.protocol.match(/^https/)?"wss":"ws"}endPointURL(){let n=Xl.appendParams(Xl.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return n.charAt(0)!=="/"?n:n.charAt(1)==="/"?`${this.protocol()}:${n}`:`${this.protocol()}://${location.host}${n}`}disconnect(n,e,s){this.connectClock++,this.disconnecting=!0,this.closeWasClean=!0,clearTimeout(this.fallbackTimer),this.reconnectTimer.reset(),this.teardown(()=>{this.disconnecting=!1,n&&n()},e,s)}connect(n){n&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=Ar(n)),!(this.conn&&!this.disconnecting)&&(this.longPollFallbackMs&&this.transport!==ea?this.connectWithFallback(ea,this.longPollFallbackMs):this.transportConnect())}log(n,e,s){this.logger&&this.logger(n,e,s)}hasLogger(){return this.logger!==null}onOpen(n){let e=this.makeRef();return this.stateChangeCallbacks.open.push([e,n]),e}onClose(n){let e=this.makeRef();return this.stateChangeCallbacks.close.push([e,n]),e}onError(n){let e=this.makeRef();return this.stateChangeCallbacks.error.push([e,n]),e}onMessage(n){let e=this.makeRef();return this.stateChangeCallbacks.message.push([e,n]),e}onHeartbeat(n){this.heartbeatCallback=n}ping(n){if(!this.isConnected())return!1;let e=this.makeRef(),s=Date.now();this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:e});let a=this.onMessage(l=>{l.ref===e&&(this.off([a]),n(Date.now()-s))});return!0}transportName(n){switch(n){case ea:return"LongPoll";default:return n.name}}transportConnect(){this.connectClock++,this.closeWasClean=!1;let n;this.authToken&&(n=["phoenix",`${Md}${btoa(this.authToken).replace(/=/g,"")}`]),this.conn=new this.transport(this.endPointURL(),n),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=()=>this.onConnOpen(),this.conn.onerror=e=>this.onConnError(e),this.conn.onmessage=e=>this.onConnMessage(e),this.conn.onclose=e=>this.onConnClose(e)}getSession(n){return this.sessionStore&&this.sessionStore.getItem(n)}storeSession(n,e){this.sessionStore&&this.sessionStore.setItem(n,e)}connectWithFallback(n,e=2500){clearTimeout(this.fallbackTimer);let s=!1,a=!0,l,u,c=this.transportName(n),d=m=>{this.log("transport",`falling back to ${c}...`,m),this.off([l,u]),a=!1,this.replaceTransport(n),this.transportConnect()};if(this.getSession(`phx:fallback:${c}`))return d("memorized");this.fallbackTimer=setTimeout(d,e),u=this.onError(m=>{this.log("transport","error",m),a&&!s&&(clearTimeout(this.fallbackTimer),d(m))}),this.fallbackRef&&this.off([this.fallbackRef]),this.fallbackRef=this.onOpen(()=>{if(s=!0,!a){let m=this.transportName(n);return this.primaryPassedHealthCheck||this.storeSession(`phx:fallback:${m}`,"true"),this.log("transport",`established ${m} fallback`)}clearTimeout(this.fallbackTimer),this.fallbackTimer=setTimeout(d,e),this.ping(m=>{this.log("transport","connected to primary after",m),this.primaryPassedHealthCheck=!0,clearTimeout(this.fallbackTimer)})}),this.transportConnect()}clearHeartbeats(){clearTimeout(this.heartbeatTimer),clearTimeout(this.heartbeatTimeoutTimer)}onConnOpen(){this.hasLogger()&&this.log("transport",`connected to ${this.endPointURL()}`),this.closeWasClean=!1,this.disconnecting=!1,this.establishedConnections++,this.flushSendBuffer(),this.reconnectTimer.reset(),this.autoSendHeartbeat&&this.resetHeartbeat(),this.triggerStateCallbacks("open")}heartbeatTimeout(){if(this.pendingHeartbeatRef){this.pendingHeartbeatRef=null,this.heartbeatSentAt=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection");try{this.heartbeatCallback("timeout")}catch(n){this.log("error","error in heartbeat callback",n)}this.triggerChanError(new Error("heartbeat timeout")),this.closeWasClean=!1,this.teardown(()=>this.reconnectTimer.scheduleTimeout(),LC,"heartbeat timeout")}}resetHeartbeat(){this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,this.clearHeartbeats(),this.heartbeatTimer=setTimeout(()=>this.sendHeartbeat(),this.heartbeatIntervalMs))}teardown(n,e,s){if(!this.conn)return n&&n();const a=this.conn;this.waitForBufferDone(a,()=>{e?a.close(e,s||""):a.close(),this.waitForSocketClosed(a,()=>{this.conn===a&&(this.conn.onopen=function(){},this.conn.onerror=function(){},this.conn.onmessage=function(){},this.conn.onclose=function(){},this.conn=null),n&&n()})})}waitForBufferDone(n,e,s=1){if(s===5||!n.bufferedAmount){e();return}setTimeout(()=>{this.waitForBufferDone(n,e,s+1)},150*s)}waitForSocketClosed(n,e,s=1){if(s===5||n.readyState===Nn.closed){e();return}setTimeout(()=>{this.waitForSocketClosed(n,e,s+1)},150*s)}onConnClose(n){this.conn&&(this.conn.onclose=()=>{}),this.hasLogger()&&this.log("transport","close",n),this.triggerChanError(n),this.clearHeartbeats(),this.closeWasClean||this.reconnectTimer.scheduleTimeout(),this.triggerStateCallbacks("close",n)}onConnError(n){this.hasLogger()&&this.log("transport","error",n);let e=this.transport,s=this.establishedConnections;this.triggerStateCallbacks("error",n,e,s),(e===this.transport||s>0)&&this.triggerChanError(n)}triggerChanError(n){this.channels.forEach(e=>{e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(si.error,n)})}connectionState(){switch(this.conn&&this.conn.readyState){case Nn.connecting:return"connecting";case Nn.open:return"open";case Nn.closing:return"closing";default:return"closed"}}isConnected(){return this.connectionState()==="open"}remove(n){this.off(n.stateChangeRefs),this.channels=this.channels.filter(e=>e!==n)}off(n){for(let e in this.stateChangeCallbacks)this.stateChangeCallbacks[e]=this.stateChangeCallbacks[e].filter(([s])=>n.indexOf(s)===-1)}channel(n,e={}){let s=new VC(n,e,this);return this.channels.push(s),s}push(n){if(this.hasLogger()){let{topic:e,event:s,payload:a,ref:l,join_ref:u}=n;this.log("push",`${e} ${s} (${u}, ${l})`,a)}this.isConnected()?this.encode(n,e=>this.conn.send(e)):this.sendBuffer.push(()=>this.encode(n,e=>this.conn.send(e)))}makeRef(){let n=this.ref+1;return n===this.ref?this.ref=0:this.ref=n,this.ref.toString()}sendHeartbeat(){if(!this.isConnected()){try{this.heartbeatCallback("disconnected")}catch(n){this.log("error","error in heartbeat callback",n)}return}if(this.pendingHeartbeatRef){this.heartbeatTimeout();return}this.pendingHeartbeatRef=this.makeRef(),this.heartbeatSentAt=Date.now(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef});try{this.heartbeatCallback("sent")}catch(n){this.log("error","error in heartbeat callback",n)}this.heartbeatTimeoutTimer=setTimeout(()=>this.heartbeatTimeout(),this.heartbeatIntervalMs)}flushSendBuffer(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach(n=>n()),this.sendBuffer=[])}onConnMessage(n){this.decode(n.data,e=>{let{topic:s,event:a,payload:l,ref:u,join_ref:c}=e;if(u&&u===this.pendingHeartbeatRef){const d=this.heartbeatSentAt?Date.now()-this.heartbeatSentAt:void 0;this.clearHeartbeats();try{this.heartbeatCallback(l.status==="ok"?"ok":"error",d)}catch(m){this.log("error","error in heartbeat callback",m)}this.pendingHeartbeatRef=null,this.heartbeatSentAt=null,this.autoSendHeartbeat&&(this.heartbeatTimer=setTimeout(()=>this.sendHeartbeat(),this.heartbeatIntervalMs))}this.hasLogger()&&this.log("receive",`${l.status||""} ${s} ${a} ${u&&"("+u+")"||""}`.trim(),l);for(let d=0;d{try{a(...e)}catch(l){this.log("error",`error in ${n} callback`,l)}})}catch(s){this.log("error",`error triggering ${n} callbacks`,s)}}leaveOpenTopic(n){let e=this.channels.find(s=>s.topic===n&&(s.isJoined()||s.isJoining()));e&&(this.hasLogger()&&this.log("transport",`leaving duplicate topic "${n}"`),e.leave())}};class Rr{constructor(e,s){const a=IC(s);this.presence=new HC(e.getChannel(),a),this.presence.onJoin((l,u,c)=>{const d=Rr.onJoinPayload(l,u,c);e.getChannel().trigger("presence",d)}),this.presence.onLeave((l,u,c)=>{const d=Rr.onLeavePayload(l,u,c);e.getChannel().trigger("presence",d)}),this.presence.onSync(()=>{e.getChannel().trigger("presence",{event:"sync"})})}get state(){return Rr.transformState(this.presence.state)}static transformState(e){return e=qC(e),Object.getOwnPropertyNames(e).reduce((s,a)=>{const l=e[a];return s[a]=zl(l),s},{})}static onJoinPayload(e,s,a){const l=_v(s),u=zl(a);return{event:"join",key:e,currentPresences:l,newPresences:u}}static onLeavePayload(e,s,a){const l=_v(s),u=zl(a);return{event:"leave",key:e,currentPresences:l,leftPresences:u}}}function zl(n){return n.metas.map(e=>(e.presence_ref=e.phx_ref,delete e.phx_ref,delete e.phx_ref_prev,e))}function qC(n){return JSON.parse(JSON.stringify(n))}function IC(n){return(n==null?void 0:n.events)&&{events:n.events}}function _v(n){return n!=null&&n.metas?zl(n):[]}var xv;(function(n){n.SYNC="sync",n.JOIN="join",n.LEAVE="leave"})(xv||(xv={}));class GC{get state(){return this.presenceAdapter.state}constructor(e,s){this.channel=e,this.presenceAdapter=new Rr(this.channel.channelAdapter,s)}}function KC(n){if(n instanceof Error)return n;if(typeof n=="string")return new Error(n);if(n&&typeof n=="object"){const e=n;if(typeof e.code=="number"){const s=typeof e.reason=="string"&&e.reason?` (${e.reason})`:"";return new Error(`socket closed: ${e.code}${s}`,{cause:n})}return new Error("channel error: transport failure",{cause:n})}return new Error("channel error: connection lost")}class FC{constructor(e,s,a){const l=YC(a);this.channel=e.getSocket().channel(s,l),this.socket=e}get state(){return this.channel.state}set state(e){this.channel.state=e}get joinedOnce(){return this.channel.joinedOnce}get joinPush(){return this.channel.joinPush}get rejoinTimer(){return this.channel.rejoinTimer}on(e,s){return this.channel.on(e,s)}off(e,s){this.channel.off(e,s)}subscribe(e){return this.channel.join(e)}unsubscribe(e){return this.channel.leave(e)}teardown(){this.channel.teardown()}onClose(e){this.channel.onClose(e)}onError(e){return this.channel.onError(e)}push(e,s,a){let l;try{l=this.channel.push(e,s,a)}catch{throw new Error(`tried to push '${e}' to '${this.channel.topic}' before joining. Use channel.subscribe() before pushing events`)}if(this.channel.pushBuffer.length>AC){const u=this.channel.pushBuffer.shift();u.cancelTimeout(),this.socket.log("channel",`discarded push due to buffer overflow: ${u.event}`,u.payload())}return l}updateJoinPayload(e){const s=this.channel.joinPush.payload();this.channel.joinPush.payload=()=>Object.assign(Object.assign({},s),e)}canPush(){return this.socket.isConnected()&&this.state===Mi.joined}isJoined(){return this.state===Mi.joined}isJoining(){return this.state===Mi.joining}isClosed(){return this.state===Mi.closed}isLeaving(){return this.state===Mi.leaving}updateFilterBindings(e){this.channel.filterBindings=e}updatePayloadTransform(e){this.channel.onMessage=e}getChannel(){return this.channel}}function YC(n){return{config:Object.assign({broadcast:{ack:!1,self:!1},presence:{key:"",enabled:!1},private:!1},n.config)}}const XC=/[,()"\\]/,JC=n=>XC.test(n)||n!==n.trim(),QC=n=>`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`,Sv=n=>{const e=n===null?"null":String(n);return JC(e)?QC(e):e},ZC=n=>n===null?"null":String(n),WC=(n,e)=>{if(n==="in"){const s=Array.isArray(e)?e:[e];if(s.length===0)throw new Error("Realtime `in` filter requires at least one value.");return`in.(${Array.from(new Set(s)).map(l=>Sv(l)).join(",")})`}return n==="is"?`is.${ZC(e)}`:`${n}.${Sv(e)}`};class ek{constructor(){this.filters=[]}add(e,s,a,l=!1){const u=l?"not.":"";return this.filters.push(`${e}=${u}${WC(s,a)}`),this}eq(e,s){return this.add(e,"eq",s)}neq(e,s){return this.add(e,"neq",s)}gt(e,s){return this.add(e,"gt",s)}gte(e,s){return this.add(e,"gte",s)}lt(e,s){return this.add(e,"lt",s)}lte(e,s){return this.add(e,"lte",s)}in(e,s){return this.add(e,"in",s)}like(e,s){return this.add(e,"like",s)}ilike(e,s){return this.add(e,"ilike",s)}match(e,s){return this.add(e,"match",s)}imatch(e,s){return this.add(e,"imatch",s)}is(e,s){return this.add(e,"is",s)}isDistinct(e,s){return this.add(e,"isdistinct",s)}not(e,s,a){return this.add(e,s,a,!0)}build(){return this.filters.join(",")}toString(){return this.build()}}var Tv;(function(n){n.ALL="*",n.INSERT="INSERT",n.UPDATE="UPDATE",n.DELETE="DELETE"})(Tv||(Tv={}));var da;(function(n){n.BROADCAST="broadcast",n.PRESENCE="presence",n.POSTGRES_CHANGES="postgres_changes",n.SYSTEM="system"})(da||(da={}));var ai;(function(n){n.SUBSCRIBED="SUBSCRIBED",n.TIMED_OUT="TIMED_OUT",n.CLOSED="CLOSED",n.CHANNEL_ERROR="CHANNEL_ERROR"})(ai||(ai={}));class Cr{get state(){return this.channelAdapter.state}set state(e){this.channelAdapter.state=e}get joinedOnce(){return this.channelAdapter.joinedOnce}get timeout(){return this.socket.timeout}get joinPush(){return this.channelAdapter.joinPush}get rejoinTimer(){return this.channelAdapter.rejoinTimer}constructor(e,s={config:{}},a){var l,u;if(this.topic=e,this.params=s,this.socket=a,this.bindings={},this.subTopic=e.replace(/^realtime:/i,""),this.params.config=Object.assign({broadcast:{ack:!1,self:!1},presence:{key:"",enabled:!1},private:!1},s.config),this.channelAdapter=new FC(this.socket.socketAdapter,e,this.params),this.presence=new GC(this),this._onClose(()=>{this.socket._remove(this)}),this._updateFilterTransform(),this.broadcastEndpointURL=Qw(this.socket.socketAdapter.endPointURL()),this.private=this.params.config.private||!1,!this.private&&(!((u=(l=this.params.config)===null||l===void 0?void 0:l.broadcast)===null||u===void 0)&&u.replay))throw new Error(`tried to use replay on public channel '${this.topic}'. It must be a private channel.`)}subscribe(e,s=this.timeout){var a,l,u;if(this.socket.isConnected()||this.socket.connect(),this.channelAdapter.isClosed()){const{config:{broadcast:c,presence:d,private:m}}=this.params,p=(l=(a=this.bindings.postgres_changes)===null||a===void 0?void 0:a.map(x=>x.filter))!==null&&l!==void 0?l:[],g=!!this.bindings[da.PRESENCE]&&this.bindings[da.PRESENCE].length>0||((u=this.params.config.presence)===null||u===void 0?void 0:u.enabled)===!0,v={},w={broadcast:c,presence:Object.assign(Object.assign({},d),{enabled:g}),postgres_changes:p,private:m};this.socket.accessTokenValue&&(v.access_token=this.socket.accessTokenValue),this._onError(x=>{e==null||e(ai.CHANNEL_ERROR,KC(x))}),this._onClose(()=>e==null?void 0:e(ai.CLOSED)),this.updateJoinPayload(Object.assign({config:w},v)),this._updateFilterMessage(),this.channelAdapter.subscribe(s).receive("ok",async({postgres_changes:x})=>{if(this.socket._isManualToken()||this.socket.setAuth(),x===void 0){e==null||e(ai.SUBSCRIBED);return}this._updatePostgresBindings(x,e)}).receive("error",x=>{this.state=Mi.errored;const S=Object.values(x).join(", ")||"error";e==null||e(ai.CHANNEL_ERROR,new Error(S,{cause:x}))}).receive("timeout",()=>{e==null||e(ai.TIMED_OUT)})}return this}_updatePostgresBindings(e,s){var a;const l=this.bindings.postgres_changes,u=(a=l==null?void 0:l.length)!==null&&a!==void 0?a:0,c=[];for(let d=0;d{var c,d,m;const p=this.channelAdapter.push(e.type,e,s.timeout||this.timeout);e.type==="broadcast"&&!(!((m=(d=(c=this.params)===null||c===void 0?void 0:c.config)===null||d===void 0?void 0:d.broadcast)===null||m===void 0)&&m.ack)&&u("ok"),p.receive("ok",()=>u("ok")),p.receive("error",()=>u("error")),p.receive("timeout",()=>u("timed out"))})}updateJoinPayload(e){this.channelAdapter.updateJoinPayload(e)}async unsubscribe(e=this.timeout){return new Promise(s=>{this.channelAdapter.unsubscribe(e).receive("ok",()=>s("ok")).receive("timeout",()=>s("timed out")).receive("error",()=>s("error"))})}teardown(){this.channelAdapter.teardown()}async _fetchWithTimeout(e,s,a){const l=new AbortController,u=setTimeout(()=>l.abort(),a),c=await this.socket.fetch(e,Object.assign(Object.assign({},s),{signal:l.signal}));return clearTimeout(u),c}_on(e,s,a){const l=e.toLocaleLowerCase(),u=s==null?void 0:s.filter;(u instanceof ek||typeof u=="object"&&u!==null&&typeof u.build=="function")&&(s=Object.assign(Object.assign({},s),{filter:u.build()}));const c=this.channelAdapter.on(e,a),d={type:l,filter:s,callback:a,ref:c};return this.bindings[l]?this.bindings[l].push(d):this.bindings[l]=[d],this._updateFilterMessage(),this}_onClose(e){this.channelAdapter.onClose(e)}_onError(e){this.channelAdapter.onError(e)}_updateFilterMessage(){this.channelAdapter.updateFilterBindings((e,s,a)=>{var l,u,c,d,m,p,g;const v=e.event.toLocaleLowerCase();if(this._notThisChannelEvent(v,a))return!1;const w=(l=this.bindings[v])===null||l===void 0?void 0:l.find(x=>x.ref===e.ref);if(!w)return!0;if(["broadcast","presence","postgres_changes"].includes(v))if("id"in w){const x=w.id,S=(u=w.filter)===null||u===void 0?void 0:u.event;return x&&((c=s.ids)===null||c===void 0?void 0:c.includes(x))&&(S==="*"||(S==null?void 0:S.toLocaleLowerCase())===((d=s.data)===null||d===void 0?void 0:d.type.toLocaleLowerCase()))}else{const x=(p=(m=w==null?void 0:w.filter)===null||m===void 0?void 0:m.event)===null||p===void 0?void 0:p.toLocaleLowerCase();return x==="*"||x===((g=s==null?void 0:s.event)===null||g===void 0?void 0:g.toLocaleLowerCase())}else return w.type.toLocaleLowerCase()===v})}_notThisChannelEvent(e,s){const{close:a,error:l,leave:u,join:c}=Xw;return s&&[a,l,u,c].includes(e)&&s!==this.joinPush.ref}_updateFilterTransform(){this.channelAdapter.updatePayloadTransform((e,s,a)=>{if(typeof s=="object"&&"ids"in s){const l=s.data,{schema:u,table:c,commit_timestamp:d,type:m,errors:p}=l;return Object.assign(Object.assign({},{schema:u,table:c,commit_timestamp:d,eventType:m,new:{},old:{},errors:p}),this._getPayloadRecords(l))}return s})}copyBindings(e){if(this.joinedOnce)throw new Error("cannot copy bindings into joined channel");for(const s in e.bindings)for(const a of e.bindings[s])this._on(a.type,a.filter,a.callback)}static isFilterValueEqual(e,s){return(e??void 0)===(s??void 0)}_getPayloadRecords(e){const s={new:{},old:{}};return(e.type==="INSERT"||e.type==="UPDATE")&&(s.new=wv(e.columns,e.record)),(e.type==="UPDATE"||e.type==="DELETE")&&(s.old=wv(e.columns,e.old_record)),s}}class tk{constructor(e,s){this.socket=new $C(e,s)}get timeout(){return this.socket.timeout}get endPoint(){return this.socket.endPoint}get transport(){return this.socket.transport}get heartbeatIntervalMs(){return this.socket.heartbeatIntervalMs}get heartbeatCallback(){return this.socket.heartbeatCallback}set heartbeatCallback(e){this.socket.heartbeatCallback=e}get heartbeatTimer(){return this.socket.heartbeatTimer}get pendingHeartbeatRef(){return this.socket.pendingHeartbeatRef}get reconnectTimer(){return this.socket.reconnectTimer}get vsn(){return this.socket.vsn}get encode(){return this.socket.encode}get decode(){return this.socket.decode}get reconnectAfterMs(){return this.socket.reconnectAfterMs}get sendBuffer(){return this.socket.sendBuffer}get stateChangeCallbacks(){return this.socket.stateChangeCallbacks}connect(){this.socket.connect()}disconnect(e,s,a,l=1e4){return new Promise(u=>{setTimeout(()=>u("timeout"),l),this.socket.disconnect(()=>{e(),u("ok")},s,a)})}push(e){this.socket.push(e)}log(e,s,a){this.socket.log(e,s,a)}makeRef(){return this.socket.makeRef()}onOpen(e){this.socket.onOpen(e)}onClose(e){this.socket.onClose(e)}onError(e){this.socket.onError(e)}onMessage(e){this.socket.onMessage(e)}isConnected(){return this.socket.isConnected()}isConnecting(){return this.socket.connectionState()==Od.connecting}isDisconnecting(){return this.socket.connectionState()==Od.closing}connectionState(){return this.socket.connectionState()}endPointURL(){return this.socket.endPointURL()}sendHeartbeat(){this.socket.sendHeartbeat()}getSocket(){return this.socket}}const Ev={HEARTBEAT_INTERVAL:25e3},nk=[1e3,2e3,5e3,1e4],ik=1e4;function sk(){const n=new Map;return{get length(){return n.size},clear(){n.clear()},getItem(e){return n.has(e)?n.get(e):null},key(e){var s;return(s=Array.from(n.keys())[e])!==null&&s!==void 0?s:null},removeItem(e){n.delete(e)},setItem(e,s){n.set(e,String(s))}}}function ak(){try{if(typeof globalThis<"u"&&globalThis.sessionStorage)return globalThis.sessionStorage}catch{}return sk()}const rk=` - addEventListener("message", (e) => { - if (e.data.event === "start") { - setInterval(() => postMessage({ event: "keepAlive" }), e.data.interval); - } - });`;class ok{get endPoint(){return this.socketAdapter.endPoint}get timeout(){return this.socketAdapter.timeout}get transport(){return this.socketAdapter.transport}get heartbeatCallback(){return this.socketAdapter.heartbeatCallback}get heartbeatIntervalMs(){return this.socketAdapter.heartbeatIntervalMs}get heartbeatTimer(){return this.worker?this._workerHeartbeatTimer:this.socketAdapter.heartbeatTimer}get pendingHeartbeatRef(){return this.worker?this._pendingWorkerHeartbeatRef:this.socketAdapter.pendingHeartbeatRef}get reconnectTimer(){return this.socketAdapter.reconnectTimer}get vsn(){return this.socketAdapter.vsn}get encode(){return this.socketAdapter.encode}get decode(){return this.socketAdapter.decode}get reconnectAfterMs(){return this.socketAdapter.reconnectAfterMs}get sendBuffer(){return this.socketAdapter.sendBuffer}get stateChangeCallbacks(){return this.socketAdapter.stateChangeCallbacks}constructor(e,s){var a;if(this.channels=new Array,this.accessTokenValue=null,this.accessToken=null,this.apiKey=null,this.httpEndpoint="",this.headers={},this.params={},this.ref=0,this.serializer=new RC,this._manuallySetToken=!1,this._authPromise=null,this._workerHeartbeatTimer=void 0,this._pendingWorkerHeartbeatRef=null,this._pendingDisconnectTimer=null,this._disconnectOnEmptyChannelsAfterMs=0,this._resolveFetch=u=>u?(...c)=>u(...c):(...c)=>fetch(...c),!(!((a=s==null?void 0:s.params)===null||a===void 0)&&a.apikey))throw new Error("API key is required to connect to Realtime");this.apiKey=s.params.apikey;const l=this._initializeOptions(s);this.socketAdapter=new tk(e,l),this.httpEndpoint=Qw(e),this.fetch=this._resolveFetch(s==null?void 0:s.fetch)}connect(){if(!(this.isConnecting()||this.isDisconnecting()||this.isConnected())){this.accessToken&&!this._authPromise&&this._setAuthSafely("connect"),this._setupConnectionHandlers();try{this.socketAdapter.connect()}catch(e){const s=e.message;throw new Error(`WebSocket not available: ${s}`)}this._handleNodeJsRaceCondition()}}endpointURL(){return this.socketAdapter.endPointURL()}async disconnect(e,s){return this._cancelPendingDisconnect(),this.isDisconnecting()?"ok":await this.socketAdapter.disconnect(()=>{clearInterval(this._workerHeartbeatTimer),this._terminateWorker()},e,s)}getChannels(){return this.channels}async removeChannel(e){const s=await e.unsubscribe();return s==="ok"&&e.teardown(),s}async removeAllChannels(){const e=this.channels.map(async a=>{const l=await a.unsubscribe();return a.teardown(),l}),s=await Promise.all(e);return await this.disconnect(),s}log(e,s,a){this.socketAdapter.log(e,s,a)}connectionState(){return this.socketAdapter.connectionState()||Od.closed}isConnected(){return this.socketAdapter.isConnected()}isConnecting(){return this.socketAdapter.isConnecting()}isDisconnecting(){return this.socketAdapter.isDisconnecting()}channel(e,s={config:{}}){const a=`realtime:${e}`,l=this.getChannels().find(u=>u.topic===a);if(l)return l;{const u=new Cr(`realtime:${e}`,s,this);return this._cancelPendingDisconnect(),this.channels.push(u),u}}push(e){this.socketAdapter.push(e)}async setAuth(e=null){this._authPromise=this._performAuth(e);try{await this._authPromise}finally{this._authPromise=null}}_isManualToken(){return this._manuallySetToken}async sendHeartbeat(){this.socketAdapter.sendHeartbeat()}onHeartbeat(e){this.socketAdapter.heartbeatCallback=this._wrapHeartbeatCallback(e)}_makeRef(){return this.socketAdapter.makeRef()}_remove(e){this.channels=this.channels.filter(s=>s.topic!==e.topic),this.channels.length===0&&(this.log("transport","no channels remaining, scheduling disconnect"),this._schedulePendingDisconnect())}_schedulePendingDisconnect(){if(this._cancelPendingDisconnect(),this._disconnectOnEmptyChannelsAfterMs===0){this.log("transport","disconnecting immediately - no channels"),this.disconnect();return}this._pendingDisconnectTimer=setTimeout(()=>{this._pendingDisconnectTimer=null,this.channels.length===0&&(this.log("transport","deferred disconnect fired - no channels, disconnecting"),this.disconnect())},this._disconnectOnEmptyChannelsAfterMs),this.log("transport",`deferred disconnect scheduled in ${this._disconnectOnEmptyChannelsAfterMs}ms`)}_cancelPendingDisconnect(){this._pendingDisconnectTimer!==null&&(this.log("transport","pending disconnect cancelled - channel activity detected"),clearTimeout(this._pendingDisconnectTimer),this._pendingDisconnectTimer=null)}async _performAuth(e=null){let s,a=!1;if(e)s=e,a=!0;else if(this.accessToken)try{s=await this.accessToken()}catch(l){this.log("error","Error fetching access token from callback",l),s=this.accessTokenValue}else s=this.accessTokenValue;a?this._manuallySetToken=!0:this.accessToken&&(this._manuallySetToken=!1),this.accessTokenValue!=s&&(this.accessTokenValue=s,this.channels.forEach(l=>{const u={access_token:s,version:xC};s&&l.updateJoinPayload(u),l.joinedOnce&&l.channelAdapter.isJoined()&&l.channelAdapter.push(Xw.access_token,{access_token:s})}))}async _waitForAuthIfNeeded(){this._authPromise&&await this._authPromise}_setAuthSafely(e="general"){this._isManualToken()||this.setAuth().catch(s=>{this.log("error",`Error setting auth in ${e}`,s)})}_setupConnectionHandlers(){this.socketAdapter.onOpen(()=>{(this._authPromise||(this.accessToken&&!this.accessTokenValue?this.setAuth():Promise.resolve())).catch(s=>{this.log("error","error waiting for auth on connect",s)}),this.worker&&!this.workerRef&&this._startWorkerHeartbeat()}),this.socketAdapter.onClose(()=>{this.worker&&this.workerRef&&this._terminateWorker()}),this.socketAdapter.onMessage(e=>{e.ref&&e.ref===this._pendingWorkerHeartbeatRef&&(this._pendingWorkerHeartbeatRef=null)})}_handleNodeJsRaceCondition(){this.socketAdapter.isConnected()&&this.socketAdapter.getSocket().onConnOpen()}_wrapHeartbeatCallback(e){return(s,a)=>{s!=="disconnected"&&(s=="sent"&&this._setAuthSafely(),e&&e(s,a))}}_startWorkerHeartbeat(){this.workerUrl?this.log("worker",`starting worker for from ${this.workerUrl}`):this.log("worker","starting default worker");const e=this._workerObjectUrl(this.workerUrl);this.workerRef=new Worker(e),this.workerRef.onerror=s=>{this.log("worker","worker error",s.message),this._terminateWorker(),this.disconnect()},this.workerRef.onmessage=s=>{s.data.event==="keepAlive"&&this.sendHeartbeat()},this.workerRef.postMessage({event:"start",interval:this.heartbeatIntervalMs})}_terminateWorker(){this.workerRef&&(this.log("worker","terminating worker"),this.workerRef.terminate(),this.workerRef=void 0)}_workerObjectUrl(e){let s;if(e)s=e;else{const a=new Blob([rk],{type:"application/javascript"});s=URL.createObjectURL(a)}return s}_initializeOptions(e){var s,a,l,u,c,d,m,p,g,v,w,x;this.worker=(s=e==null?void 0:e.worker)!==null&&s!==void 0?s:!1,this.accessToken=(a=e==null?void 0:e.accessToken)!==null&&a!==void 0?a:null;const S={};S.timeout=(l=e==null?void 0:e.timeout)!==null&&l!==void 0?l:EC,S.heartbeatIntervalMs=(u=e==null?void 0:e.heartbeatIntervalMs)!==null&&u!==void 0?u:Ev.HEARTBEAT_INTERVAL,this._disconnectOnEmptyChannelsAfterMs=(c=e==null?void 0:e.disconnectOnEmptyChannelsAfterMs)!==null&&c!==void 0?c:2*((d=e==null?void 0:e.heartbeatIntervalMs)!==null&&d!==void 0?d:Ev.HEARTBEAT_INTERVAL),S.transport=(m=e==null?void 0:e.transport)!==null&&m!==void 0?m:wC.getWebSocketConstructor(),S.params=e==null?void 0:e.params,S.logger=e==null?void 0:e.logger,S.heartbeatCallback=this._wrapHeartbeatCallback(e==null?void 0:e.heartbeatCallback),S.sessionStorage=(p=e==null?void 0:e.sessionStorage)!==null&&p!==void 0?p:ak(),S.reconnectAfterMs=(g=e==null?void 0:e.reconnectAfterMs)!==null&&g!==void 0?g:(N=>nk[N-1]||ik);let A,k;const C=(v=e==null?void 0:e.vsn)!==null&&v!==void 0?v:TC;switch(C){case SC:A=(N,O)=>O(JSON.stringify(N)),k=(N,O)=>O(JSON.parse(N));break;case Yw:A=this.serializer.encode.bind(this.serializer),k=this.serializer.decode.bind(this.serializer);break;default:throw new Error(`Unsupported serializer version: ${S.vsn}`)}if(S.vsn=C,S.encode=(w=e==null?void 0:e.encode)!==null&&w!==void 0?w:A,S.decode=(x=e==null?void 0:e.decode)!==null&&x!==void 0?x:k,S.beforeReconnect=this._reconnectAuth.bind(this),(e!=null&&e.logLevel||e!=null&&e.log_level)&&(this.logLevel=e.logLevel||e.log_level,S.params=Object.assign(Object.assign({},S.params),{log_level:this.logLevel})),this.worker){if(typeof window<"u"&&!window.Worker)throw new Error("Web Worker is not supported");this.workerUrl=e==null?void 0:e.workerUrl,S.autoSendHeartbeat=!this.worker}return S}async _reconnectAuth(){await this._waitForAuthIfNeeded(),this.isConnected()||this.connect()}}var Br=class extends Error{constructor(n,e){var s;super(n),this.name="IcebergError",this.status=e.status,this.icebergType=e.icebergType,this.icebergCode=e.icebergCode,this.details=e.details,this.isCommitStateUnknown=e.icebergType==="CommitStateUnknownException"||[500,502,504].includes(e.status)&&((s=e.icebergType)==null?void 0:s.includes("CommitState"))===!0}isNotFound(){return this.status===404}isConflict(){return this.status===409}isAuthenticationTimeout(){return this.status===419}};function lk(n,e,s){const a=new URL(e,n);if(s)for(const[l,u]of Object.entries(s))u!==void 0&&a.searchParams.set(l,u);return a.toString()}async function uk(n){return!n||n.type==="none"?{}:n.type==="bearer"?{Authorization:`Bearer ${n.token}`}:n.type==="header"?{[n.name]:n.value}:n.type==="custom"?await n.getHeaders():{}}function ck(n){const e=n.fetchImpl??globalThis.fetch;return{async request({method:s,path:a,query:l,body:u,headers:c}){const d=lk(n.baseUrl,a,l),m=await uk(n.auth),p=await e(d,{method:s,headers:{...u?{"Content-Type":"application/json"}:{},...m,...c},body:u?JSON.stringify(u):void 0}),g=await p.text(),v=(p.headers.get("content-type")||"").includes("application/json"),w=v&&g?JSON.parse(g):g;if(!p.ok){const x=v?w:void 0,S=x==null?void 0:x.error;throw new Br((S==null?void 0:S.message)??`Request failed with status ${p.status}`,{status:p.status,icebergType:S==null?void 0:S.type,icebergCode:S==null?void 0:S.code,details:x})}return{status:p.status,headers:p.headers,data:w}}}}function Sl(n){return n.join("")}var hk=class{constructor(n,e=""){this.client=n,this.prefix=e}async listNamespaces(n){const e=n?{parent:Sl(n.namespace)}:void 0;return(await this.client.request({method:"GET",path:`${this.prefix}/namespaces`,query:e})).data.namespaces.map(a=>({namespace:a}))}async createNamespace(n,e){const s={namespace:n.namespace,properties:e==null?void 0:e.properties};return(await this.client.request({method:"POST",path:`${this.prefix}/namespaces`,body:s})).data}async dropNamespace(n){await this.client.request({method:"DELETE",path:`${this.prefix}/namespaces/${Sl(n.namespace)}`})}async loadNamespaceMetadata(n){return{properties:(await this.client.request({method:"GET",path:`${this.prefix}/namespaces/${Sl(n.namespace)}`})).data.properties}}async namespaceExists(n){try{return await this.client.request({method:"HEAD",path:`${this.prefix}/namespaces/${Sl(n.namespace)}`}),!0}catch(e){if(e instanceof Br&&e.status===404)return!1;throw e}}async createNamespaceIfNotExists(n,e){try{return await this.createNamespace(n,e)}catch(s){if(s instanceof Br&&s.status===409)return;throw s}}};function ta(n){return n.join("")}var dk=class{constructor(n,e="",s){this.client=n,this.prefix=e,this.accessDelegation=s}async listTables(n){return(await this.client.request({method:"GET",path:`${this.prefix}/namespaces/${ta(n.namespace)}/tables`})).data.identifiers}async createTable(n,e){const s={};return this.accessDelegation&&(s["X-Iceberg-Access-Delegation"]=this.accessDelegation),(await this.client.request({method:"POST",path:`${this.prefix}/namespaces/${ta(n.namespace)}/tables`,body:e,headers:s})).data.metadata}async updateTable(n,e){const s=await this.client.request({method:"POST",path:`${this.prefix}/namespaces/${ta(n.namespace)}/tables/${n.name}`,body:e});return{"metadata-location":s.data["metadata-location"],metadata:s.data.metadata}}async dropTable(n,e){await this.client.request({method:"DELETE",path:`${this.prefix}/namespaces/${ta(n.namespace)}/tables/${n.name}`,query:{purgeRequested:String((e==null?void 0:e.purge)??!1)}})}async loadTable(n){const e={};return this.accessDelegation&&(e["X-Iceberg-Access-Delegation"]=this.accessDelegation),(await this.client.request({method:"GET",path:`${this.prefix}/namespaces/${ta(n.namespace)}/tables/${n.name}`,headers:e})).data.metadata}async tableExists(n){const e={};this.accessDelegation&&(e["X-Iceberg-Access-Delegation"]=this.accessDelegation);try{return await this.client.request({method:"HEAD",path:`${this.prefix}/namespaces/${ta(n.namespace)}/tables/${n.name}`,headers:e}),!0}catch(s){if(s instanceof Br&&s.status===404)return!1;throw s}}async createTableIfNotExists(n,e){try{return await this.createTable(n,e)}catch(s){if(s instanceof Br&&s.status===409)return await this.loadTable({namespace:n.namespace,name:e.name});throw s}}},fk=class{constructor(n){var a;let e="v1";n.catalogName&&(e+=`/${n.catalogName}`);const s=n.baseUrl.endsWith("/")?n.baseUrl:`${n.baseUrl}/`;this.client=ck({baseUrl:s,auth:n.auth,fetchImpl:n.fetch}),this.accessDelegation=(a=n.accessDelegation)==null?void 0:a.join(","),this.namespaceOps=new hk(this.client,e),this.tableOps=new dk(this.client,e,this.accessDelegation)}async listNamespaces(n){return this.namespaceOps.listNamespaces(n)}async createNamespace(n,e){return this.namespaceOps.createNamespace(n,e)}async dropNamespace(n){await this.namespaceOps.dropNamespace(n)}async loadNamespaceMetadata(n){return this.namespaceOps.loadNamespaceMetadata(n)}async listTables(n){return this.tableOps.listTables(n)}async createTable(n,e){return this.tableOps.createTable(n,e)}async updateTable(n,e){return this.tableOps.updateTable(n,e)}async dropTable(n,e){await this.tableOps.dropTable(n,e)}async loadTable(n){return this.tableOps.loadTable(n)}async namespaceExists(n){return this.namespaceOps.namespaceExists(n)}async tableExists(n){return this.tableOps.tableExists(n)}async createNamespaceIfNotExists(n,e){return this.namespaceOps.createNamespaceIfNotExists(n,e)}async createTableIfNotExists(n,e){return this.tableOps.createTableIfNotExists(n,e)}};function Lr(n){"@babel/helpers - typeof";return Lr=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Lr(n)}function mk(n,e){if(Lr(n)!="object"||!n)return n;var s=n[Symbol.toPrimitive];if(s!==void 0){var a=s.call(n,e);if(Lr(a)!="object")return a;throw new TypeError("@@toPrimitive must return a primitive value.")}return(e==="string"?String:Number)(n)}function pk(n){var e=mk(n,"string");return Lr(e)=="symbol"?e:e+""}function gk(n,e,s){return(e=pk(e))in n?Object.defineProperty(n,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):n[e]=s,n}function Av(n,e){var s=Object.keys(n);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(n);e&&(a=a.filter(function(l){return Object.getOwnPropertyDescriptor(n,l).enumerable})),s.push.apply(s,a)}return s}function fe(n){for(var e=1;en?(...e)=>n(...e):(...e)=>fetch(...e),bk=n=>{if(typeof n!="object"||n===null)return!1;const e=Object.getPrototypeOf(n);return(e===null||e===Object.prototype||Object.getPrototypeOf(e)===null)&&!(Symbol.toStringTag in n)&&!(Symbol.iterator in n)},Bd=n=>{if(Array.isArray(n))return n.map(s=>Bd(s));if(typeof n=="function"||n!==Object(n))return n;const e={};return Object.entries(n).forEach(([s,a])=>{const l=s.replace(/([-_][a-z])/gi,u=>u.toUpperCase().replace(/[-_]/g,""));e[l]=Bd(a)}),e},wk=n=>!n||typeof n!="string"||n.length===0||n.length>100||n.trim()!==n||n.includes("/")||n.includes("\\")?!1:/^[\w!.\*'() &$@=;:+,?-]+$/.test(n),Rv=n=>{if(typeof n=="object"&&n!==null){const e=n;if(typeof e.msg=="string")return e.msg;if(typeof e.message=="string")return e.message;if(typeof e.error_description=="string")return e.error_description;if(typeof e.error=="string")return e.error;if(typeof e.error=="object"&&e.error!==null){const s=e.error;if(typeof s.message=="string")return s.message}}return JSON.stringify(n)},_k=async(n,e,s,a)=>{if(n!==null&&typeof n=="object"&&"json"in n&&typeof n.json=="function"){const l=n;let u=parseInt(String(l.status),10);Number.isFinite(u)||(u=500),l.json().then(c=>{const d=(c==null?void 0:c.statusCode)||(c==null?void 0:c.code)||u+"";e(new Ud(Rv(c),u,d,a))}).catch(()=>{const c=u+"";e(new Ud(l.statusText||`HTTP ${u} error`,u,c,a))})}else e(new Ww(Rv(n),n,a))},xk=(n,e,s,a)=>{const l={method:n,headers:(e==null?void 0:e.headers)||{}};if(n==="GET"||n==="HEAD"||!a)return fe(fe({},l),s);if(bk(a)){var u;const c=(e==null?void 0:e.headers)||{};let d;for(const[m,p]of Object.entries(c))m.toLowerCase()==="content-type"&&(d=p);l.headers=Jl(c,"Content-Type",(u=d)!==null&&u!==void 0?u:"application/json"),l.body=JSON.stringify(a)}else l.body=a;return e!=null&&e.duplex&&(l.duplex=e.duplex),fe(fe({},l),s)};async function gr(n,e,s,a,l,u,c){return new Promise((d,m)=>{n(s,xk(e,a,l,u)).then(p=>{if(!p.ok)throw p;if(a!=null&&a.noResolveJson)return p;if(c==="vectors"){const g=p.headers.get("content-type");if(p.headers.get("content-length")==="0"||p.status===204)return{};if(!g||!g.includes("application/json"))return{}}return p.json()}).then(p=>d(p)).catch(p=>_k(p,m,a,c))})}function e_(n="storage"){return{get:async(e,s,a,l)=>gr(e,"GET",s,a,l,void 0,n),post:async(e,s,a,l,u)=>gr(e,"POST",s,l,u,a,n),put:async(e,s,a,l,u)=>gr(e,"PUT",s,l,u,a,n),head:async(e,s,a,l)=>gr(e,"HEAD",s,fe(fe({},a),{},{noResolveJson:!0}),l,void 0,n),remove:async(e,s,a,l,u)=>gr(e,"DELETE",s,l,u,a,n)}}const Sk=e_("storage"),{get:zr,post:yn,put:Ld,head:Tk,remove:Vr}=Sk,Kt=e_("vectors");var wa=class{constructor(n,e={},s,a="storage"){this.shouldThrowOnError=!1,this.url=n,this.headers=yk(e),this.fetch=vk(s),this.namespace=a}throwOnError(){return this.shouldThrowOnError=!0,this}setHeader(n,e){return this.headers=Jl(this.headers,n,e),this}async handleOperation(n){var e=this;try{return{data:await n(),error:null}}catch(s){if(e.shouldThrowOnError)throw s;if(cu(s))return{data:null,error:s};throw s}}};let t_;t_=Symbol.toStringTag;var Ek=class{constructor(n,e){this.downloadFn=n,this.shouldThrowOnError=e,this[t_]="StreamDownloadBuilder",this.promise=null}then(n,e){return this.getPromise().then(n,e)}catch(n){return this.getPromise().catch(n)}finally(n){return this.getPromise().finally(n)}getPromise(){return this.promise||(this.promise=this.execute()),this.promise}async execute(){var n=this;try{return{data:(await n.downloadFn()).body,error:null}}catch(e){if(n.shouldThrowOnError)throw e;if(cu(e))return{data:null,error:e};throw e}}};let n_;n_=Symbol.toStringTag;var Ak=class{constructor(n,e){this.downloadFn=n,this.shouldThrowOnError=e,this[n_]="BlobDownloadBuilder",this.promise=null}asStream(){return new Ek(this.downloadFn,this.shouldThrowOnError)}then(n,e){return this.getPromise().then(n,e)}catch(n){return this.getPromise().catch(n)}finally(n){return this.getPromise().finally(n)}getPromise(){return this.promise||(this.promise=this.execute()),this.promise}async execute(){var n=this;try{return{data:await(await n.downloadFn()).blob(),error:null}}catch(e){if(n.shouldThrowOnError)throw e;if(cu(e))return{data:null,error:e};throw e}}};const Jh={limit:100,offset:0,sortBy:{column:"name",order:"asc"}},Cv={cacheControl:"3600",contentType:"text/plain;charset=UTF-8",upsert:!1};var Rk=class extends wa{constructor(n,e={},s,a){super(n,e,a,"storage"),this.bucketId=s}async uploadOrUpdate(n,e,s,a){var l=this;return l.handleOperation(async()=>{let u;const c=fe(fe({},Cv),a);let d=fe(fe({},l.headers),n==="POST"&&{"x-upsert":String(c.upsert)});const m=c.metadata;if(typeof Blob<"u"&&s instanceof Blob?(u=new FormData,u.append("cacheControl",c.cacheControl),m&&u.append("metadata",l.encodeMetadata(m)),u.append("",s)):typeof FormData<"u"&&s instanceof FormData?(u=s,u.has("cacheControl")||u.append("cacheControl",c.cacheControl),m&&!u.has("metadata")&&u.append("metadata",l.encodeMetadata(m))):(u=s,d["cache-control"]=`max-age=${c.cacheControl}`,d["content-type"]=c.contentType,m&&(d["x-metadata"]=l.toBase64(l.encodeMetadata(m))),(typeof ReadableStream<"u"&&u instanceof ReadableStream||u&&typeof u=="object"&&"pipe"in u&&typeof u.pipe=="function")&&!c.duplex&&(c.duplex="half")),a!=null&&a.headers)for(const[w,x]of Object.entries(a.headers))d=Jl(d,w,x);const p=l._removeEmptyFolders(e),g=l._getFinalPath(p),v=await(n=="PUT"?Ld:yn)(l.fetch,`${l.url}/object/${g}`,u,fe({headers:d},c!=null&&c.duplex?{duplex:c.duplex}:{}));return{path:p,id:v.Id,fullPath:v.Key}})}async upload(n,e,s){return this.uploadOrUpdate("POST",n,e,s)}async uploadToSignedUrl(n,e,s,a){var l=this;const u=l._removeEmptyFolders(n),c=l._getFinalPath(u),d=new URL(l.url+`/object/upload/sign/${c}`);return d.searchParams.set("token",e),l.handleOperation(async()=>{let m;const p=fe(fe({},Cv),a);let g=fe(fe({},l.headers),{"x-upsert":String(p.upsert)});const v=p.metadata;if(typeof Blob<"u"&&s instanceof Blob?(m=new FormData,m.append("cacheControl",p.cacheControl),v&&m.append("metadata",l.encodeMetadata(v)),m.append("",s)):typeof FormData<"u"&&s instanceof FormData?(m=s,m.has("cacheControl")||m.append("cacheControl",p.cacheControl),v&&!m.has("metadata")&&m.append("metadata",l.encodeMetadata(v))):(m=s,g["cache-control"]=`max-age=${p.cacheControl}`,g["content-type"]=p.contentType,v&&(g["x-metadata"]=l.toBase64(l.encodeMetadata(v))),(typeof ReadableStream<"u"&&m instanceof ReadableStream||m&&typeof m=="object"&&"pipe"in m&&typeof m.pipe=="function")&&!p.duplex&&(p.duplex="half")),a!=null&&a.headers)for(const[w,x]of Object.entries(a.headers))g=Jl(g,w,x);return{path:u,fullPath:(await Ld(l.fetch,d.toString(),m,fe({headers:g},p!=null&&p.duplex?{duplex:p.duplex}:{}))).Key}})}async createSignedUploadUrl(n,e){var s=this;return s.handleOperation(async()=>{let a=s._getFinalPath(n);const l=fe({},s.headers);e!=null&&e.upsert&&(l["x-upsert"]="true");const u=await yn(s.fetch,`${s.url}/object/upload/sign/${a}`,{},{headers:l}),c=new URL(s.url+u.url),d=c.searchParams.get("token");if(!d)throw new uu("No token returned by API");return{signedUrl:c.toString(),path:n,token:d}})}async update(n,e,s){return this.uploadOrUpdate("PUT",n,e,s)}async move(n,e,s){var a=this;return a.handleOperation(async()=>await yn(a.fetch,`${a.url}/object/move`,{bucketId:a.bucketId,sourceKey:n,destinationKey:e,destinationBucket:s==null?void 0:s.destinationBucket},{headers:a.headers}))}async copy(n,e,s){var a=this;return a.handleOperation(async()=>({path:(await yn(a.fetch,`${a.url}/object/copy`,{bucketId:a.bucketId,sourceKey:n,destinationKey:e,destinationBucket:s==null?void 0:s.destinationBucket},{headers:a.headers})).Key}))}async createSignedUrl(n,e,s){var a=this;return a.handleOperation(async()=>{let l=a._getFinalPath(n);const u=typeof(s==null?void 0:s.transform)=="object"&&s.transform!==null&&Object.keys(s.transform).length>0;let c=await yn(a.fetch,`${a.url}/object/sign/${l}`,fe({expiresIn:e},u?{transform:s.transform}:{}),{headers:a.headers});const d=new URLSearchParams;s!=null&&s.download&&d.set("download",s.download===!0?"":s.download),(s==null?void 0:s.cacheNonce)!=null&&d.set("cacheNonce",String(s.cacheNonce));const m=d.toString();return{signedUrl:encodeURI(`${a.url}${c.signedURL}${m?`&${m}`:""}`)}})}async createSignedUrls(n,e,s){var a=this;return a.handleOperation(async()=>{const l=await yn(a.fetch,`${a.url}/object/sign/${a.bucketId}`,{expiresIn:e,paths:n},{headers:a.headers}),u=new URLSearchParams;s!=null&&s.download&&u.set("download",s.download===!0?"":s.download),(s==null?void 0:s.cacheNonce)!=null&&u.set("cacheNonce",String(s.cacheNonce));const c=u.toString();return l.map(d=>fe(fe({},d),{},{signedUrl:d.signedURL?encodeURI(`${a.url}${d.signedURL}${c?`&${c}`:""}`):null}))})}download(n,e,s){const a=typeof(e==null?void 0:e.transform)=="object"&&e.transform!==null&&Object.keys(e.transform).length>0?"render/image/authenticated":"object",l=new URLSearchParams;e!=null&&e.transform&&this.applyTransformOptsToQuery(l,e.transform),(e==null?void 0:e.cacheNonce)!=null&&l.set("cacheNonce",String(e.cacheNonce));const u=l.toString(),c=this._getFinalPath(n),d=()=>zr(this.fetch,`${this.url}/${a}/${c}${u?`?${u}`:""}`,{headers:this.headers,noResolveJson:!0},s);return new Ak(d,this.shouldThrowOnError)}async info(n){var e=this;const s=e._getFinalPath(n);return e.handleOperation(async()=>Bd(await zr(e.fetch,`${e.url}/object/info/${s}`,{headers:e.headers})))}async exists(n){var e=this;const s=e._getFinalPath(n);try{return await Tk(e.fetch,`${e.url}/object/${s}`,{headers:e.headers}),{data:!0,error:null}}catch(l){if(e.shouldThrowOnError)throw l;if(cu(l)){var a;const u=l instanceof Ud?l.status:l instanceof Ww?(a=l.originalError)===null||a===void 0?void 0:a.status:void 0;if(u!==void 0&&[400,404].includes(u))return{data:!1,error:l}}throw l}}getPublicUrl(n,e){const s=this._getFinalPath(n),a=new URLSearchParams;e!=null&&e.download&&a.set("download",e.download===!0?"":e.download),e!=null&&e.transform&&this.applyTransformOptsToQuery(a,e.transform),(e==null?void 0:e.cacheNonce)!=null&&a.set("cacheNonce",String(e.cacheNonce));const l=a.toString(),u=typeof(e==null?void 0:e.transform)=="object"&&e.transform!==null&&Object.keys(e.transform).length>0?"render/image":"object";return{data:{publicUrl:encodeURI(`${this.url}/${u}/public/${s}`)+(l?`?${l}`:"")}}}async remove(n){var e=this;return e.handleOperation(async()=>await Vr(e.fetch,`${e.url}/object/${e.bucketId}`,{prefixes:n},{headers:e.headers}))}async purgeCache(n,e,s){var a=this;return a.handleOperation(async()=>{const l=a._getFinalPath(n),u=new URLSearchParams;e!=null&&e.transformations&&u.set("transformations","true");const c=u.toString();return await Vr(a.fetch,`${a.url}/cdn/${l}${c?`?${c}`:""}`,{},{headers:a.headers},s)})}async list(n,e,s){var a=this;return a.handleOperation(async()=>{const l=e!=null&&e.sortBy?fe(fe({},Jh.sortBy),e.sortBy):Jh.sortBy,u=fe(fe(fe({},Jh),e),{},{sortBy:l,prefix:n||""});return await yn(a.fetch,`${a.url}/object/list/${a.bucketId}`,u,{headers:a.headers},s)})}async listV2(n,e){var s=this;return s.handleOperation(async()=>{const a=fe({},n);return await yn(s.fetch,`${s.url}/object/list-v2/${s.bucketId}`,a,{headers:s.headers},e)})}encodeMetadata(n){return JSON.stringify(n)}toBase64(n){return typeof Buffer<"u"?Buffer.from(n).toString("base64"):btoa(n)}_getFinalPath(n){return`${this.bucketId}/${n.replace(/^\/+/,"")}`}_removeEmptyFolders(n){return n.replace(/^\/|\/$/g,"").replace(/\/+/g,"/")}applyTransformOptsToQuery(n,e){return e.width&&n.set("width",e.width.toString()),e.height&&n.set("height",e.height.toString()),e.resize&&n.set("resize",e.resize),e.format&&n.set("format",e.format),e.quality&&n.set("quality",e.quality.toString()),n}};const Ck="2.110.2",Fr={"X-Client-Info":`storage-js/${Ck}`};var kk=class extends wa{constructor(n,e={},s,a){const l=new URL(n);a!=null&&a.useNewHostname&&/supabase\.(co|in|red)$/.test(l.hostname)&&!l.hostname.includes("storage.supabase.")&&(l.hostname=l.hostname.replace("supabase.","storage.supabase."));const u=l.href.replace(/\/$/,""),c=fe(fe({},Fr),e);super(u,c,s,"storage")}async listBuckets(n){var e=this;return e.handleOperation(async()=>{const s=e.listBucketOptionsToQueryString(n);return await zr(e.fetch,`${e.url}/bucket${s}`,{headers:e.headers})})}async getBucket(n){var e=this;return e.handleOperation(async()=>await zr(e.fetch,`${e.url}/bucket/${n}`,{headers:e.headers}))}async createBucket(n,e={public:!1}){var s=this;return s.handleOperation(async()=>await yn(s.fetch,`${s.url}/bucket`,{id:n,name:n,type:e.type,public:e.public,file_size_limit:e.fileSizeLimit,allowed_mime_types:e.allowedMimeTypes},{headers:s.headers}))}async updateBucket(n,e){var s=this;return s.handleOperation(async()=>await Ld(s.fetch,`${s.url}/bucket/${n}`,{id:n,name:n,public:e.public,file_size_limit:e.fileSizeLimit,allowed_mime_types:e.allowedMimeTypes},{headers:s.headers}))}async emptyBucket(n){var e=this;return e.handleOperation(async()=>await yn(e.fetch,`${e.url}/bucket/${n}/empty`,{},{headers:e.headers}))}async deleteBucket(n){var e=this;return e.handleOperation(async()=>await Vr(e.fetch,`${e.url}/bucket/${n}`,{},{headers:e.headers}))}async purgeBucketCache(n,e,s){var a=this;return a.handleOperation(async()=>{const l=new URLSearchParams;e!=null&&e.transformations&&l.set("transformations","true");const u=l.toString();return await Vr(a.fetch,`${a.url}/cdn/${n}${u?`?${u}`:""}`,{},{headers:a.headers},s)})}listBucketOptionsToQueryString(n){const e={};return n&&("limit"in n&&(e.limit=String(n.limit)),"offset"in n&&(e.offset=String(n.offset)),n.search&&(e.search=n.search),n.sortColumn&&(e.sortColumn=n.sortColumn),n.sortOrder&&(e.sortOrder=n.sortOrder)),Object.keys(e).length>0?"?"+new URLSearchParams(e).toString():""}},jk=class extends wa{constructor(n,e={},s){const a=n.replace(/\/$/,""),l=fe(fe({},Fr),e);super(a,l,s,"storage")}async createBucket(n){var e=this;return e.handleOperation(async()=>await yn(e.fetch,`${e.url}/bucket`,{name:n},{headers:e.headers}))}async listBuckets(n){var e=this;return e.handleOperation(async()=>{const s=new URLSearchParams;(n==null?void 0:n.limit)!==void 0&&s.set("limit",n.limit.toString()),(n==null?void 0:n.offset)!==void 0&&s.set("offset",n.offset.toString()),n!=null&&n.sortColumn&&s.set("sortColumn",n.sortColumn),n!=null&&n.sortOrder&&s.set("sortOrder",n.sortOrder),n!=null&&n.search&&s.set("search",n.search);const a=s.toString(),l=a?`${e.url}/bucket?${a}`:`${e.url}/bucket`;return await zr(e.fetch,l,{headers:e.headers})})}async deleteBucket(n){var e=this;return e.handleOperation(async()=>await Vr(e.fetch,`${e.url}/bucket/${n}`,{},{headers:e.headers}))}from(n){var e=this;if(!wk(n))throw new uu("Invalid bucket name: File, folder, and bucket names must follow AWS object key naming guidelines and should avoid the use of any other characters.");const s=new fk({baseUrl:this.url,catalogName:n,auth:{type:"custom",getHeaders:async()=>e.headers},fetch:this.fetch}),a=this.shouldThrowOnError;return new Proxy(s,{get(l,u){const c=l[u];return typeof c!="function"?c:async(...d)=>{try{return{data:await c.apply(l,d),error:null}}catch(m){if(a)throw m;return{data:null,error:m}}}}})}},Ok=class extends wa{constructor(n,e={},s){const a=n.replace(/\/$/,""),l=fe(fe({},Fr),{},{"Content-Type":"application/json"},e);super(a,l,s,"vectors")}async createIndex(n){var e=this;return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/CreateIndex`,n,{headers:e.headers})||{})}async getIndex(n,e){var s=this;return s.handleOperation(async()=>await Kt.post(s.fetch,`${s.url}/GetIndex`,{vectorBucketName:n,indexName:e},{headers:s.headers}))}async listIndexes(n){var e=this;return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/ListIndexes`,n,{headers:e.headers}))}async deleteIndex(n,e){var s=this;return s.handleOperation(async()=>await Kt.post(s.fetch,`${s.url}/DeleteIndex`,{vectorBucketName:n,indexName:e},{headers:s.headers})||{})}},Nk=class extends wa{constructor(n,e={},s){const a=n.replace(/\/$/,""),l=fe(fe({},Fr),{},{"Content-Type":"application/json"},e);super(a,l,s,"vectors")}async putVectors(n){var e=this;if(n.vectors.length<1||n.vectors.length>500)throw new Error("Vector batch size must be between 1 and 500 items");return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/PutVectors`,n,{headers:e.headers})||{})}async getVectors(n){var e=this;return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/GetVectors`,n,{headers:e.headers}))}async listVectors(n){var e=this;if(n.segmentCount!==void 0){if(n.segmentCount<1||n.segmentCount>16)throw new Error("segmentCount must be between 1 and 16");if(n.segmentIndex!==void 0&&(n.segmentIndex<0||n.segmentIndex>=n.segmentCount))throw new Error(`segmentIndex must be between 0 and ${n.segmentCount-1}`)}return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/ListVectors`,n,{headers:e.headers}))}async queryVectors(n){var e=this;return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/QueryVectors`,n,{headers:e.headers}))}async deleteVectors(n){var e=this;if(n.keys.length<1||n.keys.length>500)throw new Error("Keys batch size must be between 1 and 500 items");return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/DeleteVectors`,n,{headers:e.headers})||{})}},Dk=class extends wa{constructor(n,e={},s){const a=n.replace(/\/$/,""),l=fe(fe({},Fr),{},{"Content-Type":"application/json"},e);super(a,l,s,"vectors")}async createBucket(n){var e=this;return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/CreateVectorBucket`,{vectorBucketName:n},{headers:e.headers})||{})}async getBucket(n){var e=this;return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/GetVectorBucket`,{vectorBucketName:n},{headers:e.headers}))}async listBuckets(n={}){var e=this;return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/ListVectorBuckets`,n,{headers:e.headers}))}async deleteBucket(n){var e=this;return e.handleOperation(async()=>await Kt.post(e.fetch,`${e.url}/DeleteVectorBucket`,{vectorBucketName:n},{headers:e.headers})||{})}},Mk=class extends Dk{constructor(n,e={}){super(n,e.headers||{},e.fetch)}from(n){return new Uk(this.url,this.headers,n,this.fetch)}async createBucket(n){var e=()=>super.createBucket,s=this;return e().call(s,n)}async getBucket(n){var e=()=>super.getBucket,s=this;return e().call(s,n)}async listBuckets(n={}){var e=()=>super.listBuckets,s=this;return e().call(s,n)}async deleteBucket(n){var e=()=>super.deleteBucket,s=this;return e().call(s,n)}},Uk=class extends Ok{constructor(n,e,s,a){super(n,e,a),this.vectorBucketName=s}async createIndex(n){var e=()=>super.createIndex,s=this;return e().call(s,fe(fe({},n),{},{vectorBucketName:s.vectorBucketName}))}async listIndexes(n={}){var e=()=>super.listIndexes,s=this;return e().call(s,fe(fe({},n),{},{vectorBucketName:s.vectorBucketName}))}async getIndex(n){var e=()=>super.getIndex,s=this;return e().call(s,s.vectorBucketName,n)}async deleteIndex(n){var e=()=>super.deleteIndex,s=this;return e().call(s,s.vectorBucketName,n)}index(n){return new Bk(this.url,this.headers,this.vectorBucketName,n,this.fetch)}},Bk=class extends Nk{constructor(n,e,s,a,l){super(n,e,l),this.vectorBucketName=s,this.indexName=a}async putVectors(n){var e=()=>super.putVectors,s=this;return e().call(s,fe(fe({},n),{},{vectorBucketName:s.vectorBucketName,indexName:s.indexName}))}async getVectors(n){var e=()=>super.getVectors,s=this;return e().call(s,fe(fe({},n),{},{vectorBucketName:s.vectorBucketName,indexName:s.indexName}))}async listVectors(n={}){var e=()=>super.listVectors,s=this;return e().call(s,fe(fe({},n),{},{vectorBucketName:s.vectorBucketName,indexName:s.indexName}))}async queryVectors(n){var e=()=>super.queryVectors,s=this;return e().call(s,fe(fe({},n),{},{vectorBucketName:s.vectorBucketName,indexName:s.indexName}))}async deleteVectors(n){var e=()=>super.deleteVectors,s=this;return e().call(s,fe(fe({},n),{},{vectorBucketName:s.vectorBucketName,indexName:s.indexName}))}},Lk=class extends kk{constructor(n,e={},s,a){super(n,e,s,a)}from(n){return new Rk(this.url,this.headers,n,this.fetch)}get vectors(){return new Mk(this.url+"/vector",{headers:this.headers,fetch:this.fetch})}get analytics(){return new jk(this.url+"/iceberg",this.headers,this.fetch)}};const i_="2.110.2",ri=30*1e3,wr=3,Qh=wr*ri,zk=2*ri,Vk="http://localhost:9999",Pk="supabase.auth.token",Hk={"X-Client-Info":`gotrue-js/${i_}`},zd="X-Supabase-Api-Version",s_={"2024-01-01":{timestamp:Date.parse("2024-01-01T00:00:00.0Z"),name:"2024-01-01"}},$k=/^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$/i,qk=600*1e3;class Pr extends Error{constructor(e,s,a){super(e),this.__isAuthError=!0,this.name="AuthError",this.status=s,this.code=a}toJSON(){return{name:this.name,message:this.message,status:this.status,code:this.code}}}function te(n){return typeof n=="object"&&n!==null&&"__isAuthError"in n}class Ik extends Pr{constructor(e,s,a){super(e,s,a),this.name="AuthApiError",this.status=s,this.code=a}}function Gk(n){return te(n)&&n.name==="AuthApiError"}class vn extends Pr{constructor(e,s){super(e),this.name="AuthUnknownError",this.originalError=s}}class Bn extends Pr{constructor(e,s,a,l){super(e,a,l),this.name=s,this.status=a}}class ht extends Bn{constructor(){super("Auth session missing!","AuthSessionMissingError",400,void 0)}}function Tl(n){return te(n)&&n.name==="AuthSessionMissingError"}class na extends Bn{constructor(){super("Auth session or user missing","AuthInvalidTokenResponseError",500,void 0)}}class El extends Bn{constructor(e){super(e,"AuthInvalidCredentialsError",400,void 0)}}class Al extends Bn{constructor(e,s=null){super(e,"AuthImplicitGrantRedirectError",500,void 0),this.details=null,this.details=s}toJSON(){return Object.assign(Object.assign({},super.toJSON()),{details:this.details})}}function Kk(n){return te(n)&&n.name==="AuthImplicitGrantRedirectError"}class kv extends Bn{constructor(e,s=null){super(e,"AuthPKCEGrantCodeExchangeError",500,void 0),this.details=null,this.details=s}toJSON(){return Object.assign(Object.assign({},super.toJSON()),{details:this.details})}}class Fk extends Bn{constructor(){super("PKCE code verifier not found in storage. This can happen if the auth flow was initiated in a different browser or device, or if the storage was cleared. For SSR frameworks (Next.js, SvelteKit, etc.), use @supabase/ssr on both the server and client to store the code verifier in cookies.","AuthPKCECodeVerifierMissingError",400,"pkce_code_verifier_not_found")}}class Vd extends Bn{constructor(e,s){super(e,"AuthRetryableFetchError",s,void 0)}}function jv(n){return te(n)&&n.name==="AuthRetryableFetchError"}class Ov extends Bn{constructor(e="Refresh result discarded: session state changed mid-flight (e.g., concurrent signOut)"){super(e,"AuthRefreshDiscardedError",409,void 0)}}function Yk(n){return te(n)&&n.name==="AuthRefreshDiscardedError"}class Nv extends Bn{constructor(e,s,a){super(e,"AuthWeakPasswordError",s,"weak_password"),this.reasons=a}toJSON(){return Object.assign(Object.assign({},super.toJSON()),{reasons:this.reasons})}}class Ql extends Bn{constructor(e){super(e,"AuthInvalidJwtError",400,"invalid_jwt")}}const Zl="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".split(""),Dv=` -\r=`.split(""),Xk=(()=>{const n=new Array(128);for(let e=0;e=6;){const a=e.queue>>e.queuedBits-6&63;s(Zl[a]),e.queuedBits-=6}else if(e.queuedBits>0)for(e.queue=e.queue<<6-e.queuedBits,e.queuedBits=6;e.queuedBits>=6;){const a=e.queue>>e.queuedBits-6&63;s(Zl[a]),e.queuedBits-=6}}function a_(n,e,s){const a=Xk[n];if(a>-1)for(e.queue=e.queue<<6|a,e.queuedBits+=6;e.queuedBits>=8;)s(e.queue>>e.queuedBits-8&255),e.queuedBits-=8;else{if(a===-2)return;throw new Error(`Invalid Base64-URL character "${String.fromCharCode(n)}"`)}}function Uv(n){const e=[],s=c=>{e.push(String.fromCodePoint(c))},a={utf8seq:0,codepoint:0},l={queue:0,queuedBits:0},u=c=>{Zk(c,a,s)};for(let c=0;c>6),e(128|n&63);return}else if(n<=65535){e(224|n>>12),e(128|n>>6&63),e(128|n&63);return}else if(n<=1114111){e(240|n>>18),e(128|n>>12&63),e(128|n>>6&63),e(128|n&63);return}throw new Error(`Unrecognized Unicode codepoint: ${n.toString(16)}`)}function Qk(n,e){for(let s=0;s55295&&a<=56319){const l=(a-55296)*1024&65535;a=(n.charCodeAt(s+1)-56320&65535|l)+65536,s+=1}Jk(a,e)}}function Zk(n,e,s){if(e.utf8seq===0){if(n<=127){s(n);return}for(let a=1;a<6;a+=1)if((n>>7-a&1)===0){e.utf8seq=a;break}if(e.utf8seq===2)e.codepoint=n&31;else if(e.utf8seq===3)e.codepoint=n&15;else if(e.utf8seq===4)e.codepoint=n&7;else throw new Error("Invalid UTF-8 sequence");e.utf8seq-=1}else if(e.utf8seq>0){if(n<=127)throw new Error("Invalid UTF-8 sequence");e.codepoint=e.codepoint<<6|n&63,e.utf8seq-=1,e.utf8seq===0&&s(e.codepoint)}}function fa(n){const e=[],s={queue:0,queuedBits:0},a=l=>{e.push(l)};for(let l=0;le.push(s)),new Uint8Array(e)}function fs(n){const e=[],s={queue:0,queuedBits:0},a=l=>{e.push(l)};return n.forEach(l=>Mv(l,s,a)),Mv(null,s,a),e.join("")}function ej(n){return Math.round(Date.now()/1e3)+n}function tj(){return Symbol("auth-callback")}const xt=()=>typeof window<"u"&&typeof document<"u",rs={tested:!1,writable:!1},r_=()=>{if(!xt())return!1;try{if(typeof globalThis.localStorage!="object")return!1}catch{return!1}if(rs.tested)return rs.writable;const n=`lswt-${Math.random()}${Math.random()}`;try{globalThis.localStorage.setItem(n,n),globalThis.localStorage.removeItem(n),rs.tested=!0,rs.writable=!0}catch{rs.tested=!0,rs.writable=!1}return rs.writable};function nj(n){const e={},s=new URL(n);if(s.hash&&s.hash[0]==="#")try{new URLSearchParams(s.hash.substring(1)).forEach((l,u)=>{e[u]=l})}catch{}return s.searchParams.forEach((a,l)=>{e[l]=a}),e}const o_=n=>n?(...e)=>n(...e):(...e)=>fetch(...e),ij=n=>typeof n=="object"&&n!==null&&"status"in n&&"ok"in n&&"json"in n&&typeof n.json=="function",la=async(n,e,s)=>{await n.setItem(e,JSON.stringify(s))},fn=async(n,e)=>{const s=await n.getItem(e);if(!s)return null;try{return JSON.parse(s)}catch{return null}},et=async(n,e)=>{await n.removeItem(e)};class hu{constructor(){this.promise=new hu.promiseConstructor((e,s)=>{this.resolve=e,this.reject=s})}}hu.promiseConstructor=Promise;function Rl(n){const e=n.split(".");if(e.length!==3)throw new Ql("Invalid JWT structure");for(let a=0;a{setTimeout(()=>e(null),n)})}function aj(n,e){return new Promise((a,l)=>{(async()=>{for(let u=0;u<1/0;u++)try{const c=await n(u);if(!e(u,null,c)){a(c);return}}catch(c){if(!e(u,c)){l(c);return}}})()})}function rj(n){return("0"+n.toString(16)).substr(-2)}function oj(){const e=new Uint32Array(56);if(typeof crypto>"u"){const s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~",a=s.length;let l="";for(let u=0;u<56;u++)l+=s.charAt(Math.floor(Math.random()*a));return l}return crypto.getRandomValues(e),Array.from(e,rj).join("")}async function lj(n){const s=new TextEncoder().encode(n),a=await crypto.subtle.digest("SHA-256",s),l=new Uint8Array(a);return Array.from(l).map(u=>String.fromCharCode(u)).join("")}async function uj(n){if(!(typeof crypto<"u"&&typeof crypto.subtle<"u"&&typeof TextEncoder<"u"))return console.warn("WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256."),n;const s=await lj(n);return btoa(s).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}async function os(n,e,s=!1){const a=oj();let l=a;s&&(l+="/recovery"),await la(n,`${e}-code-verifier`,l);const u=await uj(a);return[u,a===u?"plain":"s256"]}const cj=/^2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])$/i;function hj(n){const e=n.headers.get(zd);if(!e||!e.match(cj))return null;try{return new Date(`${e}T00:00:00.0Z`)}catch{return null}}function dj(n){if(!n)throw new Error("Missing exp claim");const e=Math.floor(Date.now()/1e3);if(n<=e)throw new Error("JWT has expired")}function fj(n){switch(n){case"RS256":return{name:"RSASSA-PKCS1-v1_5",hash:{name:"SHA-256"}};case"ES256":return{name:"ECDSA",namedCurve:"P-256",hash:{name:"SHA-256"}};default:throw new Error("Invalid alg claim")}}const mj=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;function ni(n){if(!mj.test(n))throw new Error("@supabase/auth-js: Expected parameter to be UUID but is not")}function pn(n){if(!n.passkey)throw new Error("@supabase/auth-js: the passkey API is experimental and disabled by default. Enable it by passing `auth: { experimental: { passkey: true } }` to createClient (or to the GoTrueClient constructor).")}function Zh(){const n={};return new Proxy(n,{get:(e,s)=>{if(s==="__isUserNotAvailableProxy")return!0;if(typeof s=="symbol"){const a=s.toString();if(a==="Symbol(Symbol.toPrimitive)"||a==="Symbol(Symbol.toStringTag)"||a==="Symbol(util.inspect.custom)")return}throw new Error(`@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Accessing the "${s}" property of the session object is not supported. Please use getUser() instead.`)},set:(e,s)=>{throw new Error(`@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Setting the "${s}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.`)},deleteProperty:(e,s)=>{throw new Error(`@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Deleting the "${s}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.`)}})}function pj(n,e){return new Proxy(n,{get:(s,a,l)=>{if(a==="__isInsecureUserWarningProxy")return!0;if(typeof a=="symbol"){const u=a.toString();if(u==="Symbol(Symbol.toPrimitive)"||u==="Symbol(Symbol.toStringTag)"||u==="Symbol(util.inspect.custom)"||u==="Symbol(nodejs.util.inspect.custom)")return Reflect.get(s,a,l)}return!e.value&&typeof a=="string"&&(console.warn("Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server."),e.value=!0),Reflect.get(s,a,l)}})}function Bv(n){return JSON.parse(JSON.stringify(n))}const us=n=>{if(typeof n=="object"&&n!==null){const e=n;if(typeof e.msg=="string")return e.msg;if(typeof e.message=="string")return e.message;if(typeof e.error_description=="string")return e.error_description;if(typeof e.error=="string")return e.error}return JSON.stringify(n)},gj=[500,501,502,503,504,520,521,522,523,524,525,526,527,528,529,530];async function Lv(n){var e;if(!ij(n))throw new Vd(us(n),0);if(gj.includes(n.status))throw new Vd(us(n),n.status);let s;try{s=await n.json()}catch(u){throw new vn(us(u),u)}let a;const l=hj(n);if(l&&l.getTime()>=s_["2024-01-01"].timestamp&&typeof s=="object"&&s&&typeof s.code=="string"?a=s.code:typeof s=="object"&&s&&typeof s.error_code=="string"&&(a=s.error_code),a){if(a==="weak_password")throw new Nv(us(s),n.status,((e=s.weak_password)===null||e===void 0?void 0:e.reasons)||[]);if(a==="session_not_found")throw new ht}else if(typeof s=="object"&&s&&typeof s.weak_password=="object"&&s.weak_password&&Array.isArray(s.weak_password.reasons)&&s.weak_password.reasons.length&&s.weak_password.reasons.reduce((u,c)=>u&&typeof c=="string",!0))throw new Nv(us(s),n.status,s.weak_password.reasons);throw new Ik(us(s),n.status||500,a)}const yj=(n,e,s,a)=>{const l={method:n,headers:(e==null?void 0:e.headers)||{}};return n==="GET"?l:(l.headers=Object.assign({"Content-Type":"application/json;charset=UTF-8"},e==null?void 0:e.headers),l.body=JSON.stringify(a),Object.assign(Object.assign({},l),s))};async function ae(n,e,s,a){var l;const u=Object.assign({},a==null?void 0:a.headers);u[zd]||(u[zd]=s_["2024-01-01"].name),a!=null&&a.jwt&&(u.Authorization=`Bearer ${a.jwt}`);const c=(l=a==null?void 0:a.query)!==null&&l!==void 0?l:{};a!=null&&a.redirectTo&&(c.redirect_to=a.redirectTo);const d=Object.keys(c).length?"?"+new URLSearchParams(c).toString():"",m=await vj(n,e,s+d,{headers:u,noResolveJson:a==null?void 0:a.noResolveJson},{},a==null?void 0:a.body);return a!=null&&a.xform?a==null?void 0:a.xform(m):{data:Object.assign({},m),error:null}}async function vj(n,e,s,a,l,u){const c=yj(e,a,l,u);let d;try{d=await n(s,Object.assign({},c))}catch(m){throw console.error(m),new Vd(us(m),0)}if(d.ok||await Lv(d),a!=null&&a.noResolveJson)return d;try{return await d.json()}catch(m){await Lv(m)}}function rn(n){var e;let s=null;_j(n)&&(s=Object.assign({},n),n.expires_at||(s.expires_at=ej(n.expires_in)));const a=(e=n.user)!==null&&e!==void 0?e:typeof(n==null?void 0:n.id)=="string"?n:null;return{data:{session:s,user:a},error:null}}function zv(n){const e=rn(n);return!e.error&&n.weak_password&&typeof n.weak_password=="object"&&Array.isArray(n.weak_password.reasons)&&n.weak_password.reasons.length&&n.weak_password.message&&typeof n.weak_password.message=="string"&&n.weak_password.reasons.reduce((s,a)=>s&&typeof a=="string",!0)&&(e.data.weak_password=n.weak_password),e}function Ui(n){var e;return{data:{user:(e=n.user)!==null&&e!==void 0?e:n},error:null}}function bj(n){return{data:n,error:null}}function wj(n){const{action_link:e,email_otp:s,hashed_token:a,redirect_to:l,verification_type:u}=n,c=lu(n,["action_link","email_otp","hashed_token","redirect_to","verification_type"]),d={action_link:e,email_otp:s,hashed_token:a,redirect_to:l,verification_type:u},m=Object.assign({},c);return{data:{properties:d,user:m},error:null}}function Vv(n){return n}function _j(n){return!!n.access_token&&!!n.refresh_token&&!!n.expires_in}const Wh=["global","local","others"];class xj{constructor({url:e="",headers:s={},fetch:a,experimental:l}){this.url=e,this.headers=s,this.fetch=o_(a),this.experimental=l??{},this.mfa={listFactors:this._listFactors.bind(this),deleteFactor:this._deleteFactor.bind(this)},this.oauth={listClients:this._listOAuthClients.bind(this),createClient:this._createOAuthClient.bind(this),getClient:this._getOAuthClient.bind(this),updateClient:this._updateOAuthClient.bind(this),deleteClient:this._deleteOAuthClient.bind(this),regenerateClientSecret:this._regenerateOAuthClientSecret.bind(this)},this.customProviders={listProviders:this._listCustomProviders.bind(this),createProvider:this._createCustomProvider.bind(this),getProvider:this._getCustomProvider.bind(this),updateProvider:this._updateCustomProvider.bind(this),deleteProvider:this._deleteCustomProvider.bind(this)},this.passkey={listPasskeys:this._adminListPasskeys.bind(this),deletePasskey:this._adminDeletePasskey.bind(this)}}async signOut(e,s=Wh[0]){if(Wh.indexOf(s)<0)throw new Error(`@supabase/auth-js: Parameter scope must be one of ${Wh.join(", ")}`);try{return await ae(this.fetch,"POST",`${this.url}/logout?scope=${s}`,{headers:this.headers,jwt:e,noResolveJson:!0}),{data:null,error:null}}catch(a){if(te(a))return{data:null,error:a};throw a}}async inviteUserByEmail(e,s={}){try{return await ae(this.fetch,"POST",`${this.url}/invite`,{body:{email:e,data:s.data},headers:this.headers,redirectTo:s.redirectTo,xform:Ui})}catch(a){if(te(a))return{data:{user:null},error:a};throw a}}async generateLink(e){try{const{options:s}=e,a=lu(e,["options"]),l=Object.assign(Object.assign({},a),s);return"newEmail"in a&&(l.new_email=a==null?void 0:a.newEmail,delete l.newEmail),await ae(this.fetch,"POST",`${this.url}/admin/generate_link`,{body:l,headers:this.headers,xform:wj,redirectTo:s==null?void 0:s.redirectTo})}catch(s){if(te(s))return{data:{properties:null,user:null},error:s};throw s}}async createUser(e){try{return await ae(this.fetch,"POST",`${this.url}/admin/users`,{body:e,headers:this.headers,xform:Ui})}catch(s){if(te(s))return{data:{user:null},error:s};throw s}}async listUsers(e){var s,a,l,u,c,d,m;try{const p={nextPage:null,lastPage:0,total:0},g=await ae(this.fetch,"GET",`${this.url}/admin/users`,{headers:this.headers,noResolveJson:!0,query:{page:(a=(s=e==null?void 0:e.page)===null||s===void 0?void 0:s.toString())!==null&&a!==void 0?a:"",per_page:(u=(l=e==null?void 0:e.perPage)===null||l===void 0?void 0:l.toString())!==null&&u!==void 0?u:""},xform:Vv});if(g.error)throw g.error;const v=await g.json(),w=(c=g.headers.get("x-total-count"))!==null&&c!==void 0?c:0,x=(m=(d=g.headers.get("link"))===null||d===void 0?void 0:d.split(","))!==null&&m!==void 0?m:[];return x.length>0&&(x.forEach(S=>{const A=parseInt(S.split(";")[0].split("=")[1].substring(0,1)),k=JSON.parse(S.split(";")[1].split("=")[1]);p[`${k}Page`]=A}),p.total=parseInt(w)),{data:Object.assign(Object.assign({},v),p),error:null}}catch(p){if(te(p))return{data:{users:[]},error:p};throw p}}async getUserById(e){ni(e);try{return await ae(this.fetch,"GET",`${this.url}/admin/users/${e}`,{headers:this.headers,xform:Ui})}catch(s){if(te(s))return{data:{user:null},error:s};throw s}}async updateUserById(e,s){ni(e);try{return await ae(this.fetch,"PUT",`${this.url}/admin/users/${e}`,{body:s,headers:this.headers,xform:Ui})}catch(a){if(te(a))return{data:{user:null},error:a};throw a}}async deleteUser(e,s=!1){ni(e);try{return await ae(this.fetch,"DELETE",`${this.url}/admin/users/${e}`,{headers:this.headers,body:{should_soft_delete:s},xform:Ui})}catch(a){if(te(a))return{data:{user:null},error:a};throw a}}async _listFactors(e){ni(e.userId);try{const{data:s,error:a}=await ae(this.fetch,"GET",`${this.url}/admin/users/${e.userId}/factors`,{headers:this.headers,xform:l=>({data:{factors:l},error:null})});return{data:s,error:a}}catch(s){if(te(s))return{data:null,error:s};throw s}}async _deleteFactor(e){ni(e.userId),ni(e.id);try{return{data:await ae(this.fetch,"DELETE",`${this.url}/admin/users/${e.userId}/factors/${e.id}`,{headers:this.headers}),error:null}}catch(s){if(te(s))return{data:null,error:s};throw s}}async _listOAuthClients(e){var s,a,l,u,c,d,m;try{const p={nextPage:null,lastPage:0,total:0},g=await ae(this.fetch,"GET",`${this.url}/admin/oauth/clients`,{headers:this.headers,noResolveJson:!0,query:{page:(a=(s=e==null?void 0:e.page)===null||s===void 0?void 0:s.toString())!==null&&a!==void 0?a:"",per_page:(u=(l=e==null?void 0:e.perPage)===null||l===void 0?void 0:l.toString())!==null&&u!==void 0?u:""},xform:Vv});if(g.error)throw g.error;const v=await g.json(),w=(c=g.headers.get("x-total-count"))!==null&&c!==void 0?c:0,x=(m=(d=g.headers.get("link"))===null||d===void 0?void 0:d.split(","))!==null&&m!==void 0?m:[];return x.length>0&&(x.forEach(S=>{const A=parseInt(S.split(";")[0].split("=")[1].substring(0,1)),k=JSON.parse(S.split(";")[1].split("=")[1]);p[`${k}Page`]=A}),p.total=parseInt(w)),{data:Object.assign(Object.assign({},v),p),error:null}}catch(p){if(te(p))return{data:{clients:[]},error:p};throw p}}async _createOAuthClient(e){try{return await ae(this.fetch,"POST",`${this.url}/admin/oauth/clients`,{body:e,headers:this.headers,xform:s=>({data:s,error:null})})}catch(s){if(te(s))return{data:null,error:s};throw s}}async _getOAuthClient(e){try{return await ae(this.fetch,"GET",`${this.url}/admin/oauth/clients/${e}`,{headers:this.headers,xform:s=>({data:s,error:null})})}catch(s){if(te(s))return{data:null,error:s};throw s}}async _updateOAuthClient(e,s){try{return await ae(this.fetch,"PUT",`${this.url}/admin/oauth/clients/${e}`,{body:s,headers:this.headers,xform:a=>({data:a,error:null})})}catch(a){if(te(a))return{data:null,error:a};throw a}}async _deleteOAuthClient(e){try{return await ae(this.fetch,"DELETE",`${this.url}/admin/oauth/clients/${e}`,{headers:this.headers,noResolveJson:!0}),{data:null,error:null}}catch(s){if(te(s))return{data:null,error:s};throw s}}async _regenerateOAuthClientSecret(e){try{return await ae(this.fetch,"POST",`${this.url}/admin/oauth/clients/${e}/regenerate_secret`,{headers:this.headers,xform:s=>({data:s,error:null})})}catch(s){if(te(s))return{data:null,error:s};throw s}}async _listCustomProviders(e){try{const s={};return e!=null&&e.type&&(s.type=e.type),await ae(this.fetch,"GET",`${this.url}/admin/custom-providers`,{headers:this.headers,query:s,xform:a=>{var l;return{data:{providers:(l=a==null?void 0:a.providers)!==null&&l!==void 0?l:[]},error:null}}})}catch(s){if(te(s))return{data:{providers:[]},error:s};throw s}}async _createCustomProvider(e){try{return await ae(this.fetch,"POST",`${this.url}/admin/custom-providers`,{body:e,headers:this.headers,xform:s=>({data:s,error:null})})}catch(s){if(te(s))return{data:null,error:s};throw s}}async _getCustomProvider(e){try{return await ae(this.fetch,"GET",`${this.url}/admin/custom-providers/${e}`,{headers:this.headers,xform:s=>({data:s,error:null})})}catch(s){if(te(s))return{data:null,error:s};throw s}}async _updateCustomProvider(e,s){try{return await ae(this.fetch,"PUT",`${this.url}/admin/custom-providers/${e}`,{body:s,headers:this.headers,xform:a=>({data:a,error:null})})}catch(a){if(te(a))return{data:null,error:a};throw a}}async _deleteCustomProvider(e){try{return await ae(this.fetch,"DELETE",`${this.url}/admin/custom-providers/${e}`,{headers:this.headers,noResolveJson:!0}),{data:null,error:null}}catch(s){if(te(s))return{data:null,error:s};throw s}}async _adminListPasskeys(e){pn(this.experimental),ni(e.userId);try{return await ae(this.fetch,"GET",`${this.url}/admin/users/${e.userId}/passkeys`,{headers:this.headers,xform:s=>({data:s,error:null})})}catch(s){if(te(s))return{data:null,error:s};throw s}}async _adminDeletePasskey(e){pn(this.experimental),ni(e.userId),ni(e.passkeyId);try{return await ae(this.fetch,"DELETE",`${this.url}/admin/users/${e.userId}/passkeys/${e.passkeyId}`,{headers:this.headers,noResolveJson:!0}),{data:null,error:null}}catch(s){if(te(s))return{data:null,error:s};throw s}}}function Pv(n={}){return{getItem:e=>n[e]||null,setItem:(e,s)=>{n[e]=s},removeItem:e=>{delete n[e]}}}globalThis&&r_()&&globalThis.localStorage&&globalThis.localStorage.getItem("supabase.gotrue-js.locks.debug");class Sj extends Error{constructor(e){super(e),this.isAcquireTimeout=!0}}function Tj(){if(typeof globalThis!="object")try{Object.defineProperty(Object.prototype,"__magic__",{get:function(){return this},configurable:!0}),__magic__.globalThis=__magic__,delete Object.prototype.__magic__}catch{typeof self<"u"&&(self.globalThis=self)}}function l_(n){if(!/^0x[a-fA-F0-9]{40}$/.test(n))throw new Error(`@supabase/auth-js: Address "${n}" is invalid.`);return n.toLowerCase()}function Ej(n){return parseInt(n,16)}function Aj(n){const e=new TextEncoder().encode(n);return"0x"+Array.from(e,a=>a.toString(16).padStart(2,"0")).join("")}function Rj(n){var e;const{chainId:s,domain:a,expirationTime:l,issuedAt:u=new Date,nonce:c,notBefore:d,requestId:m,resources:p,scheme:g,uri:v,version:w}=n;{if(!Number.isInteger(s))throw new Error(`@supabase/auth-js: Invalid SIWE message field "chainId". Chain ID must be a EIP-155 chain ID. Provided value: ${s}`);if(!a)throw new Error('@supabase/auth-js: Invalid SIWE message field "domain". Domain must be provided.');if(c&&c.length<8)throw new Error(`@supabase/auth-js: Invalid SIWE message field "nonce". Nonce must be at least 8 characters. Provided value: ${c}`);if(!v)throw new Error('@supabase/auth-js: Invalid SIWE message field "uri". URI must be provided.');if(w!=="1")throw new Error(`@supabase/auth-js: Invalid SIWE message field "version". Version must be '1'. Provided value: ${w}`);if(!((e=n.statement)===null||e===void 0)&&e.includes(` -`))throw new Error(`@supabase/auth-js: Invalid SIWE message field "statement". Statement must not include '\\n'. Provided value: ${n.statement}`)}const x=l_(n.address),S=g?`${g}://${a}`:a,A=n.statement?`${n.statement} -`:"",k=`${S} wants you to sign in with your Ethereum account: -${x} - -${A}`;let C=`URI: ${v} -Version: ${w} -Chain ID: ${s}${c?` -Nonce: ${c}`:""} -Issued At: ${u.toISOString()}`;if(l&&(C+=` -Expiration Time: ${l.toISOString()}`),d&&(C+=` -Not Before: ${d.toISOString()}`),m&&(C+=` -Request ID: ${m}`),p){let N=` -Resources:`;for(const O of p){if(!O||typeof O!="string")throw new Error(`@supabase/auth-js: Invalid SIWE message field "resources". Every resource must be a valid string. Provided value: ${O}`);N+=` -- ${O}`}C+=N}return`${k} -${C}`}class at extends Error{constructor({message:e,code:s,cause:a,name:l}){var u;super(e,{cause:a}),this.__isWebAuthnError=!0,this.name=(u=l??(a instanceof Error?a.name:void 0))!==null&&u!==void 0?u:"Unknown Error",this.code=s}toJSON(){return{name:this.name,message:this.message,code:this.code}}}class Wl extends at{constructor(e,s){super({code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:s,message:e}),this.name="WebAuthnUnknownError",this.originalError=s}}function Cj({error:n,options:e}){var s,a,l;const{publicKey:u}=e;if(!u)throw Error("options was missing required publicKey property");if(n.name==="AbortError"){if(e.signal instanceof AbortSignal)return new at({message:"Registration ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:n})}else if(n.name==="ConstraintError"){if(((s=u.authenticatorSelection)===null||s===void 0?void 0:s.requireResidentKey)===!0)return new at({message:"Discoverable credentials were required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",cause:n});if(e.mediation==="conditional"&&((a=u.authenticatorSelection)===null||a===void 0?void 0:a.userVerification)==="required")return new at({message:"User verification was required during automatic registration but it could not be performed",code:"ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",cause:n});if(((l=u.authenticatorSelection)===null||l===void 0?void 0:l.userVerification)==="required")return new at({message:"User verification was required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",cause:n})}else{if(n.name==="InvalidStateError")return new at({message:"The authenticator was previously registered",code:"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",cause:n});if(n.name==="NotAllowedError")return new at({message:n.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:n});if(n.name==="NotSupportedError")return u.pubKeyCredParams.filter(d=>d.type==="public-key").length===0?new at({message:'No entry in pubKeyCredParams was of type "public-key"',code:"ERROR_MALFORMED_PUBKEYCREDPARAMS",cause:n}):new at({message:"No available authenticator supported any of the specified pubKeyCredParams algorithms",code:"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",cause:n});if(n.name==="SecurityError"){const c=window.location.hostname;if(u_(c)){if(u.rp.id!==c)return new at({message:`The RP ID "${u.rp.id}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:n})}else return new at({message:`${window.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:n})}else if(n.name==="TypeError"){if(u.user.id.byteLength<1||u.user.id.byteLength>64)return new at({message:"User ID was not between 1 and 64 characters",code:"ERROR_INVALID_USER_ID_LENGTH",cause:n})}else if(n.name==="UnknownError")return new at({message:"The authenticator was unable to process the specified options, or could not create a new credential",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:n})}return new at({message:"a Non-Webauthn related error has occurred",code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:n})}function kj({error:n,options:e}){const{publicKey:s}=e;if(!s)throw Error("options was missing required publicKey property");if(n.name==="AbortError"){if(e.signal instanceof AbortSignal)return new at({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:n})}else{if(n.name==="NotAllowedError")return new at({message:n.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:n});if(n.name==="SecurityError"){const a=window.location.hostname;if(u_(a)){if(s.rpId!==a)return new at({message:`The RP ID "${s.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:n})}else return new at({message:`${window.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:n})}else if(n.name==="UnknownError")return new at({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:n})}return new at({message:"a Non-Webauthn related error has occurred",code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:n})}class jj{createNewAbortSignal(){if(this.controller){const s=new Error("Cancelling existing WebAuthn API call for new one");s.name="AbortError",this.controller.abort(s)}const e=new AbortController;return this.controller=e,e.signal}cancelCeremony(){if(this.controller){const e=new Error("Manually cancelling existing WebAuthn API call");e.name="AbortError",this.controller.abort(e),this.controller=void 0}}}const Pd=new jj;function Hv(n){if(!n)throw new Error("Credential creation options are required");if(typeof PublicKeyCredential<"u"&&"parseCreationOptionsFromJSON"in PublicKeyCredential&&typeof PublicKeyCredential.parseCreationOptionsFromJSON=="function")return PublicKeyCredential.parseCreationOptionsFromJSON(n);const{challenge:e,user:s,excludeCredentials:a}=n,l=lu(n,["challenge","user","excludeCredentials"]),u=fa(e).buffer,c=Object.assign(Object.assign({},s),{id:fa(s.id).buffer}),d=Object.assign(Object.assign({},l),{challenge:u,user:c});if(a&&a.length>0){d.excludeCredentials=new Array(a.length);for(let m=0;m0){u.allowCredentials=new Array(s.length);for(let c=0;cl!==null&&typeof l=="object"&&!Array.isArray(l),s=l=>l instanceof ArrayBuffer||ArrayBuffer.isView(l),a={};for(const l of n)if(l)for(const u in l){const c=l[u];if(c!==void 0)if(Array.isArray(c))a[u]=c;else if(s(c))a[u]=c;else if(e(c)){const d=a[u];e(d)?a[u]=tu(d,c):a[u]=tu(c)}else a[u]=c}return a}function Dj(n,e){return tu(Oj,n,e||{})}function Mj(n,e){return tu(Nj,n,e||{})}class Uj{constructor(e){this.client=e,this.enroll=this._enroll.bind(this),this.challenge=this._challenge.bind(this),this.verify=this._verify.bind(this),this.authenticate=this._authenticate.bind(this),this.register=this._register.bind(this)}async _enroll(e){return this.client.mfa.enroll(Object.assign(Object.assign({},e),{factorType:"webauthn"}))}async _challenge({factorId:e,webauthn:s,friendlyName:a,signal:l},u){var c;try{const{data:d,error:m}=await this.client.mfa.challenge({factorId:e,webauthn:s});if(!d)return{data:null,error:m};const p=l??Pd.createNewAbortSignal();if(d.webauthn.type==="create"){const{user:g}=d.webauthn.credential_options.publicKey;if(!g.name){const v=a;if(v)g.name=`${g.id}:${v}`;else{const x=(await this.client.getUser()).data.user,S=((c=x==null?void 0:x.user_metadata)===null||c===void 0?void 0:c.name)||(x==null?void 0:x.email)||(x==null?void 0:x.id)||"User";g.name=`${g.id}:${S}`}}g.displayName||(g.displayName=g.name)}switch(d.webauthn.type){case"create":{const g=Dj(d.webauthn.credential_options.publicKey,u==null?void 0:u.create),{data:v,error:w}=await c_({publicKey:g,signal:p});return v?{data:{factorId:e,challengeId:d.id,webauthn:{type:d.webauthn.type,credential_response:v}},error:null}:{data:null,error:w}}case"request":{const g=Mj(d.webauthn.credential_options.publicKey,u==null?void 0:u.request),{data:v,error:w}=await h_(Object.assign(Object.assign({},d.webauthn.credential_options),{publicKey:g,signal:p}));return v?{data:{factorId:e,challengeId:d.id,webauthn:{type:d.webauthn.type,credential_response:v}},error:null}:{data:null,error:w}}}}catch(d){return te(d)?{data:null,error:d}:{data:null,error:new vn("Unexpected error in challenge",d)}}}async _verify({challengeId:e,factorId:s,webauthn:a}){return this.client.mfa.verify({factorId:s,challengeId:e,webauthn:a})}async _authenticate({factorId:e,webauthn:{rpId:s=typeof window<"u"?window.location.hostname:void 0,rpOrigins:a=typeof window<"u"?[window.location.origin]:void 0,signal:l}={}},u){if(!s)return{data:null,error:new Pr("rpId is required for WebAuthn authentication")};try{if(!eu())return{data:null,error:new vn("Browser does not support WebAuthn",null)};const{data:c,error:d}=await this.challenge({factorId:e,webauthn:{rpId:s,rpOrigins:a},signal:l},{request:u});if(!c)return{data:null,error:d};const{webauthn:m}=c;return this._verify({factorId:e,challengeId:c.challengeId,webauthn:{type:m.type,rpId:s,rpOrigins:a,credential_response:m.credential_response}})}catch(c){return te(c)?{data:null,error:c}:{data:null,error:new vn("Unexpected error in authenticate",c)}}}async _register({friendlyName:e,webauthn:{rpId:s=typeof window<"u"?window.location.hostname:void 0,rpOrigins:a=typeof window<"u"?[window.location.origin]:void 0,signal:l}={}},u){if(!s)return{data:null,error:new Pr("rpId is required for WebAuthn registration")};try{if(!eu())return{data:null,error:new vn("Browser does not support WebAuthn",null)};const{data:c,error:d}=await this._enroll({friendlyName:e});if(!c)return await this.client.mfa.listFactors().then(g=>{var v;return(v=g.data)===null||v===void 0?void 0:v.all.find(w=>w.factor_type==="webauthn"&&w.friendly_name===e&&w.status!=="unverified")}).then(g=>g?this.client.mfa.unenroll({factorId:g==null?void 0:g.id}):void 0),{data:null,error:d};const{data:m,error:p}=await this._challenge({factorId:c.id,friendlyName:c.friendly_name,webauthn:{rpId:s,rpOrigins:a},signal:l},{create:u});return m?this._verify({factorId:c.id,challengeId:m.challengeId,webauthn:{rpId:s,rpOrigins:a,type:m.webauthn.type,credential_response:m.webauthn.credential_response}}):{data:null,error:p}}catch(c){return te(c)?{data:null,error:c}:{data:null,error:new vn("Unexpected error in register",c)}}}}Tj();const Bj={url:Vk,storageKey:Pk,autoRefreshToken:!0,persistSession:!0,detectSessionInUrl:!0,headers:Hk,flowType:"implicit",debug:!1,hasCustomAuthorizationHeader:!1,throwOnError:!1,lockAcquireTimeout:5e3,skipAutoInitialize:!1,experimental:{}},ia={};class Hr{get jwks(){var e,s;return(s=(e=ia[this.storageKey])===null||e===void 0?void 0:e.jwks)!==null&&s!==void 0?s:{keys:[]}}set jwks(e){ia[this.storageKey]=Object.assign(Object.assign({},ia[this.storageKey]),{jwks:e})}get jwks_cached_at(){var e,s;return(s=(e=ia[this.storageKey])===null||e===void 0?void 0:e.cachedAt)!==null&&s!==void 0?s:Number.MIN_SAFE_INTEGER}set jwks_cached_at(e){ia[this.storageKey]=Object.assign(Object.assign({},ia[this.storageKey]),{cachedAt:e})}constructor(e){var s,a,l;this.userStorage=null,this.memoryStorage=null,this.stateChangeEmitters=new Map,this.autoRefreshTicker=null,this.autoRefreshTickTimeout=null,this.visibilityChangedCallback=null,this.refreshingDeferred=null,this.lastRefreshFailure=null,this._sessionRemovalEpoch=0,this.initializePromise=null,this._pendingInitNotifications=null,this.detectSessionInUrl=!0,this.hasCustomAuthorizationHeader=!1,this.suppressGetSessionWarning=!1,this.lock=null,this.lockAcquired=!1,this.pendingInLock=[],this.broadcastChannel=null,this.logger=console.log;const u=Object.assign(Object.assign({},Bj),e);if(this.storageKey=u.storageKey,this.instanceID=(s=Hr.nextInstanceID[this.storageKey])!==null&&s!==void 0?s:0,Hr.nextInstanceID[this.storageKey]=this.instanceID+1,this.logDebugMessages=!!u.debug,typeof u.debug=="function"&&(this.logger=u.debug),this.instanceID>0&&xt()){const c=`${this._logPrefix()} Multiple GoTrueClient instances detected in the same browser context. It is not an error, but this should be avoided as it may produce undefined behavior when used concurrently under the same storage key.`;console.warn(c),this.logDebugMessages&&console.trace(c)}if(this.persistSession=u.persistSession,this.autoRefreshToken=u.autoRefreshToken,this.experimental=(a=u.experimental)!==null&&a!==void 0?a:{},this.admin=new xj({url:u.url,headers:u.headers,fetch:u.fetch,experimental:this.experimental}),this.url=u.url,this.headers=u.headers,this.fetch=o_(u.fetch),this.detectSessionInUrl=u.detectSessionInUrl,this.flowType=u.flowType,this.hasCustomAuthorizationHeader=u.hasCustomAuthorizationHeader,this.throwOnError=u.throwOnError,this.lockAcquireTimeout=u.lockAcquireTimeout,u.lock!=null&&(this.lock=u.lock),this.jwks||(this.jwks={keys:[]},this.jwks_cached_at=Number.MIN_SAFE_INTEGER),this.mfa={verify:this._verify.bind(this),enroll:this._enroll.bind(this),unenroll:this._unenroll.bind(this),challenge:this._challenge.bind(this),listFactors:this._listFactors.bind(this),challengeAndVerify:this._challengeAndVerify.bind(this),getAuthenticatorAssuranceLevel:this._getAuthenticatorAssuranceLevel.bind(this),webauthn:new Uj(this)},this.oauth={getAuthorizationDetails:this._getAuthorizationDetails.bind(this),approveAuthorization:this._approveAuthorization.bind(this),denyAuthorization:this._denyAuthorization.bind(this),listGrants:this._listOAuthGrants.bind(this),revokeGrant:this._revokeOAuthGrant.bind(this)},this.passkey={startRegistration:this._startPasskeyRegistration.bind(this),verifyRegistration:this._verifyPasskeyRegistration.bind(this),startAuthentication:this._startPasskeyAuthentication.bind(this),verifyAuthentication:this._verifyPasskeyAuthentication.bind(this),list:this._listPasskeys.bind(this),update:this._updatePasskey.bind(this),delete:this._deletePasskey.bind(this)},this.persistSession?(u.storage?this.storage=u.storage:r_()?this.storage=globalThis.localStorage:(this.memoryStorage={},this.storage=Pv(this.memoryStorage)),u.userStorage&&(this.userStorage=u.userStorage)):(this.memoryStorage={},this.storage=Pv(this.memoryStorage)),xt()&&globalThis.BroadcastChannel&&this.persistSession&&this.storageKey){try{this.broadcastChannel=new globalThis.BroadcastChannel(this.storageKey)}catch(c){console.error("Failed to create a new BroadcastChannel, multi-tab state changes will not be available",c)}(l=this.broadcastChannel)===null||l===void 0||l.addEventListener("message",async c=>{this._debug("received broadcast notification from other tab or client",c),(c.data.event==="TOKEN_REFRESHED"||c.data.event==="SIGNED_IN")&&(this.lastRefreshFailure=null);try{await this._notifyAllSubscribers(c.data.event,c.data.session,!1)}catch(d){this._debug("#broadcastChannel","error",d)}})}u.skipAutoInitialize||this.initialize().catch(c=>{this._debug("#initialize()","error",c)})}isThrowOnErrorEnabled(){return this.throwOnError}_returnResult(e){if(this.throwOnError&&e&&e.error)throw e.error;return e}_logPrefix(){return`GoTrueClient@${this.storageKey}:${this.instanceID} (${i_}) ${new Date().toISOString()}`}_debug(...e){return this.logDebugMessages&&this.logger(this._logPrefix(),...e),this}async initialize(){var e;if(this.initializePromise)return await this.initializePromise;this._pendingInitNotifications=[],this.initializePromise=(async()=>this.lock!=null?await this._acquireLock(this.lockAcquireTimeout,async()=>await this._initialize()):await this._initialize())();const s=await this.initializePromise,a=(e=this._pendingInitNotifications)!==null&&e!==void 0?e:[];this._pendingInitNotifications=null;for(const l of a)await this._notifyAllSubscribers(l.event,l.session,l.broadcast);return s}async _initialize(){var e;try{let s={},a="none";if(xt()&&(s=nj(window.location.href),this._isImplicitGrantCallback(s)?a="implicit":await this._isPKCECallback(s)&&(a="pkce")),xt()&&this.detectSessionInUrl&&a!=="none"){const{data:l,error:u}=await this._getSessionFromURL(s,a);if(u){if(this._debug("#_initialize()","error detecting session from URL",u),Kk(u)){const m=(e=u.details)===null||e===void 0?void 0:e.code;if(m==="identity_already_exists"||m==="identity_not_found"||m==="single_identity_not_deletable")return{error:u}}return{error:u}}const{session:c,redirectType:d}=l;return this._debug("#_initialize()","detected session in URL",c,"redirect type",d),await this._saveSession(c),setTimeout(async()=>{d==="recovery"?await this._notifyAllSubscribers("PASSWORD_RECOVERY",c):await this._notifyAllSubscribers("SIGNED_IN",c)},0),{error:null}}return await this._recoverAndRefresh(),{error:null}}catch(s){return te(s)?this._returnResult({error:s}):this._returnResult({error:new vn("Unexpected error during initialization",s)})}finally{await this._handleVisibilityChange(),this._debug("#_initialize()","end")}}async signInAnonymously(e){var s,a,l;try{const u=await ae(this.fetch,"POST",`${this.url}/signup`,{headers:this.headers,body:{data:(a=(s=e==null?void 0:e.options)===null||s===void 0?void 0:s.data)!==null&&a!==void 0?a:{},gotrue_meta_security:{captcha_token:(l=e==null?void 0:e.options)===null||l===void 0?void 0:l.captchaToken}},xform:rn}),{data:c,error:d}=u;if(d||!c)return this._returnResult({data:{user:null,session:null},error:d});const m=c.session,p=c.user;return c.session&&(await this._saveSession(c.session),await this._notifyAllSubscribers("SIGNED_IN",m)),this._returnResult({data:{user:p,session:m},error:null})}catch(u){if(te(u))return this._returnResult({data:{user:null,session:null},error:u});throw u}}async signUp(e){var s,a,l;try{let u;if("email"in e){const{email:g,password:v,options:w}=e;let x=null,S=null;this.flowType==="pkce"&&([x,S]=await os(this.storage,this.storageKey)),u=await ae(this.fetch,"POST",`${this.url}/signup`,{headers:this.headers,redirectTo:w==null?void 0:w.emailRedirectTo,body:{email:g,password:v,data:(s=w==null?void 0:w.data)!==null&&s!==void 0?s:{},gotrue_meta_security:{captcha_token:w==null?void 0:w.captchaToken},code_challenge:x,code_challenge_method:S},xform:rn})}else if("phone"in e){const{phone:g,password:v,options:w}=e;u=await ae(this.fetch,"POST",`${this.url}/signup`,{headers:this.headers,body:{phone:g,password:v,data:(a=w==null?void 0:w.data)!==null&&a!==void 0?a:{},channel:(l=w==null?void 0:w.channel)!==null&&l!==void 0?l:"sms",gotrue_meta_security:{captcha_token:w==null?void 0:w.captchaToken}},xform:rn})}else throw new El("You must provide either an email or phone number and a password");const{data:c,error:d}=u;if(d||!c)return await et(this.storage,`${this.storageKey}-code-verifier`),this._returnResult({data:{user:null,session:null},error:d});const m=c.session,p=c.user;return c.session&&(await this._saveSession(c.session),await this._notifyAllSubscribers("SIGNED_IN",m)),this._returnResult({data:{user:p,session:m},error:null})}catch(u){if(await et(this.storage,`${this.storageKey}-code-verifier`),te(u))return this._returnResult({data:{user:null,session:null},error:u});throw u}}async signInWithPassword(e){try{let s;if("email"in e){const{email:u,password:c,options:d}=e;s=await ae(this.fetch,"POST",`${this.url}/token?grant_type=password`,{headers:this.headers,body:{email:u,password:c,gotrue_meta_security:{captcha_token:d==null?void 0:d.captchaToken}},xform:zv})}else if("phone"in e){const{phone:u,password:c,options:d}=e;s=await ae(this.fetch,"POST",`${this.url}/token?grant_type=password`,{headers:this.headers,body:{phone:u,password:c,gotrue_meta_security:{captcha_token:d==null?void 0:d.captchaToken}},xform:zv})}else throw new El("You must provide either an email or phone number and a password");const{data:a,error:l}=s;if(l)return this._returnResult({data:{user:null,session:null},error:l});if(!a||!a.session||!a.user){const u=new na;return this._returnResult({data:{user:null,session:null},error:u})}return a.session&&(await this._saveSession(a.session),await this._notifyAllSubscribers("SIGNED_IN",a.session)),this._returnResult({data:Object.assign({user:a.user,session:a.session},a.weak_password?{weakPassword:a.weak_password}:null),error:l})}catch(s){if(te(s))return this._returnResult({data:{user:null,session:null},error:s});throw s}}async signInWithOAuth(e){var s,a,l,u;return await this._handleProviderSignIn(e.provider,{redirectTo:(s=e.options)===null||s===void 0?void 0:s.redirectTo,scopes:(a=e.options)===null||a===void 0?void 0:a.scopes,queryParams:(l=e.options)===null||l===void 0?void 0:l.queryParams,skipBrowserRedirect:(u=e.options)===null||u===void 0?void 0:u.skipBrowserRedirect})}async exchangeCodeForSession(e){return await this.initializePromise,this.lock!=null?this._acquireLock(this.lockAcquireTimeout,async()=>this._exchangeCodeForSession(e)):this._exchangeCodeForSession(e)}async signInWithWeb3(e){const{chain:s}=e;switch(s){case"ethereum":return await this.signInWithEthereum(e);case"solana":return await this.signInWithSolana(e);default:throw new Error(`@supabase/auth-js: Unsupported chain "${s}"`)}}async signInWithEthereum(e){var s,a,l,u,c,d,m,p,g,v,w;let x,S;if("message"in e)x=e.message,S=e.signature;else{const{chain:A,wallet:k,statement:C,options:N}=e;let O;if(xt())if(typeof k=="object")O=k;else{const ee=window;if("ethereum"in ee&&typeof ee.ethereum=="object"&&"request"in ee.ethereum&&typeof ee.ethereum.request=="function")O=ee.ethereum;else throw new Error("@supabase/auth-js: No compatible Ethereum wallet interface on the window object (window.ethereum) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'ethereum', wallet: resolvedUserWallet }) instead.")}else{if(typeof k!="object"||!(N!=null&&N.url))throw new Error("@supabase/auth-js: Both wallet and url must be specified in non-browser environments.");O=k}const P=new URL((s=N==null?void 0:N.url)!==null&&s!==void 0?s:window.location.href),J=await O.request({method:"eth_requestAccounts"}).then(ee=>ee).catch(()=>{throw new Error("@supabase/auth-js: Wallet method eth_requestAccounts is missing or invalid")});if(!J||J.length===0)throw new Error("@supabase/auth-js: No accounts available. Please ensure the wallet is connected.");const X=l_(J[0]);let G=(a=N==null?void 0:N.signInWithEthereum)===null||a===void 0?void 0:a.chainId;if(!G){const ee=await O.request({method:"eth_chainId"});G=Ej(ee)}const Z={domain:P.host,address:X,statement:C,uri:P.href,version:"1",chainId:G,nonce:(l=N==null?void 0:N.signInWithEthereum)===null||l===void 0?void 0:l.nonce,issuedAt:(c=(u=N==null?void 0:N.signInWithEthereum)===null||u===void 0?void 0:u.issuedAt)!==null&&c!==void 0?c:new Date,expirationTime:(d=N==null?void 0:N.signInWithEthereum)===null||d===void 0?void 0:d.expirationTime,notBefore:(m=N==null?void 0:N.signInWithEthereum)===null||m===void 0?void 0:m.notBefore,requestId:(p=N==null?void 0:N.signInWithEthereum)===null||p===void 0?void 0:p.requestId,resources:(g=N==null?void 0:N.signInWithEthereum)===null||g===void 0?void 0:g.resources};x=Rj(Z),S=await O.request({method:"personal_sign",params:[Aj(x),X]})}try{const{data:A,error:k}=await ae(this.fetch,"POST",`${this.url}/token?grant_type=web3`,{headers:this.headers,body:Object.assign({chain:"ethereum",message:x,signature:S},!((v=e.options)===null||v===void 0)&&v.captchaToken?{gotrue_meta_security:{captcha_token:(w=e.options)===null||w===void 0?void 0:w.captchaToken}}:null),xform:rn});if(k)throw k;if(!A||!A.session||!A.user){const C=new na;return this._returnResult({data:{user:null,session:null},error:C})}return A.session&&(await this._saveSession(A.session),await this._notifyAllSubscribers("SIGNED_IN",A.session)),this._returnResult({data:Object.assign({},A),error:k})}catch(A){if(te(A))return this._returnResult({data:{user:null,session:null},error:A});throw A}}async signInWithSolana(e){var s,a,l,u,c,d,m,p,g,v,w,x;let S,A;if("message"in e)S=e.message,A=e.signature;else{const{chain:k,wallet:C,statement:N,options:O}=e;let P;if(xt())if(typeof C=="object")P=C;else{const X=window;if("solana"in X&&typeof X.solana=="object"&&("signIn"in X.solana&&typeof X.solana.signIn=="function"||"signMessage"in X.solana&&typeof X.solana.signMessage=="function"))P=X.solana;else throw new Error("@supabase/auth-js: No compatible Solana wallet interface on the window object (window.solana) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'solana', wallet: resolvedUserWallet }) instead.")}else{if(typeof C!="object"||!(O!=null&&O.url))throw new Error("@supabase/auth-js: Both wallet and url must be specified in non-browser environments.");P=C}const J=new URL((s=O==null?void 0:O.url)!==null&&s!==void 0?s:window.location.href);if("signIn"in P&&P.signIn){const X=await P.signIn(Object.assign(Object.assign(Object.assign({issuedAt:new Date().toISOString()},O==null?void 0:O.signInWithSolana),{version:"1",domain:J.host,uri:J.href}),N?{statement:N}:null));let G;if(Array.isArray(X)&&X[0]&&typeof X[0]=="object")G=X[0];else if(X&&typeof X=="object"&&"signedMessage"in X&&"signature"in X)G=X;else throw new Error("@supabase/auth-js: Wallet method signIn() returned unrecognized value");if("signedMessage"in G&&"signature"in G&&(typeof G.signedMessage=="string"||G.signedMessage instanceof Uint8Array)&&G.signature instanceof Uint8Array)S=typeof G.signedMessage=="string"?G.signedMessage:new TextDecoder().decode(G.signedMessage),A=G.signature;else throw new Error("@supabase/auth-js: Wallet method signIn() API returned object without signedMessage and signature fields")}else{if(!("signMessage"in P)||typeof P.signMessage!="function"||!("publicKey"in P)||typeof P!="object"||!P.publicKey||!("toBase58"in P.publicKey)||typeof P.publicKey.toBase58!="function")throw new Error("@supabase/auth-js: Wallet does not have a compatible signMessage() and publicKey.toBase58() API");S=[`${J.host} wants you to sign in with your Solana account:`,P.publicKey.toBase58(),...N?["",N,""]:[""],"Version: 1",`URI: ${J.href}`,`Issued At: ${(l=(a=O==null?void 0:O.signInWithSolana)===null||a===void 0?void 0:a.issuedAt)!==null&&l!==void 0?l:new Date().toISOString()}`,...!((u=O==null?void 0:O.signInWithSolana)===null||u===void 0)&&u.notBefore?[`Not Before: ${O.signInWithSolana.notBefore}`]:[],...!((c=O==null?void 0:O.signInWithSolana)===null||c===void 0)&&c.expirationTime?[`Expiration Time: ${O.signInWithSolana.expirationTime}`]:[],...!((d=O==null?void 0:O.signInWithSolana)===null||d===void 0)&&d.chainId?[`Chain ID: ${O.signInWithSolana.chainId}`]:[],...!((m=O==null?void 0:O.signInWithSolana)===null||m===void 0)&&m.nonce?[`Nonce: ${O.signInWithSolana.nonce}`]:[],...!((p=O==null?void 0:O.signInWithSolana)===null||p===void 0)&&p.requestId?[`Request ID: ${O.signInWithSolana.requestId}`]:[],...!((v=(g=O==null?void 0:O.signInWithSolana)===null||g===void 0?void 0:g.resources)===null||v===void 0)&&v.length?["Resources",...O.signInWithSolana.resources.map(G=>`- ${G}`)]:[]].join(` -`);const X=await P.signMessage(new TextEncoder().encode(S),"utf8");if(!X||!(X instanceof Uint8Array))throw new Error("@supabase/auth-js: Wallet signMessage() API returned an recognized value");A=X}}try{const{data:k,error:C}=await ae(this.fetch,"POST",`${this.url}/token?grant_type=web3`,{headers:this.headers,body:Object.assign({chain:"solana",message:S,signature:fs(A)},!((w=e.options)===null||w===void 0)&&w.captchaToken?{gotrue_meta_security:{captcha_token:(x=e.options)===null||x===void 0?void 0:x.captchaToken}}:null),xform:rn});if(C)throw C;if(!k||!k.session||!k.user){const N=new na;return this._returnResult({data:{user:null,session:null},error:N})}return k.session&&(await this._saveSession(k.session),await this._notifyAllSubscribers("SIGNED_IN",k.session)),this._returnResult({data:Object.assign({},k),error:C})}catch(k){if(te(k))return this._returnResult({data:{user:null,session:null},error:k});throw k}}async _exchangeCodeForSession(e){const s=await fn(this.storage,`${this.storageKey}-code-verifier`),[a,l]=(s??"").split("/");try{if(!a&&this.flowType==="pkce")throw new Fk;const{data:u,error:c}=await ae(this.fetch,"POST",`${this.url}/token?grant_type=pkce`,{headers:this.headers,body:{auth_code:e,code_verifier:a},xform:rn});if(await et(this.storage,`${this.storageKey}-code-verifier`),c)throw c;if(!u||!u.session||!u.user){const d=new na;return this._returnResult({data:{user:null,session:null,redirectType:null},error:d})}return u.session&&(await this._saveSession(u.session),await this._notifyAllSubscribers(l==="recovery"?"PASSWORD_RECOVERY":"SIGNED_IN",u.session)),this._returnResult({data:Object.assign(Object.assign({},u),{redirectType:l??null}),error:c})}catch(u){if(await et(this.storage,`${this.storageKey}-code-verifier`),te(u))return this._returnResult({data:{user:null,session:null,redirectType:null},error:u});throw u}}async signInWithIdToken(e){try{const{options:s,provider:a,token:l,access_token:u,nonce:c}=e,d=await ae(this.fetch,"POST",`${this.url}/token?grant_type=id_token`,{headers:this.headers,body:{provider:a,id_token:l,access_token:u,nonce:c,gotrue_meta_security:{captcha_token:s==null?void 0:s.captchaToken}},xform:rn}),{data:m,error:p}=d;if(p)return this._returnResult({data:{user:null,session:null},error:p});if(!m||!m.session||!m.user){const g=new na;return this._returnResult({data:{user:null,session:null},error:g})}return m.session&&(await this._saveSession(m.session),await this._notifyAllSubscribers("SIGNED_IN",m.session)),this._returnResult({data:m,error:p})}catch(s){if(te(s))return this._returnResult({data:{user:null,session:null},error:s});throw s}}async signInWithOtp(e){var s,a,l,u,c;try{if("email"in e){const{email:d,options:m}=e;let p=null,g=null;this.flowType==="pkce"&&([p,g]=await os(this.storage,this.storageKey));const{error:v}=await ae(this.fetch,"POST",`${this.url}/otp`,{headers:this.headers,body:{email:d,data:(s=m==null?void 0:m.data)!==null&&s!==void 0?s:{},create_user:(a=m==null?void 0:m.shouldCreateUser)!==null&&a!==void 0?a:!0,gotrue_meta_security:{captcha_token:m==null?void 0:m.captchaToken},code_challenge:p,code_challenge_method:g},redirectTo:m==null?void 0:m.emailRedirectTo});return this._returnResult({data:{user:null,session:null},error:v})}if("phone"in e){const{phone:d,options:m}=e,{data:p,error:g}=await ae(this.fetch,"POST",`${this.url}/otp`,{headers:this.headers,body:{phone:d,data:(l=m==null?void 0:m.data)!==null&&l!==void 0?l:{},create_user:(u=m==null?void 0:m.shouldCreateUser)!==null&&u!==void 0?u:!0,gotrue_meta_security:{captcha_token:m==null?void 0:m.captchaToken},channel:(c=m==null?void 0:m.channel)!==null&&c!==void 0?c:"sms"}});return this._returnResult({data:{user:null,session:null,messageId:p==null?void 0:p.message_id},error:g})}throw new El("You must provide either an email or phone number.")}catch(d){if(await et(this.storage,`${this.storageKey}-code-verifier`),te(d))return this._returnResult({data:{user:null,session:null},error:d});throw d}}async verifyOtp(e){var s,a;try{let l,u;"options"in e&&(l=(s=e.options)===null||s===void 0?void 0:s.redirectTo,u=(a=e.options)===null||a===void 0?void 0:a.captchaToken);const{data:c,error:d}=await ae(this.fetch,"POST",`${this.url}/verify`,{headers:this.headers,body:Object.assign(Object.assign({},e),{gotrue_meta_security:{captcha_token:u}}),redirectTo:l,xform:rn});if(d)throw d;if(!c)throw new Error("An error occurred on token verification.");const m=c.session,p=c.user;return m!=null&&m.access_token&&(await this._saveSession(m),await this._notifyAllSubscribers(e.type=="recovery"?"PASSWORD_RECOVERY":"SIGNED_IN",m)),this._returnResult({data:{user:p,session:m},error:null})}catch(l){if(te(l))return this._returnResult({data:{user:null,session:null},error:l});throw l}}async signInWithSSO(e){var s,a,l,u,c;try{let d=null,m=null;this.flowType==="pkce"&&([d,m]=await os(this.storage,this.storageKey));const p=await ae(this.fetch,"POST",`${this.url}/sso`,{body:Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},"providerId"in e?{provider_id:e.providerId}:null),"domain"in e?{domain:e.domain}:null),{redirect_to:(a=(s=e.options)===null||s===void 0?void 0:s.redirectTo)!==null&&a!==void 0?a:void 0}),!((l=e==null?void 0:e.options)===null||l===void 0)&&l.captchaToken?{gotrue_meta_security:{captcha_token:e.options.captchaToken}}:null),{skip_http_redirect:!0,code_challenge:d,code_challenge_method:m}),headers:this.headers,xform:bj});return!((u=p.data)===null||u===void 0)&&u.url&&xt()&&!(!((c=e.options)===null||c===void 0)&&c.skipBrowserRedirect)&&window.location.assign(p.data.url),this._returnResult(p)}catch(d){if(await et(this.storage,`${this.storageKey}-code-verifier`),te(d))return this._returnResult({data:null,error:d});throw d}}async reauthenticate(){return await this.initializePromise,this.lock!=null?await this._acquireLock(this.lockAcquireTimeout,async()=>await this._reauthenticate()):await this._reauthenticate()}async _reauthenticate(){try{return await this._useSession(async e=>{const{data:{session:s},error:a}=e;if(a)throw a;if(!s)throw new ht;const{error:l}=await ae(this.fetch,"GET",`${this.url}/reauthenticate`,{headers:this.headers,jwt:s.access_token});return this._returnResult({data:{user:null,session:null},error:l})})}catch(e){if(te(e))return this._returnResult({data:{user:null,session:null},error:e});throw e}}async resend(e){try{const s=`${this.url}/resend`;if("email"in e){const{email:a,type:l,options:u}=e;let c=null,d=null;this.flowType==="pkce"&&([c,d]=await os(this.storage,this.storageKey));const{error:m}=await ae(this.fetch,"POST",s,{headers:this.headers,body:{email:a,type:l,gotrue_meta_security:{captcha_token:u==null?void 0:u.captchaToken},code_challenge:c,code_challenge_method:d},redirectTo:u==null?void 0:u.emailRedirectTo});return m&&await et(this.storage,`${this.storageKey}-code-verifier`),this._returnResult({data:{user:null,session:null},error:m})}else if("phone"in e){const{phone:a,type:l,options:u}=e,{data:c,error:d}=await ae(this.fetch,"POST",s,{headers:this.headers,body:{phone:a,type:l,gotrue_meta_security:{captcha_token:u==null?void 0:u.captchaToken}}});return this._returnResult({data:{user:null,session:null,messageId:c==null?void 0:c.message_id},error:d})}throw new El("You must provide either an email or phone number and a type")}catch(s){if(await et(this.storage,`${this.storageKey}-code-verifier`),te(s))return this._returnResult({data:{user:null,session:null},error:s});throw s}}async getSession(){return await this.initializePromise,this.lock!=null?await this._acquireLock(this.lockAcquireTimeout,async()=>this._useSession(async e=>e)):await this._useSession(async e=>e)}async _acquireLock(e,s){this._debug("#_acquireLock","begin",e);try{if(this.lockAcquired){const a=this.pendingInLock.length?this.pendingInLock[this.pendingInLock.length-1]:Promise.resolve(),l=(async()=>(await a,await s()))();return this.pendingInLock.push((async()=>{try{await l}catch{}})()),l}return await this.lock(`lock:${this.storageKey}`,e,async()=>{this._debug("#_acquireLock","lock acquired for storage key",this.storageKey);try{this.lockAcquired=!0;const a=s();for(this.pendingInLock.push((async()=>{try{await a}catch{}})()),await a;this.pendingInLock.length;){const l=[...this.pendingInLock];await Promise.all(l),this.pendingInLock.splice(0,l.length)}return await a}finally{this._debug("#_acquireLock","lock released for storage key",this.storageKey),this.lockAcquired=!1}})}finally{this._debug("#_acquireLock","end")}}async _useSession(e){this._debug("#_useSession","begin");try{const s=await this.__loadSession();return await e(s)}finally{this._debug("#_useSession","end")}}async __loadSession(){this._debug("#__loadSession()","begin"),this.lock!=null&&!this.lockAcquired&&this._debug("#__loadSession()","used outside of an acquired lock!",new Error().stack);try{let e=null;const s=await fn(this.storage,this.storageKey);if(this._debug("#getSession()","session from storage",s),s!==null&&(this._isValidSession(s)?e=s:(this._debug("#getSession()","session from storage is not valid"),await this._removeSession())),!e)return{data:{session:null},error:null};const a=e.expires_at?e.expires_at*1e3-Date.now()Date.now())){const d=await fn(this.storage,this.storageKey);if(d&&d.refresh_token===e.refresh_token)return this._returnResult({data:{session:e},error:null})}return this._returnResult({data:{session:null},error:u})}return this._returnResult({data:{session:l},error:null})}finally{this._debug("#__loadSession()","end")}}async getUser(e){if(e)return await this._getUser(e);await this.initializePromise;let s;return this.lock!=null?s=await this._acquireLock(this.lockAcquireTimeout,async()=>await this._getUser()):s=await this._getUser(),s.data.user&&(this.suppressGetSessionWarning=!0),s}async _getUser(e){try{return e?await ae(this.fetch,"GET",`${this.url}/user`,{headers:this.headers,jwt:e,xform:Ui}):await this._useSession(async s=>{var a,l,u;const{data:c,error:d}=s;if(d)throw d;return!(!((a=c.session)===null||a===void 0)&&a.access_token)&&!this.hasCustomAuthorizationHeader?{data:{user:null},error:new ht}:await ae(this.fetch,"GET",`${this.url}/user`,{headers:this.headers,jwt:(u=(l=c.session)===null||l===void 0?void 0:l.access_token)!==null&&u!==void 0?u:void 0,xform:Ui})})}catch(s){if(te(s))return Tl(s)&&(await this._removeSession(),await et(this.storage,`${this.storageKey}-code-verifier`)),this._returnResult({data:{user:null},error:s});throw s}}async updateUser(e,s={}){return await this.initializePromise,this.lock!=null?await this._acquireLock(this.lockAcquireTimeout,async()=>await this._updateUser(e,s)):await this._updateUser(e,s)}async _updateUser(e,s={}){try{return await this._useSession(async a=>{const{data:l,error:u}=a;if(u)throw u;if(!l.session)throw new ht;const c=l.session;let d=null,m=null;this.flowType==="pkce"&&e.email!=null&&([d,m]=await os(this.storage,this.storageKey));const{data:p,error:g}=await ae(this.fetch,"PUT",`${this.url}/user`,{headers:this.headers,redirectTo:s==null?void 0:s.emailRedirectTo,body:Object.assign(Object.assign({},e),{code_challenge:d,code_challenge_method:m}),jwt:c.access_token,xform:Ui});if(g)throw g;return c.user=p.user,await this._saveSession(c),await this._notifyAllSubscribers("USER_UPDATED",c),this._returnResult({data:{user:c.user},error:null})})}catch(a){if(await et(this.storage,`${this.storageKey}-code-verifier`),te(a))return this._returnResult({data:{user:null},error:a});throw a}}async setSession(e){return await this.initializePromise,this.lock!=null?await this._acquireLock(this.lockAcquireTimeout,async()=>await this._setSession(e)):await this._setSession(e)}async _setSession(e){try{if(!e.access_token||!e.refresh_token)throw new ht;const s=Date.now()/1e3;let a=s,l=!0,u=null;const{payload:c}=Rl(e.access_token);if(c.exp&&(a=c.exp,l=a<=s),l){const{data:d,error:m}=await this._callRefreshToken(e.refresh_token);if(m)return this._returnResult({data:{user:null,session:null},error:m});if(!d)return{data:{user:null,session:null},error:null};u=d}else{const{data:d,error:m}=await this._getUser(e.access_token);if(m)return this._returnResult({data:{user:null,session:null},error:m});u={access_token:e.access_token,refresh_token:e.refresh_token,user:d.user,token_type:"bearer",expires_in:a-s,expires_at:a},await this._saveSession(u),await this._notifyAllSubscribers("SIGNED_IN",u)}return this._returnResult({data:{user:u.user,session:u},error:null})}catch(s){if(te(s))return this._returnResult({data:{session:null,user:null},error:s});throw s}}async refreshSession(e){return await this.initializePromise,this.lock!=null?await this._acquireLock(this.lockAcquireTimeout,async()=>await this._refreshSession(e)):await this._refreshSession(e)}async _refreshSession(e){try{return await this._useSession(async s=>{var a;if(!e){const{data:c,error:d}=s;if(d)throw d;e=(a=c.session)!==null&&a!==void 0?a:void 0}if(!(e!=null&&e.refresh_token))throw new ht;const{data:l,error:u}=await this._callRefreshToken(e.refresh_token);return u?this._returnResult({data:{user:null,session:null},error:u}):l?this._returnResult({data:{user:l.user,session:l},error:null}):this._returnResult({data:{user:null,session:null},error:null})})}catch(s){if(te(s))return this._returnResult({data:{user:null,session:null},error:s});throw s}}async _getSessionFromURL(e,s){var a;try{if(!xt())throw new Al("No browser detected.");if(e.error||e.error_description||e.error_code)throw new Al(e.error_description||"Error in URL with unspecified error_description",{error:e.error||"unspecified_error",code:e.error_code||"unspecified_code"});switch(s){case"implicit":if(this.flowType==="pkce")throw new kv("Not a valid PKCE flow url.");break;case"pkce":if(this.flowType==="implicit")throw new Al("Not a valid implicit grant flow url.");break;default:}if(s==="pkce"){if(this._debug("#_initialize()","begin","is PKCE flow",!0),!e.code)throw new kv("No code detected.");const{data:O,error:P}=await this._exchangeCodeForSession(e.code);if(P)throw P;const J=new URL(window.location.href);return J.searchParams.delete("code"),window.history.replaceState(window.history.state,"",J.toString()),{data:{session:O.session,redirectType:(a=O.redirectType)!==null&&a!==void 0?a:null},error:null}}const{provider_token:l,provider_refresh_token:u,access_token:c,refresh_token:d,expires_in:m,expires_at:p,token_type:g}=e;if(!c||!m||!d||!g)throw new Al("No session defined in URL");const v=Math.round(Date.now()/1e3),w=parseInt(m);let x=v+w;p&&(x=parseInt(p));const S=x-v;S*1e3<=ri&&console.warn(`@supabase/gotrue-js: Session as retrieved from URL expires in ${S}s, should have been closer to ${w}s`);const A=x-w;v-A>=120?console.warn("@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale",A,x,v):v-A<0&&console.warn("@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clock for skew",A,x,v);const{data:k,error:C}=await this._getUser(c);if(C)throw C;const N={provider_token:l,provider_refresh_token:u,access_token:c,expires_in:w,expires_at:x,refresh_token:d,token_type:g,user:k.user};return window.location.hash="",this._debug("#_getSessionFromURL()","clearing window.location.hash"),this._returnResult({data:{session:N,redirectType:e.type},error:null})}catch(l){if(te(l))return this._returnResult({data:{session:null,redirectType:null},error:l});throw l}}_isImplicitGrantCallback(e){return typeof this.detectSessionInUrl=="function"?this.detectSessionInUrl(new URL(window.location.href),e):!!(e.access_token||e.error||e.error_description||e.error_code)}async _isPKCECallback(e){const s=await fn(this.storage,`${this.storageKey}-code-verifier`);return!!(e.code&&s)}async signOut(e={scope:"global"}){return await this.initializePromise,this.lock!=null?await this._acquireLock(this.lockAcquireTimeout,async()=>await this._signOut(e)):await this._signOut(e)}async _signOut({scope:e}={scope:"global"}){return await this._useSession(async s=>{var a;const l=async()=>{await this._removeSession(),await et(this.storage,`${this.storageKey}-code-verifier`)},{data:u,error:c}=s;if(c&&!Tl(c))return this._returnResult({error:c});const d=(a=u.session)===null||a===void 0?void 0:a.access_token;if(d){const{error:m}=await this.admin.signOut(d,e);if(m&&!(Gk(m)&&(m.status===404||m.status===401||m.status===403)||Tl(m)))return e!=="others"&&await l(),this._returnResult({error:m})}return e!=="others"&&await l(),this._returnResult({error:null})})}onAuthStateChange(e){const s=tj(),a={id:s,callback:e,unsubscribe:()=>{this._debug("#unsubscribe()","state change callback with id removed",s),this.stateChangeEmitters.delete(s)}};return this._debug("#onAuthStateChange()","registered callback with id",s),this.stateChangeEmitters.set(s,a),(async()=>(await this.initializePromise,this.lock!=null?await this._acquireLock(this.lockAcquireTimeout,async()=>{this._emitInitialSession(s)}):await this._emitInitialSession(s)))(),{data:{subscription:a}}}async _emitInitialSession(e){return await this._useSession(async s=>{var a,l;try{const{data:{session:u},error:c}=s;if(c)throw c;await((a=this.stateChangeEmitters.get(e))===null||a===void 0?void 0:a.callback("INITIAL_SESSION",u)),this._debug("INITIAL_SESSION","callback id",e,"session",u)}catch(u){await((l=this.stateChangeEmitters.get(e))===null||l===void 0?void 0:l.callback("INITIAL_SESSION",null)),this._debug("INITIAL_SESSION","callback id",e,"error",u),Tl(u)?console.warn(u):console.error(u)}})}async resetPasswordForEmail(e,s={}){let a=null,l=null;this.flowType==="pkce"&&([a,l]=await os(this.storage,this.storageKey,!0));try{return await ae(this.fetch,"POST",`${this.url}/recover`,{body:{email:e,code_challenge:a,code_challenge_method:l,gotrue_meta_security:{captcha_token:s.captchaToken}},headers:this.headers,redirectTo:s.redirectTo})}catch(u){if(await et(this.storage,`${this.storageKey}-code-verifier`),te(u))return this._returnResult({data:null,error:u});throw u}}async getUserIdentities(){var e;try{const{data:s,error:a}=await this.getUser();if(a)throw a;return this._returnResult({data:{identities:(e=s.user.identities)!==null&&e!==void 0?e:[]},error:null})}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}async linkIdentity(e){return"token"in e?this.linkIdentityIdToken(e):this.linkIdentityOAuth(e)}async linkIdentityOAuth(e){var s;try{const{data:a,error:l}=await this._useSession(async u=>{var c,d,m,p,g;const{data:v,error:w}=u;if(w)throw w;const x=await this._getUrlForProvider(`${this.url}/user/identities/authorize`,e.provider,{redirectTo:(c=e.options)===null||c===void 0?void 0:c.redirectTo,scopes:(d=e.options)===null||d===void 0?void 0:d.scopes,queryParams:(m=e.options)===null||m===void 0?void 0:m.queryParams,skipBrowserRedirect:!0});return await ae(this.fetch,"GET",x,{headers:this.headers,jwt:(g=(p=v.session)===null||p===void 0?void 0:p.access_token)!==null&&g!==void 0?g:void 0})});if(l)throw l;return xt()&&!(!((s=e.options)===null||s===void 0)&&s.skipBrowserRedirect)&&window.location.assign(a==null?void 0:a.url),this._returnResult({data:{provider:e.provider,url:a==null?void 0:a.url},error:null})}catch(a){if(te(a))return this._returnResult({data:{provider:e.provider,url:null},error:a});throw a}}async linkIdentityIdToken(e){return await this._useSession(async s=>{var a;try{const{error:l,data:{session:u}}=s;if(l)throw l;const{options:c,provider:d,token:m,access_token:p,nonce:g}=e,v=await ae(this.fetch,"POST",`${this.url}/token?grant_type=id_token`,{headers:this.headers,jwt:(a=u==null?void 0:u.access_token)!==null&&a!==void 0?a:void 0,body:{provider:d,id_token:m,access_token:p,nonce:g,link_identity:!0,gotrue_meta_security:{captcha_token:c==null?void 0:c.captchaToken}},xform:rn}),{data:w,error:x}=v;return x?this._returnResult({data:{user:null,session:null},error:x}):!w||!w.session||!w.user?this._returnResult({data:{user:null,session:null},error:new na}):(w.session&&(await this._saveSession(w.session),await this._notifyAllSubscribers("USER_UPDATED",w.session)),this._returnResult({data:w,error:x}))}catch(l){if(await et(this.storage,`${this.storageKey}-code-verifier`),te(l))return this._returnResult({data:{user:null,session:null},error:l});throw l}})}async unlinkIdentity(e){try{return await this._useSession(async s=>{var a,l;const{data:u,error:c}=s;if(c)throw c;return await ae(this.fetch,"DELETE",`${this.url}/user/identities/${e.identity_id}`,{headers:this.headers,jwt:(l=(a=u.session)===null||a===void 0?void 0:a.access_token)!==null&&l!==void 0?l:void 0})})}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}async _refreshAccessToken(e){const s="#_refreshAccessToken()";this._debug(s,"begin");try{const a=Date.now();return await aj(async l=>(l>0&&await sj(200*Math.pow(2,l-1)),this._debug(s,"refreshing attempt",l),await ae(this.fetch,"POST",`${this.url}/token?grant_type=refresh_token`,{body:{refresh_token:e},headers:this.headers,xform:rn})),(l,u)=>{const c=200*Math.pow(2,l);return u&&jv(u)&&Date.now()+c-aDate.now())?this._debug(l,"proactive refresh failed, access token still valid — preserving session"):await this._removeSession()}return this.lastRefreshFailure={refreshToken:e,result:c,expiresAt:Date.now()+zk},(s=this.refreshingDeferred)===null||s===void 0||s.resolve(c),c}throw(a=this.refreshingDeferred)===null||a===void 0||a.reject(u),u}finally{this.refreshingDeferred=null,this._debug(l,"end")}}async _notifyAllSubscribers(e,s,a=!0){if(this._pendingInitNotifications!==null&&a){this._pendingInitNotifications.push({event:e,session:s,broadcast:a});return}const l=`#_notifyAllSubscribers(${e})`;this._debug(l,"begin",s,`broadcast = ${a}`);try{this.broadcastChannel&&a&&this.broadcastChannel.postMessage({event:e,session:s});const u=[],c=Array.from(this.stateChangeEmitters.values()).map(async d=>{try{await d.callback(e,s)}catch(m){u.push(m)}});if(await Promise.all(c),u.length>0){for(let d=0;dthis._autoRefreshTokenTick(),ri);this.autoRefreshTicker=e,e&&typeof e=="object"&&typeof e.unref=="function"?e.unref():typeof Deno<"u"&&typeof Deno.unrefTimer=="function"&&Deno.unrefTimer(e);const s=setTimeout(async()=>{await this.initializePromise,await this._autoRefreshTokenTick()},0);this.autoRefreshTickTimeout=s,s&&typeof s=="object"&&typeof s.unref=="function"?s.unref():typeof Deno<"u"&&typeof Deno.unrefTimer=="function"&&Deno.unrefTimer(s)}async _stopAutoRefresh(){this._debug("#_stopAutoRefresh()");const e=this.autoRefreshTicker;this.autoRefreshTicker=null,e&&clearInterval(e);const s=this.autoRefreshTickTimeout;this.autoRefreshTickTimeout=null,s&&clearTimeout(s)}async startAutoRefresh(){this._removeVisibilityChangedCallback(),await this._startAutoRefresh()}async stopAutoRefresh(){this._removeVisibilityChangedCallback(),await this._stopAutoRefresh()}async dispose(){var e;this._removeVisibilityChangedCallback(),await this._stopAutoRefresh(),(e=this.broadcastChannel)===null||e===void 0||e.close(),this.broadcastChannel=null,this.stateChangeEmitters.clear()}async _autoRefreshTokenTick(){if(this._debug("#_autoRefreshTokenTick()","begin"),this.lock!=null){try{await this._acquireLock(0,async()=>{try{const e=Date.now();try{return await this._useSession(async s=>{const{data:{session:a}}=s;if(!a||!a.refresh_token||!a.expires_at){this._debug("#_autoRefreshTokenTick()","no session");return}const l=Math.floor((a.expires_at*1e3-e)/ri);this._debug("#_autoRefreshTokenTick()",`access token expires in ${l} ticks, a tick lasts ${ri}ms, refresh threshold is ${wr} ticks`),l<=wr&&await this._callRefreshToken(a.refresh_token)})}catch(s){console.error("Auto refresh tick failed with error. This is likely a transient error.",s)}}finally{this._debug("#_autoRefreshTokenTick()","end")}})}catch(e){if(e instanceof Sj)this._debug("auto refresh token tick lock not available");else throw e}return}if(this.refreshingDeferred!==null){this._debug("#_autoRefreshTokenTick()","refresh already in flight, skipping");return}try{const e=Date.now();try{await this._useSession(async s=>{const{data:{session:a}}=s;if(!a||!a.refresh_token||!a.expires_at){this._debug("#_autoRefreshTokenTick()","no session");return}const l=Math.floor((a.expires_at*1e3-e)/ri);this._debug("#_autoRefreshTokenTick()",`access token expires in ${l} ticks, a tick lasts ${ri}ms, refresh threshold is ${wr} ticks`),l<=wr&&await this._callRefreshToken(a.refresh_token)})}catch(s){console.error("Auto refresh tick failed with error. This is likely a transient error.",s)}}finally{this._debug("#_autoRefreshTokenTick()","end")}}async _handleVisibilityChange(){if(this._debug("#_handleVisibilityChange()"),!xt()||!(window!=null&&window.addEventListener))return this.autoRefreshToken&&this.startAutoRefresh(),!1;try{this.visibilityChangedCallback=async()=>{try{await this._onVisibilityChanged(!1)}catch(e){this._debug("#visibilityChangedCallback","error",e)}},window==null||window.addEventListener("visibilitychange",this.visibilityChangedCallback),await this._onVisibilityChanged(!0)}catch(e){console.error("_handleVisibilityChange",e)}}async _onVisibilityChanged(e){const s=`#_onVisibilityChanged(${e})`;if(this._debug(s,"visibilityState",document.visibilityState),document.visibilityState==="visible"){if(this.autoRefreshToken&&this._startAutoRefresh(),!e)if(await this.initializePromise,this.lock!=null)await this._acquireLock(this.lockAcquireTimeout,async()=>{if(document.visibilityState!=="visible"){this._debug(s,"acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting");return}await this._recoverAndRefresh()});else{if(document.visibilityState!=="visible"){this._debug(s,"visibilityState is no longer visible, skipping recovery");return}await this._recoverAndRefresh()}}else document.visibilityState==="hidden"&&this.autoRefreshToken&&this._stopAutoRefresh()}async _getUrlForProvider(e,s,a){const l=[`provider=${encodeURIComponent(s)}`];if(a!=null&&a.redirectTo&&l.push(`redirect_to=${encodeURIComponent(a.redirectTo)}`),a!=null&&a.scopes&&l.push(`scopes=${encodeURIComponent(a.scopes)}`),this.flowType==="pkce"){const[u,c]=await os(this.storage,this.storageKey),d=new URLSearchParams({code_challenge:`${encodeURIComponent(u)}`,code_challenge_method:`${encodeURIComponent(c)}`});l.push(d.toString())}if(a!=null&&a.queryParams){const u=new URLSearchParams(a.queryParams);l.push(u.toString())}return a!=null&&a.skipBrowserRedirect&&l.push(`skip_http_redirect=${a.skipBrowserRedirect}`),`${e}?${l.join("&")}`}async _unenroll(e){try{return await this._useSession(async s=>{var a;const{data:l,error:u}=s;return u?this._returnResult({data:null,error:u}):await ae(this.fetch,"DELETE",`${this.url}/factors/${e.factorId}`,{headers:this.headers,jwt:(a=l==null?void 0:l.session)===null||a===void 0?void 0:a.access_token})})}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}async _enroll(e){try{return await this._useSession(async s=>{var a,l;const{data:u,error:c}=s;if(c)return this._returnResult({data:null,error:c});const d=Object.assign({friendly_name:e.friendlyName,factor_type:e.factorType},e.factorType==="phone"?{phone:e.phone}:e.factorType==="totp"?{issuer:e.issuer}:{}),{data:m,error:p}=await ae(this.fetch,"POST",`${this.url}/factors`,{body:d,headers:this.headers,jwt:(a=u==null?void 0:u.session)===null||a===void 0?void 0:a.access_token});return p?this._returnResult({data:null,error:p}):(e.factorType==="totp"&&m.type==="totp"&&(!((l=m==null?void 0:m.totp)===null||l===void 0)&&l.qr_code)&&(m.totp.qr_code=`data:image/svg+xml;utf-8,${m.totp.qr_code}`),this._returnResult({data:m,error:null}))})}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}async _verify(e){const s=async()=>{try{return await this._useSession(async a=>{var l;const{data:u,error:c}=a;if(c)return this._returnResult({data:null,error:c});const d=Object.assign({challenge_id:e.challengeId},"webauthn"in e?{webauthn:Object.assign(Object.assign({},e.webauthn),{credential_response:e.webauthn.type==="create"?qv(e.webauthn.credential_response):Iv(e.webauthn.credential_response)})}:{code:e.code}),{data:m,error:p}=await ae(this.fetch,"POST",`${this.url}/factors/${e.factorId}/verify`,{body:d,headers:this.headers,jwt:(l=u==null?void 0:u.session)===null||l===void 0?void 0:l.access_token});return p?this._returnResult({data:null,error:p}):(await this._saveSession(Object.assign({expires_at:Math.round(Date.now()/1e3)+m.expires_in},m)),await this._notifyAllSubscribers("MFA_CHALLENGE_VERIFIED",m),this._returnResult({data:m,error:p}))})}catch(a){if(te(a))return this._returnResult({data:null,error:a});throw a}};return this.lock!=null?this._acquireLock(this.lockAcquireTimeout,s):s()}async _challenge(e){const s=async()=>{try{return await this._useSession(async a=>{var l;const{data:u,error:c}=a;if(c)return this._returnResult({data:null,error:c});const d=await ae(this.fetch,"POST",`${this.url}/factors/${e.factorId}/challenge`,{body:e,headers:this.headers,jwt:(l=u==null?void 0:u.session)===null||l===void 0?void 0:l.access_token});if(d.error)return d;const{data:m}=d;if(m.type!=="webauthn")return{data:m,error:null};switch(m.webauthn.type){case"create":return{data:Object.assign(Object.assign({},m),{webauthn:Object.assign(Object.assign({},m.webauthn),{credential_options:Object.assign(Object.assign({},m.webauthn.credential_options),{publicKey:Hv(m.webauthn.credential_options.publicKey)})})}),error:null};case"request":return{data:Object.assign(Object.assign({},m),{webauthn:Object.assign(Object.assign({},m.webauthn),{credential_options:Object.assign(Object.assign({},m.webauthn.credential_options),{publicKey:$v(m.webauthn.credential_options.publicKey)})})}),error:null}}})}catch(a){if(te(a))return this._returnResult({data:null,error:a});throw a}};return this.lock!=null?this._acquireLock(this.lockAcquireTimeout,s):s()}async _challengeAndVerify(e){const{data:s,error:a}=await this._challenge({factorId:e.factorId});return a?this._returnResult({data:null,error:a}):await this._verify({factorId:e.factorId,challengeId:s.id,code:e.code})}async _listFactors(){var e;const{data:{user:s},error:a}=await this.getUser();if(a)return{data:null,error:a};const l={all:[],phone:[],totp:[],webauthn:[]};for(const u of(e=s==null?void 0:s.factors)!==null&&e!==void 0?e:[])l.all.push(u),u.status==="verified"&&l[u.factor_type].push(u);return{data:l,error:null}}async _getAuthenticatorAssuranceLevel(e){var s,a,l,u;if(e)try{const{payload:x}=Rl(e);let S=null;x.aal&&(S=x.aal);let A=S;const{data:{user:k},error:C}=await this.getUser(e);if(C)return this._returnResult({data:null,error:C});((a=(s=k==null?void 0:k.factors)===null||s===void 0?void 0:s.filter(P=>P.status==="verified"))!==null&&a!==void 0?a:[]).length>0&&(A="aal2");const O=x.amr||[];return{data:{currentLevel:S,nextLevel:A,currentAuthenticationMethods:O},error:null}}catch(x){if(te(x))return this._returnResult({data:null,error:x});throw x}const{data:{session:c},error:d}=await this.getSession();if(d)return this._returnResult({data:null,error:d});if(!c)return{data:{currentLevel:null,nextLevel:null,currentAuthenticationMethods:[]},error:null};const{payload:m}=Rl(c.access_token);let p=null;m.aal&&(p=m.aal);let g=p;((u=(l=c.user.factors)===null||l===void 0?void 0:l.filter(x=>x.status==="verified"))!==null&&u!==void 0?u:[]).length>0&&(g="aal2");const w=m.amr||[];return{data:{currentLevel:p,nextLevel:g,currentAuthenticationMethods:w},error:null}}async _getAuthorizationDetails(e){try{return await this._useSession(async s=>{const{data:{session:a},error:l}=s;return l?this._returnResult({data:null,error:l}):a?await ae(this.fetch,"GET",`${this.url}/oauth/authorizations/${e}`,{headers:this.headers,jwt:a.access_token,xform:u=>({data:u,error:null})}):this._returnResult({data:null,error:new ht})})}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}async _approveAuthorization(e,s){try{return await this._useSession(async a=>{const{data:{session:l},error:u}=a;if(u)return this._returnResult({data:null,error:u});if(!l)return this._returnResult({data:null,error:new ht});const c=await ae(this.fetch,"POST",`${this.url}/oauth/authorizations/${e}/consent`,{headers:this.headers,jwt:l.access_token,body:{action:"approve"},xform:d=>({data:d,error:null})});return c.data&&c.data.redirect_url&&xt()&&!(s!=null&&s.skipBrowserRedirect)&&window.location.assign(c.data.redirect_url),c})}catch(a){if(te(a))return this._returnResult({data:null,error:a});throw a}}async _denyAuthorization(e,s){try{return await this._useSession(async a=>{const{data:{session:l},error:u}=a;if(u)return this._returnResult({data:null,error:u});if(!l)return this._returnResult({data:null,error:new ht});const c=await ae(this.fetch,"POST",`${this.url}/oauth/authorizations/${e}/consent`,{headers:this.headers,jwt:l.access_token,body:{action:"deny"},xform:d=>({data:d,error:null})});return c.data&&c.data.redirect_url&&xt()&&!(s!=null&&s.skipBrowserRedirect)&&window.location.assign(c.data.redirect_url),c})}catch(a){if(te(a))return this._returnResult({data:null,error:a});throw a}}async _listOAuthGrants(){try{return await this._useSession(async e=>{const{data:{session:s},error:a}=e;return a?this._returnResult({data:null,error:a}):s?await ae(this.fetch,"GET",`${this.url}/user/oauth/grants`,{headers:this.headers,jwt:s.access_token,xform:l=>({data:l,error:null})}):this._returnResult({data:null,error:new ht})})}catch(e){if(te(e))return this._returnResult({data:null,error:e});throw e}}async _revokeOAuthGrant(e){try{return await this._useSession(async s=>{const{data:{session:a},error:l}=s;return l?this._returnResult({data:null,error:l}):a?(await ae(this.fetch,"DELETE",`${this.url}/user/oauth/grants`,{headers:this.headers,jwt:a.access_token,query:{client_id:e.clientId},noResolveJson:!0}),{data:{},error:null}):this._returnResult({data:null,error:new ht})})}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}async fetchJwk(e,s={keys:[]}){let a=s.keys.find(d=>d.kid===e);if(a)return a;const l=Date.now();if(a=this.jwks.keys.find(d=>d.kid===e),a&&this.jwks_cached_at+qk>l)return a;const{data:u,error:c}=await ae(this.fetch,"GET",`${this.url}/.well-known/jwks.json`,{headers:this.headers});if(c)throw c;return!u.keys||u.keys.length===0||(this.jwks=u,this.jwks_cached_at=l,a=u.keys.find(d=>d.kid===e),!a)?null:a}async getClaims(e,s={}){try{let a=e;if(!a){const{data:x,error:S}=await this.getSession();if(S||!x.session)return this._returnResult({data:null,error:S});a=x.session.access_token}const{header:l,payload:u,signature:c,raw:{header:d,payload:m}}=Rl(a);if(!(s!=null&&s.allowExpired))try{dj(u.exp)}catch(x){throw new Ql(x instanceof Error?x.message:"JWT validation failed")}const p=!l.alg||l.alg.startsWith("HS")||!l.kid||!("crypto"in globalThis&&"subtle"in globalThis.crypto)?null:await this.fetchJwk(l.kid,s!=null&&s.keys?{keys:s.keys}:s==null?void 0:s.jwks);if(!p){const{error:x}=await this.getUser(a);if(x)throw x;return{data:{claims:u,header:l,signature:c},error:null}}const g=fj(l.alg),v=await crypto.subtle.importKey("jwk",p,g,!0,["verify"]);if(!await crypto.subtle.verify(g,v,c,Wk(`${d}.${m}`)))throw new Ql("Invalid JWT signature");return{data:{claims:u,header:l,signature:c},error:null}}catch(a){if(te(a))return this._returnResult({data:null,error:a});throw a}}async signInWithPasskey(e){var s,a,l;pn(this.experimental);try{if(!eu())return this._returnResult({data:null,error:new vn("Browser does not support WebAuthn",null)});const{data:u,error:c}=await this._startPasskeyAuthentication({options:{captchaToken:(s=e==null?void 0:e.options)===null||s===void 0?void 0:s.captchaToken}});if(c||!u)return this._returnResult({data:null,error:c});const d=$v(u.options),m=(l=(a=e==null?void 0:e.options)===null||a===void 0?void 0:a.signal)!==null&&l!==void 0?l:Pd.createNewAbortSignal(),{data:p,error:g}=await h_({publicKey:d,signal:m});if(g||!p)return this._returnResult({data:null,error:g??new vn("WebAuthn ceremony failed",null)});const v=Iv(p);return this._verifyPasskeyAuthentication({challengeId:u.challenge_id,credential:v})}catch(u){if(te(u))return this._returnResult({data:null,error:u});throw u}}async registerPasskey(e){var s,a;pn(this.experimental);try{if(!eu())return this._returnResult({data:null,error:new vn("Browser does not support WebAuthn",null)});const{data:l,error:u}=await this._startPasskeyRegistration();if(u||!l)return this._returnResult({data:null,error:u});const c=Hv(l.options),d=(a=(s=e==null?void 0:e.options)===null||s===void 0?void 0:s.signal)!==null&&a!==void 0?a:Pd.createNewAbortSignal(),{data:m,error:p}=await c_({publicKey:c,signal:d});if(p||!m)return this._returnResult({data:null,error:p??new vn("WebAuthn ceremony failed",null)});const g=qv(m);return this._verifyPasskeyRegistration({challengeId:l.challenge_id,credential:g})}catch(l){if(te(l))return this._returnResult({data:null,error:l});throw l}}async _startPasskeyRegistration(){pn(this.experimental);try{return await this._useSession(async e=>{const{data:{session:s},error:a}=e;if(a)return this._returnResult({data:null,error:a});if(!s)return this._returnResult({data:null,error:new ht});const{data:l,error:u}=await ae(this.fetch,"POST",`${this.url}/passkeys/registration/options`,{headers:this.headers,jwt:s.access_token,body:{}});return u?this._returnResult({data:null,error:u}):this._returnResult({data:l,error:null})})}catch(e){if(te(e))return this._returnResult({data:null,error:e});throw e}}async _verifyPasskeyRegistration(e){pn(this.experimental);try{return await this._useSession(async s=>{const{data:{session:a},error:l}=s;if(l)return this._returnResult({data:null,error:l});if(!a)return this._returnResult({data:null,error:new ht});const{data:u,error:c}=await ae(this.fetch,"POST",`${this.url}/passkeys/registration/verify`,{headers:this.headers,jwt:a.access_token,body:{challenge_id:e.challengeId,credential:e.credential}});return c?this._returnResult({data:null,error:c}):this._returnResult({data:u,error:null})})}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}async _startPasskeyAuthentication(e){var s;pn(this.experimental);try{const{data:a,error:l}=await ae(this.fetch,"POST",`${this.url}/passkeys/authentication/options`,{headers:this.headers,body:{gotrue_meta_security:{captcha_token:(s=e==null?void 0:e.options)===null||s===void 0?void 0:s.captchaToken}}});return l?this._returnResult({data:null,error:l}):this._returnResult({data:a,error:null})}catch(a){if(te(a))return this._returnResult({data:null,error:a});throw a}}async _verifyPasskeyAuthentication(e){pn(this.experimental);try{const{data:s,error:a}=await ae(this.fetch,"POST",`${this.url}/passkeys/authentication/verify`,{headers:this.headers,body:{challenge_id:e.challengeId,credential:e.credential},xform:rn});return a?this._returnResult({data:null,error:a}):(s.session&&(await this._saveSession(s.session),await this._notifyAllSubscribers("SIGNED_IN",s.session)),this._returnResult({data:s,error:null}))}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}async _listPasskeys(){pn(this.experimental);try{return await this._useSession(async e=>{const{data:{session:s},error:a}=e;if(a)return this._returnResult({data:null,error:a});if(!s)return this._returnResult({data:null,error:new ht});const{data:l,error:u}=await ae(this.fetch,"GET",`${this.url}/passkeys`,{headers:this.headers,jwt:s.access_token,xform:c=>({data:c,error:null})});return u?this._returnResult({data:null,error:u}):this._returnResult({data:l,error:null})})}catch(e){if(te(e))return this._returnResult({data:null,error:e});throw e}}async _updatePasskey(e){pn(this.experimental);try{return await this._useSession(async s=>{const{data:{session:a},error:l}=s;if(l)return this._returnResult({data:null,error:l});if(!a)return this._returnResult({data:null,error:new ht});const{data:u,error:c}=await ae(this.fetch,"PATCH",`${this.url}/passkeys/${e.passkeyId}`,{headers:this.headers,jwt:a.access_token,body:{friendly_name:e.friendlyName}});return c?this._returnResult({data:null,error:c}):this._returnResult({data:u,error:null})})}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}async _deletePasskey(e){pn(this.experimental);try{return await this._useSession(async s=>{const{data:{session:a},error:l}=s;if(l)return this._returnResult({data:null,error:l});if(!a)return this._returnResult({data:null,error:new ht});const{error:u}=await ae(this.fetch,"DELETE",`${this.url}/passkeys/${e.passkeyId}`,{headers:this.headers,jwt:a.access_token,noResolveJson:!0});return u?this._returnResult({data:null,error:u}):this._returnResult({data:null,error:null})})}catch(s){if(te(s))return this._returnResult({data:null,error:s});throw s}}}Hr.nextInstanceID={};const Lj=Hr,zj="2.110.2";let _r="",nu;if(typeof Deno<"u"){var ed;_r="deno",nu=(ed=Deno.version)===null||ed===void 0?void 0:ed.deno}else if(typeof document<"u")_r="web";else if(typeof navigator<"u"&&navigator.product==="ReactNative")_r="react-native";else{var td;_r="node",nu=typeof process<"u"?(td=process.version)===null||td===void 0?void 0:td.replace(/^v/,""):void 0}const d_=[`runtime=${_r}`];nu&&d_.push(`runtime-version=${nu}`);const Vj={"X-Client-Info":`supabase-js/${zj}; ${d_.join("; ")}`},Pj={headers:Vj},Hj={schema:"public"},$j={autoRefreshToken:!0,persistSession:!0,detectSessionInUrl:!0,flowType:"implicit"},qj={},Ij={enabled:!1,respectSamplingDecision:!0};function Gj(n,e,s,a){function l(u){return u instanceof s?u:new s(function(c){c(u)})}return new(s||(s=Promise))(function(u,c){function d(g){try{p(a.next(g))}catch(v){c(v)}}function m(g){try{p(a.throw(g))}catch(v){c(v)}}function p(g){g.done?u(g.value):l(g.value).then(d,m)}p((a=a.apply(n,[])).next())})}let nd=null;const Kj="@opentelemetry/api";function Fj(){return nd===null&&(nd=import(Kj).catch(()=>null)),nd}function Yj(){return Gj(this,void 0,void 0,function*(){try{const n=yield Fj();if(!n||!n.propagation||!n.context)return null;const e={};n.propagation.inject(n.context.active(),e);const s=e.traceparent;return s?{traceparent:s,tracestate:e.tracestate,baggage:e.baggage}:null}catch{return null}})}function Xj(n){if(!n||typeof n!="string")return null;const e=n.split("-");if(e.length!==4)return null;const[s,a,l,u]=e;if(s.length!==2||a.length!==32||l.length!==16||u.length!==2)return null;const c=/^[0-9a-f]+$/i;return!c.test(s)||!c.test(a)||!c.test(l)||!c.test(u)||a==="00000000000000000000000000000000"||l==="0000000000000000"?null:{version:s,traceId:a,parentId:l,traceFlags:u,isSampled:(parseInt(u,16)&1)===1}}function Jj(n,e){if(!n||!e||e.length===0)return!1;let s;if(n instanceof URL)s=n;else try{s=new URL(n)}catch{return!1}for(const a of e)try{if(typeof a=="string"){if(Qj(s.hostname,a))return!0}else if(a instanceof RegExp){if(a.test(s.hostname))return!0}else if(typeof a=="function"&&a(s))return!0}catch{continue}return!1}function Qj(n,e){if(e===n)return!0;if(e.startsWith("*.")){const s=e.slice(2);if(n.endsWith(s)&&(n===s||n.endsWith("."+s)))return!0}return!1}function Zj(n){const e=[];try{const s=new URL(n);e.push(s.hostname)}catch{}return e.push("*.supabase.co","*.supabase.in"),e.push("localhost","127.0.0.1","[::1]"),e}function $r(n){"@babel/helpers - typeof";return $r=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},$r(n)}function Wj(n,e){if($r(n)!="object"||!n)return n;var s=n[Symbol.toPrimitive];if(s!==void 0){var a=s.call(n,e);if($r(a)!="object")return a;throw new TypeError("@@toPrimitive must return a primitive value.")}return(e==="string"?String:Number)(n)}function eO(n){var e=Wj(n,"string");return $r(e)=="symbol"?e:e+""}function tO(n,e,s){return(e=eO(e))in n?Object.defineProperty(n,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):n[e]=s,n}function Gv(n,e){var s=Object.keys(n);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(n);e&&(a=a.filter(function(l){return Object.getOwnPropertyDescriptor(n,l).enumerable})),s.push.apply(s,a)}return s}function Je(n){for(var e=1;en?(...e)=>n(...e):(...e)=>fetch(...e),iO=()=>Headers,sO=(n,e,s,a,l)=>{const u=nO(a),c=iO(),d=(l==null?void 0:l.enabled)===!0,m=(l==null?void 0:l.respectSamplingDecision)!==!1,p=d?Zj(e):null;return async(g,v)=>{var w;const x=(w=await s())!==null&&w!==void 0?w:n;let S=new c(v==null?void 0:v.headers);if(S.has("apikey")||S.set("apikey",n),S.has("Authorization")||S.set("Authorization",`Bearer ${x}`),p){const A=await aO(g,p,m);A&&(A.traceparent&&!S.has("traceparent")&&S.set("traceparent",A.traceparent),A.tracestate&&!S.has("tracestate")&&S.set("tracestate",A.tracestate),A.baggage&&!S.has("baggage")&&S.set("baggage",A.baggage))}return u(g,Je(Je({},v),{},{headers:S}))}};async function aO(n,e,s){if(!Jj(typeof n=="string"||n instanceof URL?n:n.url,e))return null;const a=await Yj();if(!a||!a.traceparent)return null;if(s){const l=Xj(a.traceparent);if(l&&!l.isSampled)return null}return a}function Kv(n){return typeof n=="boolean"?{enabled:n}:n}function rO(n){return n.endsWith("/")?n:n+"/"}function oO(n,e){var s,a,l,u,c,d;const{db:m,auth:p,realtime:g,global:v}=n,{db:w,auth:x,realtime:S,global:A}=e,k=Kv(n.tracePropagation),C=Kv(e.tracePropagation),N={db:Je(Je({},w),m),auth:Je(Je({},x),p),realtime:Je(Je({},S),g),storage:{},global:Je(Je(Je({},A),v),{},{headers:Je(Je({},(s=A==null?void 0:A.headers)!==null&&s!==void 0?s:{}),(a=v==null?void 0:v.headers)!==null&&a!==void 0?a:{})}),tracePropagation:{enabled:(l=(u=k==null?void 0:k.enabled)!==null&&u!==void 0?u:C==null?void 0:C.enabled)!==null&&l!==void 0?l:!1,respectSamplingDecision:(c=(d=k==null?void 0:k.respectSamplingDecision)!==null&&d!==void 0?d:C==null?void 0:C.respectSamplingDecision)!==null&&c!==void 0?c:!0},accessToken:async()=>""};return n.accessToken?N.accessToken=n.accessToken:delete N.accessToken,N}function lO(n){const e=n==null?void 0:n.trim();if(!e)throw new Error("supabaseUrl is required.");if(!e.match(/^https?:\/\//i))throw new Error("Invalid supabaseUrl: Must be a valid HTTP or HTTPS URL.");try{return new URL(rO(e))}catch{throw Error("Invalid supabaseUrl: Provided URL is malformed.")}}var uO=class extends Lj{constructor(n){super(n)}},cO=class{constructor(n,e,s){var a,l;this.supabaseUrl=n,this.supabaseKey=e;const u=lO(n);if(!e)throw new Error("supabaseKey is required.");this.realtimeUrl=new URL("realtime/v1",u),this.realtimeUrl.protocol=this.realtimeUrl.protocol.replace("http","ws"),this.authUrl=new URL("auth/v1",u),this.storageUrl=new URL("storage/v1",u),this.functionsUrl=new URL("functions/v1",u);const c=`sb-${u.hostname.split(".")[0]}-auth-token`,d={db:Hj,realtime:qj,auth:Je(Je({},$j),{},{storageKey:c}),global:Pj,tracePropagation:Ij},m=oO(s??{},d);if(this.settings=m,this.storageKey=(a=m.auth.storageKey)!==null&&a!==void 0?a:"",this.headers=(l=m.global.headers)!==null&&l!==void 0?l:{},m.accessToken)this.accessToken=m.accessToken,this.auth=new Proxy({},{get:(g,v)=>{throw new Error(`@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.${String(v)} is not possible`)}});else{var p;this.auth=this._initSupabaseAuthClient((p=m.auth)!==null&&p!==void 0?p:{},this.headers,m.global.fetch)}this.fetch=sO(e,n,this._getAccessToken.bind(this),m.global.fetch,m.tracePropagation),this.realtime=this._initRealtimeClient(Je({headers:this.headers,accessToken:this._getAccessToken.bind(this),fetch:this.fetch},m.realtime)),this.accessToken&&Promise.resolve(this.accessToken()).then(g=>this.realtime.setAuth(g)).catch(g=>console.warn("Failed to set initial Realtime auth token:",g)),this.rest=new bC(new URL("rest/v1",u).href,{headers:this.headers,schema:m.db.schema,fetch:this.fetch,timeout:m.db.timeout,urlLengthLimit:m.db.urlLengthLimit}),this.storage=new Lk(this.storageUrl.href,this.headers,this.fetch,s==null?void 0:s.storage),m.accessToken||this._listenForAuthEvents()}get functions(){return new cC(this.functionsUrl.href,{headers:this.headers,customFetch:this.fetch})}from(n){return this.rest.from(n)}schema(n){return this.rest.schema(n)}rpc(n,e={},s={head:!1,get:!1,count:void 0}){return this.rest.rpc(n,e,s)}channel(n,e={config:{}}){return this.realtime.channel(n,e)}getChannels(){return this.realtime.getChannels()}removeChannel(n){return this.realtime.removeChannel(n)}removeAllChannels(){return this.realtime.removeAllChannels()}async _getAccessToken(){var n=this,e,s;if(n.accessToken)return await n.accessToken();const{data:a}=await n.auth.getSession();return(e=(s=a.session)===null||s===void 0?void 0:s.access_token)!==null&&e!==void 0?e:n.supabaseKey}_initSupabaseAuthClient({autoRefreshToken:n,persistSession:e,detectSessionInUrl:s,storage:a,userStorage:l,storageKey:u,flowType:c,lock:d,debug:m,throwOnError:p,experimental:g,lockAcquireTimeout:v,skipAutoInitialize:w},x,S){const A={Authorization:`Bearer ${this.supabaseKey}`,apikey:`${this.supabaseKey}`};return new uO({url:this.authUrl.href,headers:Je(Je({},A),x),storageKey:u,autoRefreshToken:n,persistSession:e,detectSessionInUrl:s,storage:a,userStorage:l,flowType:c,lock:d,debug:m,throwOnError:p,experimental:g,fetch:S,lockAcquireTimeout:v,skipAutoInitialize:w,hasCustomAuthorizationHeader:Object.keys(this.headers).some(k=>k.toLowerCase()==="authorization")})}_initRealtimeClient(n){return new ok(this.realtimeUrl.href,Je(Je({},n),{},{params:Je(Je({},{apikey:this.supabaseKey}),n==null?void 0:n.params)}))}_listenForAuthEvents(){return this.auth.onAuthStateChange((n,e)=>{this._handleTokenChanged(n,"CLIENT",e==null?void 0:e.access_token)})}_handleTokenChanged(n,e,s){(n==="TOKEN_REFRESHED"||n==="SIGNED_IN")&&this.changedAccessToken!==s?(this.changedAccessToken=s,this.realtime.setAuth(s)):n==="SIGNED_OUT"&&(this.realtime.setAuth(),e=="STORAGE"&&this.auth.signOut(),this.changedAccessToken=void 0)}};const hO=(n,e,s)=>new cO(n,e,s);function dO(){if(typeof window<"u")return!1;const n=globalThis.process;if(!n)return!1;const e=n.version;if(e==null)return!1;const s=e.match(/^v(\d+)\./);return s?parseInt(s[1],10)<=20:!1}dO()&&console.warn("⚠️ Node.js 20 and below are deprecated and will no longer be supported in future versions of @supabase/supabase-js. Please upgrade to Node.js 22 or later. For more information, visit: https://github.com/orgs/supabase/discussions/45715");const fO="https://dbit.digitalcompass.agency",f_="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE",kr=!!f_,Ut=kr?hO(fO,f_,{auth:{persistSession:!0,autoRefreshToken:!0,detectSessionInUrl:!0}}):null;function mO(){return new URL("/cruce-cuentas/",window.location.origin).toString()}const Cl=10;function pO(n){return n==="TT"?"https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-tt-historicos":"https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-historicos"}function gO(n){return n==="TT"?"https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-tt-marcar-resuelto":"https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-marcar-resuelto"}function gn(n){const e=Number(n);return Number.isFinite(e)?e:0}function yO(n,e){if(!n)return"";const s=new Date(String(n));return Number.isNaN(s.getTime())?String(n).slice(0,10):s.toLocaleDateString(e,{year:"numeric",month:"2-digit",day:"2-digit"})}function Hd(n,e){if(!n)return"";const s=new Date(String(n));return Number.isNaN(s.getTime())?String(n):s.toLocaleString(e,{dateStyle:"short",timeStyle:"short"})}function vO(n){return String(n??"").toLowerCase().includes("resuelto")?"Resuelto":"Pendiente revisión"}function Fv(n,e){const s=n,a=!Array.isArray(s)&&s?s:{};return(Array.isArray(s)?s:Array.isArray(a.reports)?a.reports:[]).map((u,c)=>{const d=u,m=gn(d.discrepancias??d.discrepancies),p=gn(d.bancoSinNomina??d.banco_sin_nomina??d.bankWithoutPayroll),g=gn(d.bancoSinBamboo??d.banco_sin_bamboo??d.bankWithoutBamboo),v=gn(d.nominaSinCuenta??d.nomina_sin_cuenta??d.payrollWithoutAccount),w=gn(d.diferenciasNombreBanco??d.diferencias_nombre_banco??d.nameDifferences),x=m+p+g+v+w,S=String(d.country??d.pais??d.country_code??e.code).toUpperCase(),A=Array.isArray(d.archivosBanco)?d.archivosBanco.map(String):Array.isArray(d.bank_file_names)?d.bank_file_names.map(String):[],k=d.fechaEjecucion??d.ejecutado_en??d.created_at;return{id:String(d.id??`reporte-${c+1}`),country:S==="TT"?"TT":"GT",date:yO(d.fecha??d.period_end??k,e.locale),executionDate:Hd(k,e.locale),period:String(d.periodo??d.period_label??d.period??"Período no especificado"),payrollFile:String(d.archivoNomina??d.payroll_file_name??d.payrollFile??""),bankFiles:A,matches:gn(d.coincidencias??d.matches),discrepancies:m,bankWithoutPayroll:p,bankWithoutBamboo:g,payrollWithoutAccount:v,nameDifferences:w,totalPending:x,totalPayroll:gn(d.totalNomina??d.total_nomina),totalBank:gn(d.totalBanco??d.total_banco),totalDifference:gn(d.diferenciaTotal??d.diferencia_total),status:vO(d.estado??d.status??d.estadoLabel),reportUrl:d.reportUrl||d.report_url?String(d.reportUrl??d.report_url):void 0,spreadsheetId:d.spreadsheetId||d.spreadsheet_id?String(d.spreadsheetId??d.spreadsheet_id):void 0,executedBy:d.ejecutadoPorNombre||d.ejecutado_por_nombre?String(d.ejecutadoPorNombre??d.ejecutado_por_nombre):void 0,executedByEmail:d.ejecutadoPorEmail||d.ejecutado_por_email?String(d.ejecutadoPorEmail??d.ejecutado_por_email):void 0,resolvedBy:d.resueltoPorNombre||d.resuelto_por_nombre?String(d.resueltoPorNombre??d.resuelto_por_nombre):void 0,resolvedByEmail:d.resueltoPorEmail||d.resuelto_por_email?String(d.resueltoPorEmail??d.resuelto_por_email):void 0,resolvedAt:Hd(d.resueltoEn??d.resuelto_en,e.locale),resolutionComment:d.comentarioResolucion||d.comentario_resolucion?String(d.comentarioResolucion??d.comentario_resolucion):void 0,_sortTime:new Date(String(k??d.period_end??"")).getTime()||0}}).filter(u=>u.country===e.code).sort((u,c)=>c._sortTime-u._sortTime).map(({_sortTime:u,...c})=>c)}function bO(n){return n==="Resuelto"?"bg-blue-50 text-blue-800 border-blue-200":"bg-amber-50 text-amber-800 border-amber-200"}function wO(n){return n==="Resuelto"?"RESUELTO":"PENDIENTE REVISIÓN"}function _O({onShowToast:n,onOpenSidebar:e,operatorName:s,operatorEmail:a,country:l}){const[u,c]=V.useState(""),[d,m]=V.useState("todos"),[p,g]=V.useState([]),[v,w]=V.useState(1),[x,S]=V.useState(!1),[A,k]=V.useState(!1),[C,N]=V.useState(""),[O,P]=V.useState(null),[J,X]=V.useState(""),[G,Z]=V.useState(0),[ee,ne]=V.useState(0),le=V.useRef(0),de=V.useRef(null),je=Math.max(1,Math.ceil(ee/Cl)),Re=p;V.useEffect(()=>{w(1)},[u,d,l.code]),V.useEffect(()=>{v>je&&w(je)},[v,je]);const ke=async()=>{var B;const I=++le.current;(B=de.current)==null||B.abort();const oe=new AbortController;de.current=oe;const E=()=>le.current===I&&!oe.signal.aborted;S(!0),N("");try{if(Ut){const wt=d==="pendiente"?"pendiente_revision":d==="resuelto"?"resuelto":null,{data:Yt,error:oi}=await Ut.rpc("cruce_cuentas_get_historicos",{p_country:l.code,p_page:v,p_page_size:Cl,p_search:u.trim()||null,p_status:wt});if(!E())return;if(!oi&&Yt){const dt=Array.isArray(Yt)?Yt[0]:Yt,Pi=Fv(dt,l),F=gn(dt==null?void 0:dt.total),ge=gn((dt==null?void 0:dt.filtered_total)??F);g(Pi),Z(F),ne(ge);return}}if(!E())return;const K=pO(l.code);if(!K)throw new Error(`No está configurado el origen histórico de ${l.name}.`);const W=new URL(K);W.searchParams.set("country",l.code);const he=await fetch(W.toString(),{method:"GET",signal:oe.signal}),ue=await he.json().catch(()=>null);if(!E())return;if(!he.ok||(ue==null?void 0:ue.ok)===!1)throw new Error("No se pudieron cargar los reportes históricos.");const _e=Fv(ue,l),qe=u.trim().toLowerCase(),He=_e.filter(wt=>{const Yt=d==="todos"||d==="pendiente"&&wt.status==="Pendiente revisión"||d==="resuelto"&&wt.status==="Resuelto",oi=!qe||wt.period.toLowerCase().includes(qe)||wt.payrollFile.toLowerCase().includes(qe)||wt.id.toLowerCase().includes(qe)||(wt.resolvedBy??"").toLowerCase().includes(qe)||(wt.executedBy??"").toLowerCase().includes(qe);return Yt&&oi});if(!E())return;const _n=(v-1)*Cl;g(He.slice(_n,_n+Cl)),Z(_e.length),ne(He.length)}catch(K){if(!E()||K instanceof DOMException&&K.name==="AbortError")return;g([]),Z(0),ne(0),N(K instanceof Error?K.message:"No se pudieron cargar los reportes históricos. Intenta nuevamente."),n("No se pudieron cargar los reportes históricos.","error")}finally{E()&&(S(!1),de.current===oe&&(de.current=null))}};V.useEffect(()=>{const I=window.setTimeout(()=>{ke()},u.trim()?300:0);return()=>{var oe;window.clearTimeout(I),le.current+=1,(oe=de.current)==null||oe.abort(),de.current=null}},[l.code,v,u,d]);const L=I=>{if(I.reportUrl){window.open(I.reportUrl,"_blank","noopener,noreferrer");return}n("Este reporte aún no tiene un Google Sheet disponible.","info")},Q=I=>{if(I.status==="Resuelto"){n("Este reporte ya fue marcado como resuelto.","info");return}P(I),X("")},Y=async()=>{if(!O)return;const I=J.trim();if(I.length<10){n("Debes escribir un comentario de resolución más descriptivo.","error");return}const oe=gO(l.code);if(!oe){n(`La resolución de reportes para ${l.name} estará disponible próximamente.`,"info");return}k(!0);try{const E=await fetch(oe,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({reportId:O.id,comentarioResolucion:I,resueltoPorNombre:s,resueltoPorEmail:a,country:l.code,countryName:l.name})}),B=await E.json().catch(()=>null);if(!E.ok||(B==null?void 0:B.ok)===!1){const he=Array.isArray(B==null?void 0:B.errors)?B.errors.join(` -`):(B==null?void 0:B.message)||"No se pudo marcar el reporte como resuelto.";throw new Error(he)}const K=B.report??{},W=Hd(K.resueltoEn??new Date().toISOString(),l.locale);g(he=>he.map(ue=>ue.id===O.id?{...ue,status:"Resuelto",resolvedBy:String(K.resueltoPorNombre??s),resolvedByEmail:String(K.resueltoPorEmail??a),resolvedAt:W,resolutionComment:String(K.comentarioResolucion??I)}:ue)),P(null),X(""),n("Reporte marcado como resuelto correctamente.","success")}catch(E){const B=E instanceof Error?E.message:"No se pudo marcar el reporte como resuelto.";n(B,"error")}finally{k(!1)}};return b.jsxs("div",{className:"flex-1 flex flex-col min-h-screen bg-neutral-50 text-neutral-800 font-sans",children:[b.jsxs("header",{className:"bg-white border-b-4 border-[#6CC24A] flex justify-between items-center w-full px-4 sm:px-8 h-[84px] sticky top-0 z-40 shadow-sm gap-x-4",children:[b.jsxs("div",{className:"flex items-center gap-2 sm:gap-3 min-w-0",children:[b.jsx("button",{onClick:e,className:"md:hidden p-2 -ml-2 text-neutral-600 hover:text-neutral-800 hover:bg-neutral-100 transition-colors flex-shrink-0","aria-label":"Abrir menú",children:b.jsx(Pw,{className:"w-6 h-6"})}),b.jsx("h1",{className:"text-xs sm:text-sm md:text-lg lg:text-2xl font-black text-[#4F758B] tracking-tight uppercase flex items-center gap-1.5 sm:gap-2 min-w-0",children:b.jsxs("span",{className:"truncate",children:["Reportes Históricos - ",l.name]})})]}),b.jsxs("div",{className:"flex items-center gap-3 flex-shrink-0",children:[b.jsx("div",{className:"hidden sm:flex items-center border border-neutral-200 bg-white px-2.5 py-2 shadow-sm","aria-label":`Bandera de ${l.name}`,children:b.jsx("img",{src:l.flag,alt:`Bandera de ${l.name}`,className:"h-5 sm:h-6 w-auto object-contain"})}),b.jsxs("button",{onClick:ke,disabled:x,className:"hidden sm:inline-flex items-center gap-2 border border-[#4F758B]/20 text-[#4F758B] px-3 py-2 text-xs font-bold hover:border-[#4F758B]/50 disabled:opacity-50",children:[b.jsx(DR,{className:`w-4 h-4 ${x?"animate-spin":""}`}),"Actualizar"]})]})]}),b.jsxs("main",{className:"flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 py-8 flex flex-col gap-6",children:[b.jsxs("div",{className:"bg-white border border-[#D0D0D0] p-4 flex flex-col lg:flex-row items-center justify-between gap-4 shadow-sm",children:[b.jsxs("div",{className:"relative w-full lg:max-w-md",children:[b.jsx(Hw,{className:"absolute left-3.5 top-1/2 transform -translate-y-1/2 text-neutral-400 w-4 h-4"}),b.jsx("input",{type:"text",placeholder:"Buscar por período, reporte, archivo o resolutor...",value:u,onChange:I=>c(I.target.value),className:"w-full pl-10 pr-4 py-2 border border-[#D0D0D0] text-xs font-medium placeholder-neutral-400 focus:outline-none focus:border-[#4F758B] transition-colors"})]}),b.jsxs("div",{className:"flex flex-col sm:flex-row sm:items-center gap-3 w-full lg:w-auto",children:[b.jsx("div",{className:"flex flex-wrap gap-2",children:[["todos","Todos"],["pendiente","Pendiente revisión"],["resuelto","Resuelto"]].map(([I,oe])=>b.jsx("button",{onClick:()=>m(I),className:`px-3 py-1.5 text-[10px] uppercase tracking-wider font-black border transition-colors ${d===I?"bg-[#4F758B] text-white border-[#4F758B]":"bg-white text-neutral-500 border-neutral-300 hover:border-[#4F758B] hover:text-[#4F758B]"}`,children:oe},I))}),b.jsxs("div",{className:"flex items-center gap-2 text-xs font-bold text-neutral-500",children:[b.jsx(vR,{className:"w-4 h-4 text-neutral-400"}),b.jsxs("span",{children:["Mostrando: ",ee," de ",G," reportes"]})]})]})]}),C&&b.jsxs("div",{className:"border border-red-200 bg-red-50 text-[#ba1a1a] px-4 py-3 text-xs font-bold flex items-center gap-2",children:[b.jsx(xf,{className:"w-4 h-4"}),C]}),b.jsxs("div",{className:"bg-white border border-[#D0D0D0] shadow-sm overflow-hidden",children:[b.jsx("div",{className:"overflow-x-auto",children:b.jsxs("table",{className:"w-full text-left border-collapse min-w-[960px] xl:min-w-full",children:[b.jsx("thead",{children:b.jsxs("tr",{className:"bg-[#4F758B] text-white text-xs font-bold uppercase tracking-wider",children:[b.jsx("th",{className:"py-3 px-3 xl:px-4",children:"Fecha"}),b.jsx("th",{className:"py-3 px-3 xl:px-4",children:"Período"}),b.jsx("th",{className:"py-3 px-3 xl:px-4",children:"Archivo de Nómina"}),b.jsx("th",{className:"py-3 px-3 xl:px-4 text-center",children:"Coincidencias"}),b.jsx("th",{className:"py-3 px-3 xl:px-4 text-center",children:"Banco sin Bamboo"}),b.jsx("th",{className:"py-3 px-3 xl:px-4 text-center",children:"Pendientes"}),b.jsx("th",{className:"py-3 px-3 xl:px-4 text-center",children:"Estado"}),b.jsx("th",{className:"py-3 px-3 xl:px-4",children:"Resuelto por"}),b.jsx("th",{className:"py-3 px-3 xl:px-4 text-center",children:"Acciones"})]})}),b.jsx("tbody",{className:"divide-y divide-neutral-200 text-xs",children:x?b.jsx("tr",{children:b.jsx("td",{colSpan:9,className:"py-12 text-center text-neutral-400 font-medium",children:b.jsxs("div",{className:"inline-flex items-center gap-2",children:[b.jsx(kd,{className:"w-4 h-4 animate-spin"}),"Cargando reportes históricos..."]})})}):Re.length>0?Re.map(I=>b.jsxs(bn.tr,{initial:{opacity:0},animate:{opacity:1},className:"hover:bg-neutral-50/70 transition-colors",children:[b.jsx("td",{className:"py-3 px-3 xl:px-4 font-medium text-neutral-700",children:b.jsxs("div",{className:"flex items-center gap-2",children:[b.jsx(nR,{className:"w-4 h-4 text-neutral-400"}),b.jsxs("div",{children:[b.jsx("span",{children:I.date}),I.executionDate&&b.jsx("p",{className:"text-[9px] text-neutral-400 mt-0.5",children:I.executionDate})]})]})}),b.jsx("td",{className:"py-3 px-3 xl:px-4 font-bold text-[#4F758B]",children:I.period}),b.jsx("td",{className:"py-3 px-3 xl:px-4 font-mono text-neutral-500 truncate max-w-[260px]",title:I.payrollFile,children:b.jsxs("div",{className:"flex items-center gap-1.5",children:[b.jsx(Ll,{className:"w-4 h-4 text-neutral-400 flex-shrink-0"}),b.jsx("span",{className:"truncate",children:I.payrollFile||"No especificado"})]})}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-center font-black text-emerald-800",children:I.matches}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-center",children:b.jsx("span",{className:`inline-block min-w-8 px-2.5 py-1 text-[10px] font-black border ${I.bankWithoutBamboo>0?"bg-orange-50 text-orange-800 border-orange-200":"bg-neutral-50 text-neutral-400 border-neutral-200"}`,children:I.bankWithoutBamboo})}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-center",children:b.jsx("span",{className:`inline-block px-2.5 py-1 text-[10px] font-bold border ${I.totalPending>0?"bg-red-50 text-[#ba1a1a] border-red-200":"bg-emerald-50 text-emerald-800 border-emerald-200"}`,children:I.totalPending===0?"Sin pendientes":`${I.totalPending} pendiente${I.totalPending>1?"s":""}`})}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-center",children:b.jsx("span",{className:`inline-block px-2.5 py-1 text-[9px] uppercase font-black tracking-wider border ${bO(I.status)}`,children:wO(I.status)})}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-neutral-600 max-w-[220px]",children:I.resolvedBy?b.jsxs("div",{title:I.resolutionComment||"",children:[b.jsx("p",{className:"font-bold text-neutral-700 truncate",children:I.resolvedBy}),b.jsx("p",{className:"text-[10px] text-neutral-400",children:I.resolvedAt})]}):b.jsx("span",{className:"text-neutral-400",children:"Pendiente"})}),b.jsx("td",{className:"py-3 px-3 xl:px-4 text-center",children:b.jsxs("div",{className:"flex justify-center gap-2",children:[b.jsxs("button",{onClick:()=>L(I),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",children:[b.jsx(mR,{className:"w-3.5 h-3.5"}),b.jsx("span",{children:"Ver"})]}),b.jsxs("button",{onClick:()=>Q(I),disabled:I.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",children:[b.jsx(_f,{className:"w-3.5 h-3.5"}),b.jsx("span",{children:I.status==="Resuelto"?"Resuelto":"Marcar resuelto"})]})]})})]},I.id)):b.jsx("tr",{children:b.jsx("td",{colSpan:9,className:"py-12 text-center text-neutral-400 font-medium",children:"No se encontraron reportes históricos."})})})]})}),b.jsxs("div",{className:"p-4 border-t border-neutral-200 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-xs text-neutral-500",children:[b.jsxs("span",{children:["Mostrando ",Re.length," de ",ee," reporte(s)."]}),b.jsxs("div",{className:"flex items-center gap-2",children:[b.jsx("button",{onClick:()=>w(I=>Math.max(1,I-1)),disabled:v===1,className:"px-3 py-1.5 border border-neutral-300 bg-white disabled:opacity-40 disabled:cursor-not-allowed hover:border-[#4F758B] font-bold",children:"Anterior"}),b.jsxs("span",{className:"font-bold text-neutral-700",children:["Página ",v," de ",je]}),b.jsx("button",{onClick:()=>w(I=>Math.min(je,I+1)),disabled:v===je,className:"px-3 py-1.5 border border-neutral-300 bg-white disabled:opacity-40 disabled:cursor-not-allowed hover:border-[#4F758B] font-bold",children:"Siguiente"})]})]})]})]}),b.jsx(cs,{children:O&&b.jsx(bn.div,{initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},className:"fixed inset-0 z-[80] flex items-center justify-center bg-black/45 px-4",children:b.jsxs(bn.div,{initial:{opacity:0,y:12},animate:{opacity:1,y:0},exit:{opacity:0,y:12},className:"w-full max-w-lg bg-white border border-[#D0D0D0] shadow-xl",children:[b.jsxs("div",{className:"border-b border-neutral-200 px-5 py-4 flex items-center justify-between",children:[b.jsxs("div",{children:[b.jsx("h2",{className:"text-base font-black text-[#4F758B] uppercase tracking-tight",children:"Marcar reporte como resuelto"}),b.jsx("p",{className:"text-xs text-neutral-500 mt-1",children:O.period})]}),b.jsx("button",{onClick:()=>P(null),disabled:A,className:"p-2 text-neutral-400 hover:text-neutral-700 disabled:opacity-50","aria-label":"Cerrar",children:b.jsx(Sf,{className:"w-4 h-4"})})]}),b.jsxs("div",{className:"p-5 space-y-4",children:[b.jsx("div",{className:"bg-amber-50 border border-amber-200 text-amber-800 p-3 text-xs leading-relaxed",children:"Antes de resolver, confirma que las diferencias fueron revisadas y explica cómo se cerró el caso."}),b.jsxs("label",{className:"block",children:[b.jsx("span",{className:"text-[10px] uppercase tracking-wider font-black text-neutral-500",children:"Comentario de resolución"}),b.jsx("textarea",{value:J,onChange:I=>X(I.target.value),placeholder:"Ejemplo: Se corrigió la cuenta bancaria en nómina y se validó contra el banco.",className:"mt-2 w-full min-h-[130px] border border-[#D0D0D0] px-3 py-2 text-sm focus:outline-none focus:border-[#4F758B]"})]})]}),b.jsxs("div",{className:"border-t border-neutral-200 px-5 py-4 flex justify-end gap-3",children:[b.jsx("button",{onClick:()=>P(null),disabled:A,className:"border border-neutral-300 bg-white px-4 py-2 text-xs font-bold text-neutral-600 hover:bg-neutral-50 disabled:opacity-50",children:"Cancelar"}),b.jsxs("button",{onClick:Y,disabled:A,className:"bg-[#4F758B] hover:bg-[#41677b] text-white px-4 py-2 text-xs font-black uppercase tracking-wider disabled:opacity-50 inline-flex items-center gap-2",children:[A&&b.jsx(kd,{className:"w-4 h-4 animate-spin"}),"Confirmar resolución"]})]})]})})})]})}function id({message:n,type:e,onClose:s}){V.useEffect(()=>{const u=setTimeout(()=>{s()},2e3);return()=>clearTimeout(u)},[s]);const a={success:"bg-[#6CC24A] text-white border-l-4 border-emerald-700",error:"bg-[#ba1a1a] text-white border-l-4 border-red-800",info:"bg-[#4F758B] text-white border-l-4 border-slate-700"},l={success:_f,error:rR,info:Vw}[e];return b.jsx("div",{className:"fixed bottom-6 right-6 z-50 max-w-sm w-full",children:b.jsxs(bn.div,{initial:{opacity:0,y:30,scale:.95},animate:{opacity:1,y:0,scale:1},exit:{opacity:0,y:20,scale:.95},className:`flex items-center gap-3 p-4 shadow-lg rounded-md border border-neutral-200/10 ${a[e]}`,children:[b.jsx("span",{className:"flex-shrink-0",children:b.jsx(l,{className:"w-5 h-5"})}),b.jsx("div",{className:"flex-1 font-medium text-sm leading-tight pr-2",children:n}),b.jsx("button",{onClick:s,className:"flex-shrink-0 hover:bg-white/20 p-1 rounded transition-colors text-white",children:b.jsx(Sf,{className:"w-4 h-4"})})]})})}function xO({onShowToast:n}){const[e,s]=V.useState(!1),a=async()=>{if(!kr||!Ut){n("El acceso con Google aún no está disponible. Contacta a IT.","error");return}s(!0);const{error:l}=await Ut.auth.signInWithOAuth({provider:"google",options:{redirectTo:mO(),queryParams:{access_type:"offline",prompt:"select_account"}}});l&&(s(!1),n("No se pudo iniciar sesión con Google. Intenta nuevamente.","error"))};return b.jsxs("div",{className:"min-h-screen w-full flex flex-col justify-between bg-[#F5F5F5] relative overflow-hidden font-sans",children:[b.jsx("div",{className:"h-1.5 w-full bg-[#4F758B]"}),b.jsx("div",{className:"flex-1 flex items-center justify-center p-4",children:b.jsxs(bn.div,{initial:{opacity:0,y:15},animate:{opacity:1,y:0},transition:{duration:.5,ease:"easeOut"},className:"w-full max-w-md bg-white border border-[#D0D0D0] p-8 shadow-xl flex flex-col items-center gap-6",children:[b.jsx("div",{className:"w-full flex justify-center py-2",children:b.jsx("img",{alt:"Logo GOMEZLEE MARKETING",className:"h-14 object-contain",src:"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png",referrerPolicy:"no-referrer"})}),b.jsx("div",{className:"text-center w-full",children:b.jsx("h1",{className:"text-2xl font-bold text-[#4F758B] tracking-tight",children:"Portal de Cruce de Cuentas GLM"})}),b.jsx("button",{onClick:a,disabled:e||!kr,className:"w-full relative overflow-hidden flex items-center justify-center gap-3 bg-[#6CC24A] hover:bg-[#5bb03c] disabled:bg-neutral-300 disabled:cursor-not-allowed text-white font-semibold py-3.5 px-6 transition-all duration-150 transform active:scale-[0.99] select-none text-sm tracking-wide",children:e?b.jsxs("div",{className:"flex items-center gap-2",children:[b.jsxs("svg",{className:"animate-spin h-5 w-5 text-white",fill:"none",viewBox:"0 0 24 24",children:[b.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),b.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),b.jsx("span",{children:"Redirigiendo a Google..."})]}):b.jsxs(b.Fragment,{children:[b.jsxs("svg",{className:"w-5 h-5 flex-shrink-0 fill-current text-white",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:[b.jsx("path",{d:"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"}),b.jsx("path",{d:"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"}),b.jsx("path",{d:"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"}),b.jsx("path",{d:"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"})]}),b.jsx("span",{children:"Continuar con Google"})]})}),!kr&&b.jsxs("div",{className:"w-full flex items-start gap-2 border border-amber-200 bg-amber-50 px-3 py-2 text-[11px] text-amber-800 leading-relaxed",children:[b.jsx(xf,{className:"w-4 h-4 mt-0.5 flex-shrink-0"}),b.jsx("span",{children:"El acceso con Google está pendiente de activación."})]})]})})]})}const SO="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png";function TO({onSelect:n}){return b.jsxs("div",{className:"min-h-screen bg-[#F5F5F5] flex flex-col font-sans",children:[b.jsx("div",{className:"h-1.5 bg-[#6CC24A]"}),b.jsx("main",{className:"flex-1 flex items-center justify-center px-4 py-10",children:b.jsxs(bn.section,{initial:{opacity:0,y:12},animate:{opacity:1,y:0},className:"w-full max-w-3xl bg-white border border-[#D0D0D0] shadow-xl p-6 sm:p-9",children:[b.jsx("img",{src:SO,alt:"GomezLee Marketing",className:"h-14 object-contain mb-7"}),b.jsxs("div",{className:"flex items-start gap-3 mb-7",children:[b.jsx("div",{className:"bg-[#4F758B]/10 p-2.5 rounded-full",children:b.jsx(zw,{className:"w-5 h-5 text-[#4F758B]"})}),b.jsxs("div",{children:[b.jsx("h1",{className:"text-xl sm:text-2xl font-black text-[#4F758B] uppercase tracking-tight",children:"Selecciona el país"}),b.jsx("p",{className:"text-sm text-neutral-500 mt-1",children:"El portal cargará las reglas, la moneda y los reportes correspondientes."})]})]}),b.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 gap-4",children:Mr.map(e=>b.jsxs("button",{type:"button",disabled:!e.enabled,onClick:()=>e.enabled&&n(e.code),className:`text-left border p-5 transition-all min-h-[150px] flex flex-col justify-between ${e.enabled?"border-[#4F758B]/30 hover:border-[#4F758B] hover:shadow-md bg-white":"border-neutral-200 bg-neutral-50 cursor-not-allowed opacity-70"}`,children:[b.jsxs("div",{className:"flex items-start justify-between gap-3",children:[b.jsx("img",{src:e.flag,alt:"",className:"w-12 h-8 sm:w-16 sm:h-10 object-cover shadow-sm border border-neutral-200","aria-hidden":"true"}),e.enabled?b.jsx(Q2,{className:"w-5 h-5 text-[#6CC24A]"}):b.jsx(TR,{className:"w-4 h-4 text-neutral-400"})]}),b.jsxs("div",{children:[b.jsx("p",{className:"font-black text-[#4F758B] uppercase tracking-wide",children:e.name}),b.jsx("p",{className:"text-xs text-neutral-500 mt-1",children:e.enabled?`Moneda ${e.currency}`:"Próximamente"})]})]},e.code))})]})})]})}const EO="cruce_cuentas_mi_acceso",Yv=2;function m_(n){return(n??"").trim().toLowerCase()}function AO(n){return Array.isArray(n)?Array.from(new Set(n.map(e=>String(e??"").trim().toUpperCase()).filter(Boolean))):[]}function RO(n){const e=Array.isArray(n)?n[0]:n;if(!e||typeof e!="object")throw new Error("Supabase no devolvió una respuesta válida al comprobar el acceso.");return{ok:e.ok===!0,authenticated:e.autenticado===!0,authorized:e.autorizado===!0,email:m_(typeof e.email=="string"?e.email:""),name:typeof e.nombre=="string"?e.nombre.trim():"",active:e.activo===!0,appAccess:e.acceso_app===!0,historyAccess:e.acceso_historicos===!0,receivesGeneratedReports:e.recibe_reportes_generados===!0,receivesResolvedEmails:e.recibe_correos_resuelto===!0,countries:AO(e.paises),reason:typeof e.motivo=="string"?e.motivo.trim():""}}function CO(n){return new Promise(e=>window.setTimeout(e,n))}async function kO(){if(!Ut)throw new Error("Supabase no está configurado.");let n=null;for(let s=1;s<=Yv;s+=1){const{data:a,error:l}=await Ut.rpc(EO);if(!l)return RO(a);n=l,s{if(typeof indexedDB>"u"){e(new Error("IndexedDB no está disponible en este navegador."));return}const s=indexedDB.open(jO,OO);s.onupgradeneeded=()=>{const a=s.result;a.objectStoreNames.contains(Un)||a.createObjectStore(Un,{keyPath:"sessionId"})},s.onsuccess=()=>n(s.result),s.onerror=()=>e(s.error??new Error("No se pudo abrir IndexedDB.")),s.onblocked=()=>e(new Error("La base local está bloqueada por otra pestaña."))}),sa.catch(()=>{sa=null}),sa)}function DO(n){return new Promise((e,s)=>{n.onsuccess=()=>e(n.result),n.onerror=()=>s(n.error??new Error("Falló una operación local."))})}function du(n){return new Promise((e,s)=>{n.oncomplete=()=>e(),n.onerror=()=>s(n.error??new Error("Falló la transacción local.")),n.onabort=()=>s(n.error??new Error("La transacción local fue cancelada."))})}function MO(n){if(!n||typeof n!="object")return!1;const e=n;return e.version===p_&&typeof e.sessionId=="string"&&typeof e.updatedAt=="number"&&(e.currentView==="cruce"||e.currentView==="historial")&&Array.isArray(e.uploadedBankFiles)&&typeof e.isCruceExecuted=="boolean"}async function UO(n,e){const s=Date.now()-NO,a=n.transaction(Un,"readwrite"),l=du(a),c=a.objectStore(Un).openCursor();await new Promise((d,m)=>{c.onsuccess=()=>{const p=c.result;if(!p){d();return}const g=p.value;g.sessionId!==e&&typeof g.updatedAt=="number"&&g.updatedAtm(c.error??new Error("No se pudieron limpiar borradores antiguos."))}),await l}async function BO(){const n=Ef();try{const e=await Af(),s=e.transaction(Un,"readonly"),a=du(s),l=await DO(s.objectStore(Un).get(n));return await a,UO(e,n).catch(()=>{}),MO(l)?l:null}catch{return null}}function LO(n){const e=Ef(),s={...n,version:p_,sessionId:e,updatedAt:Date.now()};return ma=ma.catch(()=>{}).then(async()=>{const l=(await Af()).transaction(Un,"readwrite"),u=du(l);l.objectStore(Un).put(s),await u}).catch(()=>{}),ma}function Zv(){const n=Ef();return ma=ma.catch(()=>{}).then(async()=>{const s=(await Af()).transaction(Un,"readwrite"),a=du(s);s.objectStore(Un).delete(n),await a}).catch(()=>{}),ma}const Wv=!0,$d="glm_cruce_selected_country",zO={name:"Isaac Aracena",email:"iaracena@gomezleemarketing.com"};function VO(n){const e=(n==null?void 0:n.user_metadata)??{},s=e.full_name||e.name||e.display_name;if(s)return String(s);const a=String((n==null?void 0:n.email)??"Usuario GLM");return a.includes("@")?a.split("@")[0]:a}function PO(){try{const n=window.localStorage.getItem($d),e=Mr.find(s=>s.code===n&&s.enabled);return(e==null?void 0:e.code)??null}catch{return null}}function HO(){const[n,e]=V.useState("cruce"),[s,a]=V.useState(!1),[l,u]=V.useState(null),[c,d]=V.useState([]),[m,p]=V.useState(!1),[g,v]=V.useState(null),[w,x]=V.useState(0),[S,A]=V.useState(()=>PO()),[k,C]=V.useState(null),[N,O]=V.useState(Wv),[P,J]=V.useState(!1),[X,G]=V.useState(null),Z=V.useRef(0),ee=V.useMemo(()=>kr&&Ut,[]),ne=S?nv(S):null,le=(Y,I)=>{G({message:Y,type:I})};V.useEffect(()=>{if(!ee||!Ut){O(!1);return}let Y=!0;const I=async B=>{const K=++Z.current;if(!(B!=null&&B.email)){Y&&K===Z.current&&C(null);return}try{const W=await kO();if(!Y||K!==Z.current)return;const he=m_(B.email),ue=!!W.email&&W.email===he;if(!W.ok||!W.authenticated||!W.authorized||!W.active||!W.appAccess||!ue){C(null),le(W.reason||"Tu usuario no tiene acceso autorizado a este módulo.","error"),Ut.auth.signOut();return}C({name:W.name||VO(B),email:he})}catch(W){if(!Y||K!==Z.current)return;console.error("No se pudo validar el acceso en Supabase:",W),C(null),le("No se pudo validar tu acceso en Supabase. Intenta iniciar sesión nuevamente.","error"),Ut.auth.signOut()}};(async()=>{var B;try{const{data:K,error:W}=await Ut.auth.getSession();if(W)throw W;if(!Y)return;await I((B=K.session)==null?void 0:B.user)}catch(K){if(!Y)return;console.error("No se pudo recuperar la sesión de Supabase:",K),C(null),le("No se pudo recuperar tu sesión. Intenta iniciar sesión nuevamente.","error")}finally{Y&&O(!1)}})();const{data:E}=Ut.auth.onAuthStateChange((B,K)=>{if(Y){if(K!=null&&K.user){window.setTimeout(()=>{Y&&I(K.user)},0);return}B==="SIGNED_OUT"&&(Z.current+=1,C(null),O(!1))}});return()=>{Y=!1,Z.current+=1,E.subscription.unsubscribe()}},[ee]),V.useEffect(()=>{let Y=!0;return(async()=>{const oe=await BO();Y&&(oe&&(!S||oe.countryCode===S?(oe.countryCode&&A(oe.countryCode),e(oe.currentView),u(oe.uploadedPayroll),d(oe.uploadedBankFiles),p(oe.isCruceExecuted),v(oe.cruceResponse)):await Zv()),Y&&J(!0))})(),()=>{Y=!1}},[]),V.useEffect(()=>{P&&LO({countryCode:S,currentView:n,uploadedPayroll:l,uploadedBankFiles:c,isCruceExecuted:m,cruceResponse:g})},[P,S,n,l,c,m,g]);const de=()=>{u(null),d([]),p(!1),v(null),x(Y=>Y+1)},je=async()=>{await Zv(),Ut&&await Ut.auth.signOut(),C(null),A(null),window.localStorage.removeItem($d),de(),e("cruce"),le("Sesión cerrada correctamente.","success")},Re=()=>{de(),e("cruce"),le("Nuevo cruce iniciado.","success")},ke=Y=>{const I=nv(Y);if(!I.enabled){le(`${I.name} estará disponible próximamente.`,"info");return}S&&S!==Y&&de(),A(Y),window.localStorage.setItem($d,Y),e("cruce"),a(!1)},L=()=>{A(null),a(!1)};if(N)return b.jsx("div",{className:"min-h-screen bg-[#F5F5F5] flex items-center justify-center text-[#4F758B] font-black uppercase tracking-wider",children:"Validando acceso..."});if(!k)return b.jsxs(b.Fragment,{children:[b.jsx(xO,{onShowToast:le}),b.jsx(cs,{children:X&&b.jsx(id,{message:X.message,type:X.type,onClose:()=>G(null)})})]});if(!ne)return b.jsxs(b.Fragment,{children:[b.jsx(TO,{onSelect:ke}),b.jsx(cs,{children:X&&b.jsx(id,{message:X.message,type:X.type,onClose:()=>G(null)})})]});const Q=k??zO;return b.jsxs("div",{className:"min-h-screen bg-[#F5F5F5] selection:bg-[#6CC24A] selection:text-white antialiased font-sans flex flex-col",children:[b.jsxs(bn.div,{initial:{opacity:0},animate:{opacity:1},className:"min-h-screen flex w-full relative overflow-x-hidden",children:[b.jsx(cs,{children:s&&b.jsx(bn.div,{initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},onClick:()=>a(!1),className:"fixed inset-0 bg-black/45 z-45 md:hidden"},"backdrop")}),b.jsx(PR,{currentView:n,onViewChange:Y=>{e(Y),a(!1)},onNewCruce:()=>{Re(),a(!1)},isSidebarOpen:s,onCloseSidebar:()=>a(!1),user:Q,isAuthEnabled:Wv,onLogout:je,country:ne,onCountryChange:ke,onChooseAnotherCountry:L}),b.jsx("div",{className:"pl-0 md:pl-64 flex-1 flex flex-col w-full min-h-screen transition-all duration-300",children:b.jsx(cs,{mode:"wait",children:n==="cruce"?b.jsx(bn.div,{initial:{opacity:0,x:10},animate:{opacity:1,x:0},exit:{opacity:0,x:-10},transition:{duration:.2},className:"flex-1 flex flex-col",children:b.jsx(rC,{onShowToast:le,uploadedPayroll:l,setUploadedPayroll:u,uploadedBankFiles:c,setUploadedBankFiles:d,isCruceExecuted:m,setIsCruceExecuted:p,response:g,setResponse:v,onOpenSidebar:()=>a(!0),operatorName:Q.name,operatorEmail:Q.email,resetToken:w,country:ne})},`view-cruce-${ne.code}`):b.jsx(bn.div,{initial:{opacity:0,x:10},animate:{opacity:1,x:0},exit:{opacity:0,x:-10},transition:{duration:.2},className:"flex-1 flex flex-col",children:b.jsx(_O,{onShowToast:le,onOpenSidebar:()=>a(!0),operatorName:Q.name,operatorEmail:Q.email,country:ne})},`view-history-${ne.code}`)})})]},`interface-${ne.code}`),b.jsx(cs,{children:X&&b.jsx(id,{message:X.message,type:X.type,onClose:()=>G(null)})})]})}IS.createRoot(document.getElementById("root")).render(b.jsx(V.StrictMode,{children:b.jsx(HO,{})})); diff --git a/dist/favicon.png b/dist/favicon.png deleted file mode 100644 index 11eccc8..0000000 Binary files a/dist/favicon.png and /dev/null differ diff --git a/dist/gt-flag.png b/dist/gt-flag.png deleted file mode 100644 index 06a8db6..0000000 Binary files a/dist/gt-flag.png and /dev/null differ diff --git a/dist/index.html b/dist/index.html deleted file mode 100644 index 5e7a274..0000000 --- a/dist/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Portal de Cruce de Cuentas GLM - - - - - - -
- - - diff --git a/dist/tt-flag.png b/dist/tt-flag.png deleted file mode 100644 index 2ba6c21..0000000 Binary files a/dist/tt-flag.png and /dev/null differ diff --git a/src/App.tsx b/src/App.tsx index e3cf2c0..318d0b3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import Sidebar from './components/Sidebar'; import Dashboard from './components/Dashboard'; @@ -14,8 +14,14 @@ import { UploadedFile, ViewType, } from './types'; -import { isAllowedEmail } from './lib/auth'; +import { getCurrentUserAccess, normalizeEmail } from './lib/auth'; +import { ensureFreshSupabaseSession } from './lib/sessionHealth'; import { isSupabaseConfigured, supabase } from './lib/supabase'; +import { + clearTabSessionDraft, + loadTabSessionDraft, + saveTabSessionDraft, +} from './lib/tabSessionState'; import { COUNTRIES, getCountryConfig } from './config/countries'; const AUTH_ENABLED = import.meta.env.VITE_ENABLE_SUPABASE_AUTH === 'true'; @@ -56,7 +62,9 @@ export default function App() { const [selectedCountryCode, setSelectedCountryCode] = useState(() => getStoredCountry()); const [currentUser, setCurrentUser] = useState(AUTH_ENABLED ? null : TEMPORARY_USER); const [authLoading, setAuthLoading] = useState(AUTH_ENABLED); + const [tabDraftHydrated, setTabDraftHydrated] = useState(false); const [toast, setToast] = useState<{ message: string; type: ToastType } | null>(null); + const authValidationIdRef = useRef(0); const authReady = useMemo(() => AUTH_ENABLED && isSupabaseConfigured && supabase, []); const selectedCountry = selectedCountryCode ? getCountryConfig(selectedCountryCode) : null; @@ -76,43 +84,198 @@ export default function App() { let isMounted = true; const applySessionUser = async (sessionUser: any) => { + const validationId = ++authValidationIdRef.current; + if (!sessionUser?.email) { - setCurrentUser(null); + if (isMounted && validationId === authValidationIdRef.current) { + setCurrentUser(null); + } return; } - if (!isAllowedEmail(sessionUser.email)) { - await supabase.auth.signOut(); - setCurrentUser(null); - handleShowToast('Tu usuario no tiene acceso autorizado a este módulo.', 'error'); - return; - } + try { + const access = await getCurrentUserAccess(); - setCurrentUser({ - name: getDisplayNameFromSessionUser(sessionUser), - email: sessionUser.email, - }); + if (!isMounted || validationId !== authValidationIdRef.current) { + return; + } + + const sessionEmail = normalizeEmail(sessionUser.email); + const accessEmailMatchesSession = + Boolean(access.email) && access.email === sessionEmail; + + if ( + !access.ok || + !access.authenticated || + !access.authorized || + !access.active || + !access.appAccess || + !accessEmailMatchesSession + ) { + setCurrentUser(null); + handleShowToast( + access.reason || 'Tu usuario no tiene acceso autorizado a este módulo.', + 'error', + ); + void supabase.auth.signOut(); + return; + } + + setCurrentUser({ + name: access.name || getDisplayNameFromSessionUser(sessionUser), + email: sessionEmail, + }); + } catch (error) { + if (!isMounted || validationId !== authValidationIdRef.current) { + return; + } + + console.error('No se pudo validar el acceso en Supabase:', error); + setCurrentUser(null); + handleShowToast( + 'No se pudo validar tu acceso en Supabase. Intenta iniciar sesión nuevamente.', + 'error', + ); + void supabase.auth.signOut(); + } }; const loadSession = async () => { - const { data } = await supabase.auth.getSession(); - if (!isMounted) return; - await applySessionUser(data.session?.user); - setAuthLoading(false); + try { + const { data, error } = await supabase.auth.getSession(); + if (error) throw error; + if (!isMounted) return; + await applySessionUser(data.session?.user); + } catch (error) { + if (!isMounted) return; + console.error('No se pudo recuperar la sesión de Supabase:', error); + setCurrentUser(null); + handleShowToast( + 'No se pudo recuperar tu sesión. Intenta iniciar sesión nuevamente.', + 'error', + ); + } finally { + if (isMounted) setAuthLoading(false); + } }; - loadSession(); + void loadSession(); - const { data: listener } = supabase.auth.onAuthStateChange(async (_event, session) => { - await applySessionUser(session?.user); + const { data: listener } = supabase.auth.onAuthStateChange((event, session) => { + if (!isMounted) return; + + if (session?.user) { + // Supabase recomienda no encadenar llamadas asíncronas al cliente dentro + // del callback. Se difiere la RPC para evitar bloqueos durante el OAuth o + // la renovación automática del token. + window.setTimeout(() => { + if (isMounted) void applySessionUser(session.user); + }, 0); + return; + } + + // Evita desmontar toda la interfaz por un estado nulo transitorio durante + // la reanudación o renovación de la sesión al volver a una pestaña inactiva. + if (event === 'SIGNED_OUT') { + authValidationIdRef.current += 1; + setCurrentUser(null); + setAuthLoading(false); + } }); return () => { isMounted = false; + authValidationIdRef.current += 1; listener.subscription.unsubscribe(); }; }, [authReady]); + useEffect(() => { + if (!AUTH_ENABLED || !authReady || !supabase) return; + + let lastResumeAttempt = 0; + + const resumeSession = () => { + if (document.visibilityState !== 'visible') return; + + const now = Date.now(); + if (now - lastResumeAttempt < 5000) return; + lastResumeAttempt = now; + + // Una pestaña suspendida varias horas puede volver antes de que Supabase + // complete su renovación automática. La revalidación es silenciosa y no + // desmonta la interfaz por errores transitorios de red. + void ensureFreshSupabaseSession(300).catch((error) => { + console.warn('No se pudo revalidar la sesión al reanudar la pestaña:', error); + }); + }; + + document.addEventListener('visibilitychange', resumeSession); + window.addEventListener('focus', resumeSession); + + return () => { + document.removeEventListener('visibilitychange', resumeSession); + window.removeEventListener('focus', resumeSession); + }; + }, [authReady]); + + useEffect(() => { + let isMounted = true; + + const restoreTabDraft = async () => { + const draft = await loadTabSessionDraft(); + if (!isMounted) return; + + if (draft) { + // Si el país fue cambiado explícitamente en otra parte de la app, no se + // restaura un borrador perteneciente a otro módulo. + if (!selectedCountryCode || draft.countryCode === selectedCountryCode) { + if (draft.countryCode) { + setSelectedCountryCode(draft.countryCode); + } + setCurrentView(draft.currentView); + setUploadedPayroll(draft.uploadedPayroll); + setUploadedBankFiles(draft.uploadedBankFiles); + setIsCruceExecuted(draft.isCruceExecuted); + setCruceResponse(draft.cruceResponse); + } else { + await clearTabSessionDraft(); + } + } + + if (isMounted) setTabDraftHydrated(true); + }; + + void restoreTabDraft(); + + return () => { + isMounted = false; + }; + // La restauración ocurre una sola vez usando los valores iniciales del tab. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!tabDraftHydrated) return; + + void saveTabSessionDraft({ + countryCode: selectedCountryCode, + currentView, + uploadedPayroll, + uploadedBankFiles, + isCruceExecuted, + cruceResponse, + }); + }, [ + tabDraftHydrated, + selectedCountryCode, + currentView, + uploadedPayroll, + uploadedBankFiles, + isCruceExecuted, + cruceResponse, + ]); + const clearCurrentCruce = () => { setUploadedPayroll(null); setUploadedBankFiles([]); @@ -122,6 +285,7 @@ export default function App() { }; const handleLogout = async () => { + await clearTabSessionDraft(); if (supabase) await supabase.auth.signOut(); setCurrentUser(AUTH_ENABLED ? null : TEMPORARY_USER); setSelectedCountryCode(null); diff --git a/src/components/BambooCorrectionModal.tsx b/src/components/BambooCorrectionModal.tsx new file mode 100644 index 0000000..2b06899 --- /dev/null +++ b/src/components/BambooCorrectionModal.tsx @@ -0,0 +1,338 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { AlertTriangle, Send, X } from 'lucide-react'; +import { CountryConfig, PeriodRange, ReconciliationRow } from '../types'; +import { supabase } from '../lib/supabase'; +import { ensureFreshSupabaseSession } from '../lib/sessionHealth'; + +const REPORT_BAMBOO_URL = + (import.meta.env.VITE_REPORTAR_BAMBOO_URL as string | undefined) || ''; + +interface BambooCorrectionModalProps { + row: ReconciliationRow; + country: CountryConfig; + periodRange: PeriodRange; + operatorName: string; + operatorEmail: string; + executionId?: string; + reportUrl?: string; + onClose: () => void; + onSuccess: (rowId: string) => void; + onShowToast: (message: string, type: 'success' | 'error' | 'info') => void; +} + +const REASONS = [ + 'Nombre mal escrito en BambooHR', + 'Apodo o nombre de uso', + 'Nombre incompleto / nombres adicionales', + 'Otro', +]; + +function parseJsonSafely(text: string) { + try { + return text ? JSON.parse(text) : null; + } catch { + return null; + } +} + +export default function BambooCorrectionModal({ + row, + country, + periodRange, + operatorName, + operatorEmail, + executionId, + reportUrl, + onClose, + onSuccess, + onShowToast, +}: BambooCorrectionModalProps) { + const suggested = row.bestBambooCandidate; + const [bambooName, setBambooName] = useState(suggested?.employeeName || ''); + const [employeeNumber, setEmployeeNumber] = useState(suggested?.employeeNumber || ''); + const [reason, setReason] = useState(REASONS[0]); + const [comment, setComment] = useState(''); + const [confirmed, setConfirmed] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + setBambooName(suggested?.employeeName || ''); + setEmployeeNumber(suggested?.employeeNumber || ''); + setReason(REASONS[0]); + setComment(''); + setConfirmed(false); + }, [row.id, suggested?.employeeName, suggested?.employeeNumber]); + + const recipientLabel = useMemo( + () => + country.code === 'TT' + ? 'Laurele Nicome-Jack' + : 'Pablo Gamboa Cú, Sara Meyer Lima y María Morales Adler', + [country.code], + ); + + const sourceNames = useMemo( + () => + Array.from( + new Set( + [row.employee, row.bankNameFile, row.bankAccountHolder] + .map((value) => String(value || '').trim()) + .filter(Boolean), + ), + ), + [row.employee, row.bankNameFile, row.bankAccountHolder], + ); + + const handleSubmit = async () => { + if (!REPORT_BAMBOO_URL) { + onShowToast('Falta configurar VITE_REPORTAR_BAMBOO_URL.', 'error'); + return; + } + + if (!bambooName.trim()) { + onShowToast('Escribe el nombre correcto del empleado en BambooHR.', 'error'); + return; + } + + if (!confirmed) { + onShowToast('Confirma que se trata de la misma persona antes de enviar el caso.', 'error'); + return; + } + + if (!supabase) { + onShowToast('Supabase no está configurado en esta aplicación.', 'error'); + return; + } + + setIsSubmitting(true); + + try { + const session = await ensureFreshSupabaseSession(300); + const accessToken = session?.access_token; + + if (!accessToken) { + throw new Error('Tu sesión expiró. Inicia sesión nuevamente antes de reportar el caso.'); + } + + const correctionPayload = { + country: country.code, + country_name: country.name, + source_name: row.employee, + source_names: sourceNames, + bank_name_file: row.bankNameFile || '', + bank_account_holder: row.bankAccountHolder || '', + bank_account: row.account || '', + bank_amount: row.amountBank, + currency: country.currency, + bamboo_name: bambooName.trim(), + bamboo_employee_number: employeeNumber.trim(), + reason, + comment: comment.trim(), + confirmed_same_person: true, + execution_id: executionId || '', + report_url: reportUrl || '', + period_label: periodRange.label, + period_start: periodRange.start, + period_end: periodRange.end, + requested_by_name: operatorName, + requested_by_email: operatorEmail, + }; + + // FormData evita una preflight CORS innecesaria al llamar el webhook de n8n. + // El JWT sigue viajando únicamente por HTTPS y n8n lo valida contra Supabase. + const formData = new FormData(); + formData.append('payload', JSON.stringify(correctionPayload)); + formData.append('access_token', accessToken); + + const response = await fetch(REPORT_BAMBOO_URL, { + method: 'POST', + body: formData, + }); + + const responseText = await response.text(); + const payload = parseJsonSafely(responseText); + + if (!response.ok || payload?.ok === false) { + const message = + payload?.message || + payload?.error || + payload?.errors?.[0] || + responseText || + 'No se pudo enviar el caso a RRHH.'; + throw new Error(String(message)); + } + + onSuccess(row.id); + onShowToast( + `Caso enviado a RRHH de ${country.name}. La equivalencia quedó registrada para futuros cruces.`, + 'success', + ); + onClose(); + } catch (error) { + const message = error instanceof Error ? error.message : 'No se pudo enviar el caso a RRHH.'; + onShowToast(message, 'error'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ +
+ +
+
+
+

Nombre detectado

+

{row.employee}

+
+
+

Cuenta bancaria

+

{row.account || '—'}

+
+
+

Se notificará a

+

{recipientLabel}

+
+
+ + {suggested?.employeeName && ( +
+ +
+ Candidato sugerido por el cruce:{' '} + {suggested.employeeName} + {suggested.employeeNumber ? ` · Employee Number ${suggested.employeeNumber}` : ''}. + Verifícalo manualmente antes de confirmar. +
+
+ )} + +
+ + + + + + +