initial push

This commit is contained in:
Marcelo Dares
2026-03-15 15:03:56 +01:00
parent d48b9d5352
commit 65aaf9275e
146 changed files with 70245 additions and 100 deletions

View 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>
);
}