initial push
This commit is contained in:
737
src/components/app/onboarding-wizard.tsx
Normal file
737
src/components/app/onboarding-wizard.tsx
Normal file
@@ -0,0 +1,737 @@
|
||||
"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<ActaUiFields>;
|
||||
lookupDictionary?: ActaLookupDictionary;
|
||||
rawText?: string;
|
||||
methodUsed?: "direct" | "ocr";
|
||||
extractionEngine?: "ai" | "regex_fallback";
|
||||
aiModel?: string | null;
|
||||
numPages?: number;
|
||||
warnings?: string[];
|
||||
extractedData?: Partial<OnboardingValues> & {
|
||||
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<OnboardingValues>(initialValues);
|
||||
const [selectedActaFile, setSelectedActaFile] = useState<File | null>(null);
|
||||
const [actaReady, setActaReady] = useState(hasActaDocument);
|
||||
const [lastActaUploadAt, setLastActaUploadAt] = useState<string | null>(actaUploadedAt);
|
||||
const [extractConfidence, setExtractConfidence] = useState<"low" | "medium" | "high" | null>(null);
|
||||
const [detectedFields, setDetectedFields] = useState<string[]>([]);
|
||||
const [detectedLookupFields, setDetectedLookupFields] = useState<string[]>([]);
|
||||
const [latestFields, setLatestFields] = useState<ActaUiFields | null>(null);
|
||||
const [lookupDictionaryDebug, setLookupDictionaryDebug] = useState<Record<string, unknown> | null>(null);
|
||||
const [analysisMethod, setAnalysisMethod] = useState<"direct" | "ocr" | null>(null);
|
||||
const [extractionEngine, setExtractionEngine] = useState<"ai" | "regex_fallback" | null>(null);
|
||||
const [aiModel, setAiModel] = useState<string | null>(null);
|
||||
const [analysisWarnings, setAnalysisWarnings] = useState<string[]>([]);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(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<ActaUiFields> | 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<ActaUiFields>) | 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<string, unknown>;
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<Stepper steps={steps} currentStep={step} />
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-xl font-semibold text-[#1f2a40]">Onboarding con Acta constitutiva</h2>
|
||||
<p className="text-sm text-[#67738c]">
|
||||
Primero sube el PDF de Acta constitutiva para extraer datos legales, luego confirma y continua al diagnostico.
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-5">
|
||||
<div className="rounded-lg border border-[#dde5f2] bg-[#f8fbff] px-3 py-2 text-sm font-medium text-[#50607a]">
|
||||
Progreso: {completion}%
|
||||
</div>
|
||||
|
||||
{step === 1 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-[#dbe4f1] bg-[#f9fbff] p-4">
|
||||
<p className="text-sm font-semibold text-[#2d3c57]">Documento requerido</p>
|
||||
<p className="mt-1 text-sm text-[#5c6b86]">
|
||||
Carga el archivo <span className="font-semibold">Acta constitutiva</span> en formato PDF. Este documento se guarda como llave de
|
||||
referencia de tu empresa.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="acta-file">Archivo PDF</Label>
|
||||
<Input
|
||||
id="acta-file"
|
||||
type="file"
|
||||
accept="application/pdf,.pdf"
|
||||
onChange={(event) => setSelectedActaFile(event.target.files?.[0] ?? null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button onClick={handleActaUpload} disabled={isUploadingActa || isSubmitting}>
|
||||
{isUploadingActa ? "Procesando..." : actaReady ? "Reemplazar y reanalizar PDF" : "Subir y analizar Acta"}
|
||||
</Button>
|
||||
{selectedActaFile ? <p className="text-xs text-[#5e6c86]">Archivo: {selectedActaFile.name}</p> : null}
|
||||
</div>
|
||||
|
||||
{actaReady ? (
|
||||
<div className="rounded-lg border border-[#ccead8] bg-[#ebf8f0] px-3 py-2 text-sm text-[#206546]">
|
||||
Acta guardada correctamente{lastActaUploadAt ? ` (${new Date(lastActaUploadAt).toLocaleString()})` : ""}.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{extractConfidence ? (
|
||||
<p className="text-xs text-[#5d6b86]">
|
||||
Calidad de extraccion: <span className="font-semibold uppercase">{extractConfidence}</span>.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{analysisMethod === "ocr" ? (
|
||||
<p className="text-xs font-medium text-[#2f5d94]">OCR aplicado para recuperar texto de un PDF escaneado.</p>
|
||||
) : null}
|
||||
|
||||
{extractionEngine === "ai" ? (
|
||||
<p className="text-xs font-medium text-[#206546]">
|
||||
Extraccion legal realizada con AI{aiModel ? ` (${aiModel})` : ""}.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{extractionEngine === "regex_fallback" ? (
|
||||
<p className="text-xs font-medium text-[#975f08]">
|
||||
AI no estuvo disponible; se aplico extraccion de respaldo para no bloquear onboarding.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{analysisWarnings.length ? (
|
||||
<p className="text-xs text-[#60708b]">{analysisWarnings.join(" ")}</p>
|
||||
) : null}
|
||||
|
||||
{latestFields ? (
|
||||
<div className="rounded-xl border border-[#dbe4f1] bg-white p-4">
|
||||
<p className="text-sm font-semibold text-[#2d3c57]">Campos extraidos</p>
|
||||
<div className="mt-3 grid gap-3 text-sm md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs uppercase text-[#7886a1]">Razon social / nombre legal</p>
|
||||
<p className="font-medium text-[#1f2a40]">{latestFields.name ?? "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-[#7886a1]">RFC</p>
|
||||
<p className="font-medium text-[#1f2a40]">{latestFields.rfc ?? "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-[#7886a1]">Representante legal</p>
|
||||
<p className="font-medium text-[#1f2a40]">{latestFields.legalRepresentative ?? "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-[#7886a1]">Fecha de constitucion</p>
|
||||
<p className="font-medium text-[#1f2a40]">{latestFields.incorporationDate ?? "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-[#7886a1]">Numero de escritura</p>
|
||||
<p className="font-medium text-[#1f2a40]">{latestFields.deedNumber ?? "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase text-[#7886a1]">Notario</p>
|
||||
<p className="font-medium text-[#1f2a40]">{latestFields.notaryName ?? "-"}</p>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<p className="text-xs uppercase text-[#7886a1]">Domicilio fiscal</p>
|
||||
<p className="font-medium text-[#1f2a40]">{latestFields.fiscalAddress ?? "-"}</p>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<p className="text-xs uppercase text-[#7886a1]">Objeto social</p>
|
||||
<p className="font-medium text-[#1f2a40]">{latestFields.businessPurpose ?? "-"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{lookupDictionaryDebug ? (
|
||||
<details className="rounded-xl border border-[#dbe4f1] bg-[#f8fbff] p-4">
|
||||
<summary className="cursor-pointer text-sm font-semibold text-[#2d3c57]">Debug de extraccion</summary>
|
||||
{detectedLookupFields.length ? (
|
||||
<p className="mt-2 text-xs text-[#60708b]">Campos detectados en diccionario: {detectedLookupFields.join(", ")}.</p>
|
||||
) : null}
|
||||
<pre className="mt-3 overflow-x-auto rounded-lg bg-white p-3 text-xs text-[#33435f]">
|
||||
{JSON.stringify(lookupDictionaryDebug, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 2 ? (
|
||||
<div className="space-y-4">
|
||||
{detectedFields.length ? (
|
||||
<p className="text-xs text-[#60708b]">Campos detectados automaticamente: {detectedFields.join(", ")}.</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<Label htmlFor="org-name">Razon social / nombre legal</Label>
|
||||
<Input
|
||||
id="org-name"
|
||||
value={values.name}
|
||||
onChange={(event) => updateValue("name", event.target.value)}
|
||||
placeholder="Innova S.A. de C.V."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-rfc">RFC</Label>
|
||||
<Input id="org-rfc" value={values.rfc} onChange={(event) => updateValue("rfc", event.target.value)} placeholder="ABC123456T12" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-representative">Representante legal</Label>
|
||||
<Input
|
||||
id="org-representative"
|
||||
value={values.legalRepresentative}
|
||||
onChange={(event) => updateValue("legalRepresentative", event.target.value)}
|
||||
placeholder="Ana Torres"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-inc-date">Fecha de constitucion</Label>
|
||||
<Input
|
||||
id="org-inc-date"
|
||||
value={values.incorporationDate}
|
||||
onChange={(event) => updateValue("incorporationDate", event.target.value)}
|
||||
placeholder="15 de marzo de 2019"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-deed">Numero de escritura</Label>
|
||||
<Input
|
||||
id="org-deed"
|
||||
value={values.deedNumber}
|
||||
onChange={(event) => updateValue("deedNumber", event.target.value)}
|
||||
placeholder="12345"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-notary">Notario</Label>
|
||||
<Input
|
||||
id="org-notary"
|
||||
value={values.notaryName}
|
||||
onChange={(event) => updateValue("notaryName", event.target.value)}
|
||||
placeholder="Notario Publico No. 15"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="org-address">Domicilio fiscal</Label>
|
||||
<textarea
|
||||
id="org-address"
|
||||
value={values.fiscalAddress}
|
||||
onChange={(event) => updateValue("fiscalAddress", event.target.value)}
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-[#cfd8e6] bg-white px-3 py-2 text-sm text-[#1f2a3d] placeholder:text-[#8c96ab] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
|
||||
placeholder="Calle, numero, colonia, municipio, estado"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="org-purpose">Objeto social</Label>
|
||||
<textarea
|
||||
id="org-purpose"
|
||||
value={values.businessPurpose}
|
||||
onChange={(event) => updateValue("businessPurpose", event.target.value)}
|
||||
rows={3}
|
||||
className="w-full rounded-lg border border-[#cfd8e6] bg-white px-3 py-2 text-sm text-[#1f2a3d] placeholder:text-[#8c96ab] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
|
||||
placeholder="Actividad principal segun Acta constitutiva"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 3 ? (
|
||||
<div className="space-y-5">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<Label htmlFor="org-trade-name">Nombre comercial</Label>
|
||||
<Input
|
||||
id="org-trade-name"
|
||||
value={values.tradeName}
|
||||
onChange={(event) => updateValue("tradeName", event.target.value)}
|
||||
placeholder="Treisole"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-industry">Sector / Giro</Label>
|
||||
<Input
|
||||
id="org-industry"
|
||||
value={values.industry}
|
||||
onChange={(event) => updateValue("industry", event.target.value)}
|
||||
placeholder="Alimentos y Bebidas"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-state">Estado</Label>
|
||||
<Input
|
||||
id="org-state"
|
||||
value={values.operatingState}
|
||||
onChange={(event) => updateValue("operatingState", event.target.value)}
|
||||
placeholder="Nuevo Leon"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-municipality">Municipio</Label>
|
||||
<Input
|
||||
id="org-municipality"
|
||||
value={values.municipality}
|
||||
onChange={(event) => updateValue("municipality", event.target.value)}
|
||||
placeholder="Monterrey"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-size">Tamano de empresa</Label>
|
||||
<select
|
||||
id="org-size"
|
||||
value={values.companySize}
|
||||
onChange={(event) => updateValue("companySize", event.target.value)}
|
||||
className="w-full rounded-lg border border-[#cfd8e6] bg-white px-3 py-2 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
|
||||
>
|
||||
<option value="">Selecciona un tamano</option>
|
||||
{companySizeOptions.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-years">Anos de operacion</Label>
|
||||
<Input
|
||||
id="org-years"
|
||||
value={values.yearsOfOperation}
|
||||
onChange={(event) => updateValue("yearsOfOperation", event.target.value)}
|
||||
placeholder="7"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="org-revenue">Facturacion anual</Label>
|
||||
<select
|
||||
id="org-revenue"
|
||||
value={values.annualRevenueRange}
|
||||
onChange={(event) => updateValue("annualRevenueRange", event.target.value)}
|
||||
className="w-full rounded-lg border border-[#cfd8e6] bg-white px-3 py-2 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
|
||||
>
|
||||
<option value="">Selecciona un rango</option>
|
||||
{annualRevenueRangeOptions.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#2d3c57]">Has participado en licitaciones con gobierno?</p>
|
||||
<div className="mt-2 flex gap-5">
|
||||
<Label className="flex cursor-pointer items-center gap-2 text-sm text-[#1f2a3d]">
|
||||
<input
|
||||
type="radio"
|
||||
name="government-contracts"
|
||||
className="h-4 w-4 accent-[#0f2a5f]"
|
||||
checked={values.hasGovernmentContracts === "yes"}
|
||||
onChange={() => updateValue("hasGovernmentContracts", "yes")}
|
||||
/>
|
||||
Si
|
||||
</Label>
|
||||
<Label className="flex cursor-pointer items-center gap-2 text-sm text-[#1f2a3d]">
|
||||
<input
|
||||
type="radio"
|
||||
name="government-contracts"
|
||||
className="h-4 w-4 accent-[#0f2a5f]"
|
||||
checked={values.hasGovernmentContracts === "no"}
|
||||
onChange={() => updateValue("hasGovernmentContracts", "no")}
|
||||
/>
|
||||
No
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 4 ? (
|
||||
<div className="space-y-3 rounded-xl border border-[#dde5f2] bg-[#fdfefe] p-4 text-sm text-[#52617b]">
|
||||
<p className="font-semibold text-[#2b3a54]">Confirmacion final</p>
|
||||
<p>
|
||||
<span className="font-semibold">Acta cargada:</span> {actaReady ? "Si" : "No"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Razon social:</span> {values.name || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Nombre comercial:</span> {values.tradeName || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">RFC:</span> {values.rfc || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Representante legal:</span> {values.legalRepresentative || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Tamano de empresa:</span> {values.companySize || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Facturacion anual:</span> {values.annualRevenueRange || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Licitaciones con gobierno:</span>{" "}
|
||||
{values.hasGovernmentContracts ? (values.hasGovernmentContracts === "yes" ? "Si" : "No") : "-"}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{errorMessage ? (
|
||||
<p className="rounded-lg border border-[#f6d0d0] bg-[#fff3f3] px-3 py-2 text-sm text-[#9d3030]">{errorMessage}</p>
|
||||
) : null}
|
||||
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button variant="ghost" onClick={goBack} disabled={step === 1 || isSubmitting || isUploadingActa}>
|
||||
Atras
|
||||
</Button>
|
||||
|
||||
{step < steps.length ? (
|
||||
<Button onClick={goNext} disabled={isSubmitting || isUploadingActa}>
|
||||
Siguiente
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting || isUploadingActa}>
|
||||
{isSubmitting ? "Guardando..." : "Finalizar y continuar al diagnostico"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user