"use client"; import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Stepper } from "@/components/ui/stepper"; import type { ActaLookupDictionary } from "@/lib/extraction/schema"; type OnboardingValues = { name: string; tradeName: string; rfc: string; legalRepresentative: string; incorporationDate: string; deedNumber: string; notaryName: string; fiscalAddress: string; businessPurpose: string; industry: string; operatingState: string; municipality: string; companySize: string; yearsOfOperation: string; annualRevenueRange: string; hasGovernmentContracts: string; country: string; primaryObjective: string; }; type OnboardingWizardProps = { initialValues: OnboardingValues; hasActaDocument: boolean; actaUploadedAt: string | null; }; type OnboardingPrefillPayload = { [K in keyof OnboardingValues]?: string | null; }; type ActaUiFields = { name: string | null; rfc: string | null; legalRepresentative: string | null; incorporationDate: string | null; deedNumber: string | null; notaryName: string | null; fiscalAddress: string | null; businessPurpose: string | null; stateOfIncorporation: string | null; }; type ExtractActaPayload = { ok?: boolean; error?: string; code?: string; fields?: Partial; lookupDictionary?: ActaLookupDictionary; rawText?: string; methodUsed?: "direct" | "ocr"; extractionEngine?: "ai" | "regex_fallback"; aiModel?: string | null; numPages?: number; warnings?: string[]; extractedData?: Partial & { lookupDictionary?: ActaLookupDictionary; extractedFields?: string[]; detectedLookupFields?: string[]; confidence?: "low" | "medium" | "high"; extractionEngine?: "ai" | "regex_fallback"; }; actaUploadedAt?: string; }; const steps = ["Acta constitutiva", "Datos legales", "Perfil", "Confirmacion"]; const companySizeOptions = [ "Micro (1-10 empleados)", "Pequena (11-50 empleados)", "Mediana (51-250 empleados)", "Grande (251+ empleados)", ]; const annualRevenueRangeOptions = [ "Menos de $250,000", "$250,000 - $500,000", "$500,001 - $1,000,000", "Mas de $1,000,000", ]; const MAX_ACTA_UPLOAD_BYTES = 15 * 1024 * 1024; export function OnboardingWizard({ initialValues, hasActaDocument, actaUploadedAt }: OnboardingWizardProps) { const router = useRouter(); const [step, setStep] = useState(1); const [values, setValues] = useState(initialValues); const [selectedActaFile, setSelectedActaFile] = useState(null); const [actaReady, setActaReady] = useState(hasActaDocument); const [lastActaUploadAt, setLastActaUploadAt] = useState(actaUploadedAt); const [extractConfidence, setExtractConfidence] = useState<"low" | "medium" | "high" | null>(null); const [detectedFields, setDetectedFields] = useState([]); const [detectedLookupFields, setDetectedLookupFields] = useState([]); const [latestFields, setLatestFields] = useState(null); const [lookupDictionaryDebug, setLookupDictionaryDebug] = useState | null>(null); const [analysisMethod, setAnalysisMethod] = useState<"direct" | "ocr" | null>(null); const [extractionEngine, setExtractionEngine] = useState<"ai" | "regex_fallback" | null>(null); const [aiModel, setAiModel] = useState(null); const [analysisWarnings, setAnalysisWarnings] = useState([]); const [errorMessage, setErrorMessage] = useState(null); const [isUploadingActa, setIsUploadingActa] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const completion = useMemo(() => Math.round((step / steps.length) * 100), [step]); function updateValue(field: keyof OnboardingValues, value: string) { setValues((previous) => ({ ...previous, [field]: value })); } function normalizeField(value: string | null | undefined) { if (!value) { return null; } const cleaned = value.trim(); return cleaned || null; } function toActaUiFields(payload: Partial | undefined): ActaUiFields | null { if (!payload) { return null; } return { name: normalizeField(payload.name), rfc: normalizeField(payload.rfc), legalRepresentative: normalizeField(payload.legalRepresentative), incorporationDate: normalizeField(payload.incorporationDate), deedNumber: normalizeField(payload.deedNumber), notaryName: normalizeField(payload.notaryName), fiscalAddress: normalizeField(payload.fiscalAddress), businessPurpose: normalizeField(payload.businessPurpose), stateOfIncorporation: normalizeField(payload.stateOfIncorporation), }; } function applyExtractedData(payload: (OnboardingPrefillPayload & Partial) | undefined) { if (!payload) { return; } const fromExtracted = (value: string | null | undefined, previous: string) => (value === undefined ? previous : (value ?? "")); setValues((previous) => ({ ...previous, name: fromExtracted(payload.name, previous.name), tradeName: previous.tradeName || fromExtracted(payload.name, previous.tradeName), // RFC is user-managed; acta extraction should never auto-fill it. rfc: "", legalRepresentative: fromExtracted(payload.legalRepresentative, previous.legalRepresentative), incorporationDate: fromExtracted(payload.incorporationDate, previous.incorporationDate), deedNumber: fromExtracted(payload.deedNumber, previous.deedNumber), notaryName: fromExtracted(payload.notaryName, previous.notaryName), fiscalAddress: fromExtracted(payload.fiscalAddress, previous.fiscalAddress), businessPurpose: fromExtracted(payload.businessPurpose, previous.businessPurpose), industry: fromExtracted(payload.industry, previous.industry), country: fromExtracted(payload.country, previous.country), })); } function compactDictionaryForDebug(dictionary: ActaLookupDictionary | undefined) { if (!dictionary) { return null; } const compact = JSON.parse( JSON.stringify(dictionary, (_key, value: unknown) => { if (value === null || value === undefined) { return undefined; } if (Array.isArray(value) && value.length === 0) { return undefined; } return value; }), ) as Record; return Object.keys(compact).length > 0 ? compact : null; } async function handleActaUpload() { if (!selectedActaFile) { setErrorMessage("Selecciona primero el archivo PDF del Acta constitutiva."); return; } if (selectedActaFile.size > MAX_ACTA_UPLOAD_BYTES) { setErrorMessage("El archivo excede el limite de 15MB."); return; } setErrorMessage(null); setIsUploadingActa(true); try { const formData = new FormData(); formData.append("file", selectedActaFile); const response = await fetch("/api/onboarding/acta", { method: "POST", body: formData, }); let payload: ExtractActaPayload | null = null; try { payload = (await response.json()) as ExtractActaPayload; } catch { payload = null; } if (!response.ok || !payload?.ok) { if (response.status === 413) { setErrorMessage("El servidor/proxy rechazo la carga (413). Ajusta el limite de upload en Nginx y vuelve a intentar."); return; } const codeSuffix = payload?.code ? ` [${payload.code}]` : ""; setErrorMessage((payload?.error ?? "No fue posible procesar el Acta constitutiva.") + codeSuffix); return; } const normalizedFields = toActaUiFields(payload.fields); applyExtractedData(payload.fields ?? payload.extractedData); setLatestFields(normalizedFields); setExtractConfidence(payload.extractedData?.confidence ?? null); setLookupDictionaryDebug(compactDictionaryForDebug(payload.lookupDictionary ?? payload.extractedData?.lookupDictionary)); setDetectedLookupFields(payload.extractedData?.detectedLookupFields ?? []); setDetectedFields( payload.extractedData?.extractedFields ?? Object.entries(normalizedFields ?? {}) .filter(([, value]) => Boolean(value)) .map(([field]) => field), ); setAnalysisMethod(payload.methodUsed ?? null); setExtractionEngine(payload.extractionEngine ?? payload.extractedData?.extractionEngine ?? null); setAiModel(payload.aiModel ?? null); setAnalysisWarnings(payload.warnings ?? []); setActaReady(true); setLastActaUploadAt(payload.actaUploadedAt ?? new Date().toISOString()); setStep(2); } catch { setErrorMessage("No fue posible subir y analizar el documento."); } finally { setIsUploadingActa(false); } } function validateCurrentStep() { if (step === 1 && !actaReady) { setErrorMessage("Debes cargar el Acta constitutiva en PDF para continuar."); return false; } if (step === 2 && !values.name.trim()) { setErrorMessage("Confirma el nombre legal de la empresa."); return false; } if (step === 3 && !values.tradeName.trim()) { setErrorMessage("Ingresa el nombre comercial."); return false; } if (step === 3 && !values.industry.trim()) { setErrorMessage("Selecciona el sector o giro de la empresa."); return false; } if (step === 3 && !values.operatingState.trim()) { setErrorMessage("Ingresa el estado de operacion."); return false; } if (step === 3 && !values.municipality.trim()) { setErrorMessage("Ingresa el municipio."); return false; } if (step === 3 && !values.companySize.trim()) { setErrorMessage("Selecciona el tamano de empresa."); return false; } if (step === 3 && !values.yearsOfOperation.trim()) { setErrorMessage("Ingresa los anos de operacion."); return false; } if (step === 3 && !values.annualRevenueRange.trim()) { setErrorMessage("Selecciona el rango de facturacion anual."); return false; } if (step === 3 && !values.hasGovernmentContracts.trim()) { setErrorMessage("Indica si has participado en licitaciones con gobierno."); return false; } setErrorMessage(null); return true; } function goNext() { if (!validateCurrentStep()) { return; } setStep((previous) => Math.min(previous + 1, steps.length)); } function goBack() { setErrorMessage(null); setStep((previous) => Math.max(previous - 1, 1)); } async function handleSubmit() { if (!validateCurrentStep()) { return; } setIsSubmitting(true); setErrorMessage(null); try { const response = await fetch("/api/onboarding", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(values), }); const payload = (await response.json()) as { ok?: boolean; redirectTo?: string; error?: string }; if (!response.ok || !payload.ok) { setErrorMessage(payload.error ?? "No fue posible guardar el onboarding."); return; } router.push(payload.redirectTo ?? "/diagnostic"); router.refresh(); } catch { setErrorMessage("No fue posible guardar el onboarding."); } finally { setIsSubmitting(false); } } return (

Onboarding con Acta constitutiva

Primero sube el PDF de Acta constitutiva para extraer datos legales, luego confirma y continua al diagnostico.

Progreso: {completion}%
{step === 1 ? (

Documento requerido

Carga el archivo Acta constitutiva en formato PDF. Este documento se guarda como llave de referencia de tu empresa.

setSelectedActaFile(event.target.files?.[0] ?? null)} />
{selectedActaFile ?

Archivo: {selectedActaFile.name}

: null}
{actaReady ? (
Acta guardada correctamente{lastActaUploadAt ? ` (${new Date(lastActaUploadAt).toLocaleString()})` : ""}.
) : null} {extractConfidence ? (

Calidad de extraccion: {extractConfidence}.

) : null} {analysisMethod === "ocr" ? (

OCR aplicado para recuperar texto de un PDF escaneado.

) : null} {extractionEngine === "ai" ? (

Extraccion legal realizada con AI{aiModel ? ` (${aiModel})` : ""}.

) : null} {extractionEngine === "regex_fallback" ? (

AI no estuvo disponible; se aplico extraccion de respaldo para no bloquear onboarding.

) : null} {analysisWarnings.length ? (

{analysisWarnings.join(" ")}

) : null} {latestFields ? (

Campos extraidos

Razon social / nombre legal

{latestFields.name ?? "-"}

RFC

{latestFields.rfc ?? "-"}

Representante legal

{latestFields.legalRepresentative ?? "-"}

Fecha de constitucion

{latestFields.incorporationDate ?? "-"}

Numero de escritura

{latestFields.deedNumber ?? "-"}

Notario

{latestFields.notaryName ?? "-"}

Domicilio fiscal

{latestFields.fiscalAddress ?? "-"}

Objeto social

{latestFields.businessPurpose ?? "-"}

) : null} {lookupDictionaryDebug ? (
Debug de extraccion {detectedLookupFields.length ? (

Campos detectados en diccionario: {detectedLookupFields.join(", ")}.

) : null}
                    {JSON.stringify(lookupDictionaryDebug, null, 2)}
                  
) : null}
) : null} {step === 2 ? (
{detectedFields.length ? (

Campos detectados automaticamente: {detectedFields.join(", ")}.

) : null}
updateValue("name", event.target.value)} placeholder="Innova S.A. de C.V." />
updateValue("rfc", event.target.value)} placeholder="ABC123456T12" />
updateValue("legalRepresentative", event.target.value)} placeholder="Ana Torres" />
updateValue("incorporationDate", event.target.value)} placeholder="15 de marzo de 2019" />
updateValue("deedNumber", event.target.value)} placeholder="12345" />
updateValue("notaryName", event.target.value)} placeholder="Notario Publico No. 15" />