Files
Kontia/src/components/app/strategic-diagnostic-wizard.tsx
Marcelo Dares ea23136288 changes
2026-04-29 01:15:50 +02:00

1567 lines
63 KiB
TypeScript

"use client";
import Link from "next/link";
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
CONTRACT_TYPE_OPTIONS,
ORGANIZATION_STRUCTURE_OPTIONS,
STRATEGIC_EVIDENCE_CATEGORY_OPTIONS,
TEAM_AVAILABILITY_OPTIONS,
type StrategicDiagnosticData,
type StrategicDiagnosticScores,
type StrategicEvidenceDocumentView,
type StrategicSectionKey,
} from "@/lib/strategic-diagnostic/types";
type StrategicDiagnosticWizardProps = {
initialData: StrategicDiagnosticData;
initialScores: StrategicDiagnosticScores;
initialEvidenceBySection: Record<StrategicSectionKey, StrategicEvidenceDocumentView[]>;
initialCompletedAt: string | null;
};
type TabKey = StrategicSectionKey | "results";
type StrategicAiInsights = {
sectionGaps: {
sectionKey: StrategicSectionKey;
gap: string;
impact: string;
urgency: "alta" | "media" | "baja";
}[];
priorityActions: {
title: string;
description: string;
priority: "alta" | "media" | "baja";
ownerSuggestion: string;
targetDateSuggestion: string;
}[];
suggestedEvidence: {
sectionKey: StrategicSectionKey;
category: string;
reason: string;
}[];
suggestedFieldValues: {
sectionKey: StrategicSectionKey;
fieldPath: string;
suggestedValue: string;
rationale: string;
}[];
confidence: "low" | "medium" | "high";
};
const tabItems: { key: TabKey; label: string }[] = [
{ key: "technical", label: "Capacidades Tecnicas" },
{ key: "experience", label: "Experiencia" },
{ key: "organization", label: "Organizacion" },
{ key: "publicProcurement", label: "Contratacion Publica" },
{ key: "results", label: "Resultados" },
];
function getKnowledgeLabel(level: number) {
if (level <= 1) {
return "Muy bajo (1/5)";
}
if (level === 2) {
return "Bajo (2/5)";
}
if (level === 3) {
return "Medio (3/5)";
}
if (level === 4) {
return "Alto (4/5)";
}
return "Muy alto (5/5)";
}
function ToggleCard({
title,
description,
checked,
onChange,
}: {
title: string;
description?: string;
checked: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<div className="flex items-center justify-between gap-3 rounded-xl border border-[#d0d9e6] bg-white px-4 py-3">
<div>
<p className="text-lg font-semibold text-[#1b2a46]">{title}</p>
{description ? <p className="text-sm text-[#5f6f8a]">{description}</p> : null}
</div>
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={`relative h-8 w-14 rounded-full border transition-colors ${
checked ? "border-[#0f2a5f] bg-[#0f2a5f]" : "border-[#cbd4e1] bg-[#e2e7f0]"
}`}
>
<span
className={`absolute top-1 h-6 w-6 rounded-full bg-white transition-transform ${
checked ? "translate-x-7" : "translate-x-1"
}`}
/>
</button>
</div>
);
}
function Chip({
label,
removable,
onRemove,
selected,
onClick,
}: {
label: string;
removable?: boolean;
onRemove?: () => void;
selected?: boolean;
onClick?: () => void;
}) {
return (
<span
className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-sm font-semibold ${
selected ? "border-[#0f2a5f] bg-[#102c65] text-white" : "border-[#c7d1df] bg-white text-[#1e2b45]"
}`}
>
<button type="button" className="cursor-pointer" onClick={onClick}>
{label}
</button>
{removable ? (
<button type="button" className="text-xs opacity-80 hover:opacity-100" onClick={onRemove}>
x
</button>
) : null}
</span>
);
}
function SectionScoreBar({ title, subtitle, score }: { title: string; subtitle: string; score: number }) {
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-2xl font-semibold text-[#1b2a45]">{title}</p>
<p className="rounded-full bg-[#eef3ff] px-3 py-1 text-sm font-semibold text-[#18356f]">{Math.round(score)}%</p>
</div>
<div className="relative h-2 overflow-hidden rounded-full bg-[#2ead80]">
<div className="h-full bg-[#1b2f66]" style={{ width: `${Math.max(0, Math.min(100, score))}%` }} />
</div>
<p className="text-sm text-[#637490]">{subtitle}</p>
</div>
);
}
function setNestedValue(target: unknown, pathSegments: string[], value: string): unknown {
if (!pathSegments.length || !target || typeof target !== "object" || Array.isArray(target)) {
return target;
}
const [head, ...tail] = pathSegments;
const record = target as Record<string, unknown>;
if (!(head in record)) {
return target;
}
if (tail.length === 0) {
if (typeof record[head] !== "string") {
return target;
}
return {
...record,
[head]: value,
};
}
const nextValue = setNestedValue(record[head], tail, value);
if (nextValue === record[head]) {
return target;
}
return {
...record,
[head]: nextValue,
};
}
export function StrategicDiagnosticWizard({
initialData,
initialScores,
initialEvidenceBySection,
initialCompletedAt,
}: StrategicDiagnosticWizardProps) {
const [activeTab, setActiveTab] = useState<TabKey>("technical");
const [data, setData] = useState<StrategicDiagnosticData>(initialData);
const [scores, setScores] = useState<StrategicDiagnosticScores>(initialScores);
const [evidenceBySection, setEvidenceBySection] = useState(initialEvidenceBySection);
const [completedAt, setCompletedAt] = useState<string | null>(initialCompletedAt);
const [isSaving, setIsSaving] = useState(false);
const [savingSection, setSavingSection] = useState<TabKey | null>(null);
const [uploadingSection, setUploadingSection] = useState<StrategicSectionKey | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [coverageInput, setCoverageInput] = useState("");
const [certificationInput, setCertificationInput] = useState("");
const [sectorsServedInput, setSectorsServedInput] = useState("");
const [selectedEvidenceCategory, setSelectedEvidenceCategory] = useState<Record<StrategicSectionKey, string>>({
technical: STRATEGIC_EVIDENCE_CATEGORY_OPTIONS.technical[0] ?? "Otro",
experience: STRATEGIC_EVIDENCE_CATEGORY_OPTIONS.experience[0] ?? "Otro",
organization: STRATEGIC_EVIDENCE_CATEGORY_OPTIONS.organization[0] ?? "Otro",
publicProcurement: STRATEGIC_EVIDENCE_CATEGORY_OPTIONS.publicProcurement[0] ?? "Otro",
});
const [aiInsights, setAiInsights] = useState<StrategicAiInsights | null>(null);
const [aiSuggestionId, setAiSuggestionId] = useState<string | null>(null);
const [isLoadingAiInsights, setIsLoadingAiInsights] = useState(false);
const [isApplyingAiField, setIsApplyingAiField] = useState<string | null>(null);
const progressPercent = useMemo(() => {
return Math.round((scores.completedSections / scores.totalSections) * 100);
}, [scores.completedSections, scores.totalSections]);
function clearFeedback() {
setErrorMessage(null);
setSuccessMessage(null);
}
function updateSection<K extends keyof StrategicDiagnosticData>(
section: K,
updater: (value: StrategicDiagnosticData[K]) => StrategicDiagnosticData[K],
) {
setData((previous) => ({
...previous,
[section]: updater(previous[section]),
}));
}
function nextTab(current: TabKey) {
const index = tabItems.findIndex((item) => item.key === current);
return tabItems[index + 1]?.key ?? "results";
}
async function setAiDecision(decision: "accept" | "dismiss") {
if (!aiSuggestionId) {
return;
}
await fetch(`/api/ai/suggestions/${encodeURIComponent(aiSuggestionId)}/decision`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ decision }),
});
}
async function persistData(section: TabKey, forceCompleted = false) {
setIsSaving(true);
setSavingSection(section);
clearFeedback();
try {
const response = await fetch("/api/strategic-diagnostic", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
data,
forceCompleted,
}),
});
const payload = (await response.json().catch(() => ({}))) as {
ok?: boolean;
error?: string;
payload?: {
data: StrategicDiagnosticData;
scores: StrategicDiagnosticScores;
evidenceBySection: Record<StrategicSectionKey, StrategicEvidenceDocumentView[]>;
completedAt: string | null;
};
};
if (!response.ok || !payload.ok || !payload.payload) {
setErrorMessage(payload.error ?? "No se pudo guardar esta seccion.");
return false;
}
setData(payload.payload.data);
setScores(payload.payload.scores);
setEvidenceBySection(payload.payload.evidenceBySection);
setCompletedAt(payload.payload.completedAt);
setSuccessMessage("Seccion guardada correctamente.");
return true;
} catch {
setErrorMessage("No se pudo guardar esta seccion.");
return false;
} finally {
setIsSaving(false);
setSavingSection(null);
}
}
async function saveAndContinue(section: TabKey, forceCompleted = false) {
const saved = await persistData(section, forceCompleted);
if (saved) {
setActiveTab(nextTab(section));
}
}
async function uploadEvidence(section: StrategicSectionKey, file: File) {
setUploadingSection(section);
clearFeedback();
try {
const formData = new FormData();
formData.append("file", file);
formData.append("section", section);
formData.append("category", selectedEvidenceCategory[section]);
const response = await fetch("/api/strategic-diagnostic/evidence", {
method: "POST",
body: formData,
});
const payload = (await response.json().catch(() => ({}))) as {
ok?: boolean;
error?: string;
payload?: {
data: StrategicDiagnosticData;
scores: StrategicDiagnosticScores;
evidenceBySection: Record<StrategicSectionKey, StrategicEvidenceDocumentView[]>;
completedAt: string | null;
};
};
if (!response.ok || !payload.ok || !payload.payload) {
setErrorMessage(payload.error ?? "No se pudo subir la evidencia.");
return;
}
setData(payload.payload.data);
setScores(payload.payload.scores);
setEvidenceBySection(payload.payload.evidenceBySection);
setCompletedAt(payload.payload.completedAt);
setSuccessMessage("Evidencia subida correctamente.");
} catch {
setErrorMessage("No se pudo subir la evidencia.");
} finally {
setUploadingSection(null);
}
}
async function generateAiInsights() {
setIsLoadingAiInsights(true);
clearFeedback();
try {
const response = await fetch("/api/strategic-diagnostic/ai/insights", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
data,
evidenceMetadata: {
technical: {
count: evidenceBySection.technical.length,
categories: Array.from(new Set(evidenceBySection.technical.map((item) => item.category))),
},
experience: {
count: evidenceBySection.experience.length,
categories: Array.from(new Set(evidenceBySection.experience.map((item) => item.category))),
},
organization: {
count: evidenceBySection.organization.length,
categories: Array.from(new Set(evidenceBySection.organization.map((item) => item.category))),
},
publicProcurement: {
count: evidenceBySection.publicProcurement.length,
categories: Array.from(new Set(evidenceBySection.publicProcurement.map((item) => item.category))),
},
},
}),
});
const payload = (await response.json().catch(() => ({}))) as {
ok?: boolean;
error?: string;
sectionGaps?: StrategicAiInsights["sectionGaps"];
priorityActions?: StrategicAiInsights["priorityActions"];
suggestedEvidence?: StrategicAiInsights["suggestedEvidence"];
suggestedFieldValues?: StrategicAiInsights["suggestedFieldValues"];
confidence?: StrategicAiInsights["confidence"];
suggestionId?: string;
};
if (!response.ok || !payload.ok) {
setErrorMessage(payload.error ?? "No fue posible generar plan IA.");
return;
}
setAiInsights({
sectionGaps: payload.sectionGaps ?? [],
priorityActions: payload.priorityActions ?? [],
suggestedEvidence: payload.suggestedEvidence ?? [],
suggestedFieldValues: payload.suggestedFieldValues ?? [],
confidence: payload.confidence ?? "low",
});
setAiSuggestionId(payload.suggestionId ?? null);
setSuccessMessage("Plan sugerido por IA generado.");
} catch {
setErrorMessage("No fue posible generar plan IA.");
} finally {
setIsLoadingAiInsights(false);
}
}
async function applySuggestedFieldValue(fieldPath: string, value: string) {
const segments = fieldPath.split(".").map((segment) => segment.trim()).filter(Boolean);
if (!segments.length) {
return;
}
setIsApplyingAiField(fieldPath);
clearFeedback();
try {
let applied = false;
setData((previous) => {
const nextValue = setNestedValue(previous, segments, value);
applied = nextValue !== previous;
return (nextValue as StrategicDiagnosticData) ?? previous;
});
if (!applied) {
setErrorMessage("Este campo no acepta aplicacion directa desde IA. Aplica manualmente.");
return;
}
await setAiDecision("accept");
setSuccessMessage(`Valor sugerido aplicado: ${fieldPath}.`);
} catch {
setErrorMessage("No fue posible aplicar el valor sugerido.");
} finally {
setIsApplyingAiField(null);
}
}
function addListItem(section: "technical" | "experience", field: "coverageRegions" | "certifications" | "sectorsServed", rawValue: string) {
const value = rawValue.trim();
if (!value) {
return;
}
if (section === "technical") {
updateSection("technical", (previous) => {
const current = previous[field as "coverageRegions" | "certifications"];
return current.includes(value) ? previous : { ...previous, [field]: [...current, value] };
});
return;
}
updateSection("experience", (previous) => {
const current = previous[field as "sectorsServed"];
return current.includes(value) ? previous : { ...previous, sectorsServed: [...current, value] };
});
}
return (
<div className="space-y-6">
<Card>
<CardContent className="space-y-3 py-4">
<div className="flex items-center justify-between">
<p className="text-lg font-semibold text-[#1f2a40]">Progreso del diagnostico</p>
<p className="text-lg font-semibold text-[#1f2a40]">
{scores.completedSections} de {scores.totalSections} secciones completadas
</p>
</div>
<div className="h-2 rounded-full bg-[#d7dde8]">
<div className="h-2 rounded-full bg-[#28a673] transition-all" style={{ width: `${progressPercent}%` }} />
</div>
</CardContent>
</Card>
<div className="flex flex-wrap gap-2 rounded-xl border border-[#dce3ef] bg-[#f2f4f8] p-2">
{tabItems.map((tab) => (
<button
key={tab.key}
type="button"
onClick={() => setActiveTab(tab.key)}
className={`rounded-lg px-4 py-2 text-sm font-semibold transition-colors ${
activeTab === tab.key ? "bg-[#0f2a5f] text-white" : "bg-transparent text-[#5a6985] hover:bg-white"
}`}
>
{tab.label}
</button>
))}
</div>
{activeTab === "technical" ? (
<div className="space-y-5">
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Productos y Servicios</h3>
<p className="text-sm text-[#60718e]">Describe detalladamente que ofrece tu empresa</p>
</CardHeader>
<CardContent className="space-y-2">
<Label>Descripcion de productos/servicios</Label>
<textarea
value={data.technical.productsServicesDescription}
onChange={(event) =>
updateSection("technical", (previous) => ({
...previous,
productsServicesDescription: event.target.value,
}))
}
rows={4}
className="w-full rounded-xl border border-[#c9d3e3] px-3 py-2 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: Servicios de consultoria en tecnologia..."
/>
<p className="text-sm text-[#64758f]">Incluye todos los productos y servicios que ofreces al mercado</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Capacidad Instalada</h3>
<p className="text-sm text-[#60718e]">Recursos humanos, equipamiento e infraestructura disponible</p>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Personal disponible (numero de empleados)</Label>
<input
type="number"
value={data.technical.installedCapacityEmployees}
onChange={(event) =>
updateSection("technical", (previous) => ({
...previous,
installedCapacityEmployees: event.target.value,
}))
}
className="mt-1 h-11 w-full rounded-xl border border-[#c9d3e3] px-3 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: 25"
/>
</div>
<div>
<Label>Equipamiento</Label>
<textarea
value={data.technical.equipment}
onChange={(event) =>
updateSection("technical", (previous) => ({
...previous,
equipment: event.target.value,
}))
}
rows={3}
className="mt-1 w-full rounded-xl border border-[#c9d3e3] px-3 py-2 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: 15 equipos de computo, 3 servidores..."
/>
</div>
<div>
<Label>Infraestructura</Label>
<textarea
value={data.technical.infrastructure}
onChange={(event) =>
updateSection("technical", (previous) => ({
...previous,
infrastructure: event.target.value,
}))
}
rows={3}
className="mt-1 w-full rounded-xl border border-[#c9d3e3] px-3 py-2 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: Oficinas de 200m2, bodega..."
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Cobertura Geografica</h3>
</CardHeader>
<CardContent className="space-y-3">
<Label>Estados/regiones donde opera</Label>
<div className="flex gap-2">
<input
value={coverageInput}
onChange={(event) => setCoverageInput(event.target.value)}
className="h-11 flex-1 rounded-xl border border-[#c9d3e3] px-3 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: Ciudad de Mexico"
/>
<Button
onClick={() => {
addListItem("technical", "coverageRegions", coverageInput);
setCoverageInput("");
}}
className="bg-[#21ad76] hover:bg-[#1a9a68]"
>
Agregar
</Button>
</div>
<div className="flex flex-wrap gap-2">
{data.technical.coverageRegions.map((region) => (
<Chip
key={region}
label={region}
removable
onRemove={() =>
updateSection("technical", (previous) => ({
...previous,
coverageRegions: previous.coverageRegions.filter((item) => item !== region),
}))
}
/>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Tiempos de Respuesta</h3>
</CardHeader>
<CardContent>
<Label>Tiempos de entrega tipicos</Label>
<textarea
value={data.technical.responseTimes}
onChange={(event) =>
updateSection("technical", (previous) => ({
...previous,
responseTimes: event.target.value,
}))
}
rows={3}
className="mt-1 w-full rounded-xl border border-[#c9d3e3] px-3 py-2 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: Proyectos pequenos: 2-4 semanas..."
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Certificaciones Tecnicas</h3>
</CardHeader>
<CardContent className="space-y-3">
<Label>Certificaciones y acreditaciones</Label>
<div className="flex gap-2">
<input
value={certificationInput}
onChange={(event) => setCertificationInput(event.target.value)}
className="h-11 flex-1 rounded-xl border border-[#c9d3e3] px-3 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: ISO 9001:2015"
/>
<Button
onClick={() => {
addListItem("technical", "certifications", certificationInput);
setCertificationInput("");
}}
className="bg-[#21ad76] hover:bg-[#1a9a68]"
>
Agregar
</Button>
</div>
<div className="flex flex-wrap gap-2">
{data.technical.certifications.map((certification) => (
<Chip
key={certification}
label={certification}
removable
onRemove={() =>
updateSection("technical", (previous) => ({
...previous,
certifications: previous.certifications.filter((item) => item !== certification),
}))
}
/>
))}
</div>
</CardContent>
</Card>
<Card className="border-dashed">
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Documentos de Evidencia - Capacidades Tecnicas</h3>
<p className="text-sm text-[#60718e]">Sube certificaciones, fichas tecnicas o documentos de respaldo</p>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2">
{STRATEGIC_EVIDENCE_CATEGORY_OPTIONS.technical.map((category) => (
<Chip
key={category}
label={category}
selected={selectedEvidenceCategory.technical === category}
onClick={() =>
setSelectedEvidenceCategory((previous) => ({
...previous,
technical: category,
}))
}
/>
))}
</div>
<label className="inline-flex cursor-pointer items-center gap-2 rounded-xl border border-[#c6d1df] px-3 py-2 text-sm font-semibold text-[#1d2d4b]">
<input
type="file"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) {
void uploadEvidence("technical", file);
}
event.target.value = "";
}}
/>
{uploadingSection === "technical" ? "Subiendo..." : `Subir ${selectedEvidenceCategory.technical}`}
</label>
<p className="text-sm text-[#60718e]">PDF, DOC, DOCX, JPG, PNG (max. 10MB)</p>
<ul className="space-y-2">
{evidenceBySection.technical.map((document) => (
<li key={document.id} className="rounded-lg border border-[#d9e2ee] bg-white px-3 py-2 text-sm text-[#33455f]">
<span className="font-semibold">{document.category}:</span> {document.fileName}
</li>
))}
</ul>
</CardContent>
</Card>
<div className="flex justify-end">
<Button disabled={isSaving} onClick={() => void saveAndContinue("technical")}>
{isSaving && savingSection === "technical" ? "Guardando..." : "Guardar y continuar"}
</Button>
</div>
</div>
) : null}
{activeTab === "experience" ? (
<div className="space-y-5">
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Experiencia por Sector</h3>
<p className="text-sm text-[#60718e]">En que sectores ha trabajado tu empresa?</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 md:grid-cols-2">
<ToggleCard
title="Sector Gobierno"
description="Experiencia con entidades publicas"
checked={data.experience.hasGovernmentSectorExperience}
onChange={(checked) =>
updateSection("experience", (previous) => ({
...previous,
hasGovernmentSectorExperience: checked,
}))
}
/>
<ToggleCard
title="Sector Privado"
description="Experiencia con empresas privadas"
checked={data.experience.hasPrivateSectorExperience}
onChange={(checked) =>
updateSection("experience", (previous) => ({
...previous,
hasPrivateSectorExperience: checked,
}))
}
/>
</div>
<div>
<Label>Sectores atendidos</Label>
<div className="mt-1 flex gap-2">
<input
value={sectorsServedInput}
onChange={(event) => setSectorsServedInput(event.target.value)}
className="h-11 flex-1 rounded-xl border border-[#c9d3e3] px-3 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: Salud, Educacion, Tecnologia"
/>
<Button
onClick={() => {
addListItem("experience", "sectorsServed", sectorsServedInput);
setSectorsServedInput("");
}}
className="bg-[#21ad76] hover:bg-[#1a9a68]"
>
Agregar
</Button>
</div>
<div className="mt-2 flex flex-wrap gap-2">
{data.experience.sectorsServed.map((sector) => (
<Chip
key={sector}
label={sector}
removable
onRemove={() =>
updateSection("experience", (previous) => ({
...previous,
sectorsServed: previous.sectorsServed.filter((item) => item !== sector),
}))
}
/>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Tipos de Contratos</h3>
<p className="text-sm text-[#60718e]">Que tipos de contratos ha manejado tu empresa?</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-2">
{CONTRACT_TYPE_OPTIONS.map((contractType) => {
const selected = data.experience.contractTypes.includes(contractType);
return (
<Chip
key={contractType}
label={contractType}
selected={selected}
onClick={() =>
updateSection("experience", (previous) => ({
...previous,
contractTypes: selected
? previous.contractTypes.filter((item) => item !== contractType)
: [...previous.contractTypes, contractType],
}))
}
/>
);
})}
</div>
<div>
<Label>Montos aproximados de contratos</Label>
<input
value={data.experience.approximateContractAmounts}
onChange={(event) =>
updateSection("experience", (previous) => ({
...previous,
approximateContractAmounts: event.target.value,
}))
}
className="mt-1 h-11 w-full rounded-xl border border-[#c9d3e3] px-3 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: $500,000 - $5,000,000 MXN"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Casos de Exito</h3>
<p className="text-sm text-[#60718e]">Describe proyectos exitosos que demuestren tu experiencia</p>
</CardHeader>
<CardContent>
<Label>Casos de exito destacados</Label>
<textarea
value={data.experience.successCases}
onChange={(event) =>
updateSection("experience", (previous) => ({
...previous,
successCases: event.target.value,
}))
}
rows={4}
className="mt-1 w-full rounded-xl border border-[#c9d3e3] px-3 py-2 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
placeholder="Ej: Implementacion de sistema de gestion para..."
/>
</CardContent>
</Card>
<Card className="border-dashed">
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Documentos de Evidencia - Experiencia</h3>
<p className="text-sm text-[#60718e]">Sube contratos, cartas de recomendacion o constancias de cumplimiento</p>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2">
{STRATEGIC_EVIDENCE_CATEGORY_OPTIONS.experience.map((category) => (
<Chip
key={category}
label={category}
selected={selectedEvidenceCategory.experience === category}
onClick={() =>
setSelectedEvidenceCategory((previous) => ({
...previous,
experience: category,
}))
}
/>
))}
</div>
<label className="inline-flex cursor-pointer items-center gap-2 rounded-xl border border-[#c6d1df] px-3 py-2 text-sm font-semibold text-[#1d2d4b]">
<input
type="file"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) {
void uploadEvidence("experience", file);
}
event.target.value = "";
}}
/>
{uploadingSection === "experience" ? "Subiendo..." : `Subir ${selectedEvidenceCategory.experience}`}
</label>
<p className="text-sm text-[#60718e]">PDF, DOC, DOCX, JPG, PNG (max. 10MB)</p>
<ul className="space-y-2">
{evidenceBySection.experience.map((document) => (
<li key={document.id} className="rounded-lg border border-[#d9e2ee] bg-white px-3 py-2 text-sm text-[#33455f]">
<span className="font-semibold">{document.category}:</span> {document.fileName}
</li>
))}
</ul>
</CardContent>
</Card>
<div className="flex justify-end">
<Button disabled={isSaving} onClick={() => void saveAndContinue("experience")}>
{isSaving && savingSection === "experience" ? "Guardando..." : "Guardar y continuar"}
</Button>
</div>
</div>
) : null}
{activeTab === "organization" ? (
<div className="space-y-5">
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Estructura Organizacional</h3>
<p className="text-sm text-[#60718e]">Define como esta organizada tu empresa</p>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Tipo de estructura</Label>
<select
value={data.organization.structureType}
onChange={(event) =>
updateSection("organization", (previous) => ({
...previous,
structureType: event.target.value,
}))
}
className="mt-1 h-11 w-full rounded-xl border border-[#c9d3e3] px-3 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
>
<option value="">Selecciona el tipo de estructura</option>
{ORGANIZATION_STRUCTURE_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
<div className="space-y-2">
<p className="text-lg font-semibold text-[#1f2a40]">Roles definidos en la organizacion</p>
<div className="grid gap-3 md:grid-cols-2">
<ToggleCard
title="Direccion General"
checked={data.organization.roles.generalManagement}
onChange={(checked) =>
updateSection("organization", (previous) => ({
...previous,
roles: {
...previous.roles,
generalManagement: checked,
},
}))
}
/>
<ToggleCard
title="Area Juridica"
checked={data.organization.roles.legal}
onChange={(checked) =>
updateSection("organization", (previous) => ({
...previous,
roles: {
...previous.roles,
legal: checked,
},
}))
}
/>
<ToggleCard
title="Area Financiera/Contable"
checked={data.organization.roles.finance}
onChange={(checked) =>
updateSection("organization", (previous) => ({
...previous,
roles: {
...previous.roles,
finance: checked,
},
}))
}
/>
<ToggleCard
title="Area de Operaciones"
checked={data.organization.roles.operations}
onChange={(checked) =>
updateSection("organization", (previous) => ({
...previous,
roles: {
...previous.roles,
operations: checked,
},
}))
}
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Cumplimiento Normativo</h3>
<p className="text-sm text-[#60718e]">Estado de cumplimiento regulatorio de la empresa</p>
</CardHeader>
<CardContent className="grid gap-3 md:grid-cols-3">
<ToggleCard
title="Cumplimiento Fiscal"
description="SAT al corriente"
checked={data.organization.compliance.fiscal}
onChange={(checked) =>
updateSection("organization", (previous) => ({
...previous,
compliance: {
...previous.compliance,
fiscal: checked,
},
}))
}
/>
<ToggleCard
title="Cumplimiento Laboral"
description="IMSS, INFONAVIT"
checked={data.organization.compliance.labor}
onChange={(checked) =>
updateSection("organization", (previous) => ({
...previous,
compliance: {
...previous.compliance,
labor: checked,
},
}))
}
/>
<ToggleCard
title="Procesos Documentados"
description="Manuales y politicas"
checked={data.organization.compliance.documentedProcesses}
onChange={(checked) =>
updateSection("organization", (previous) => ({
...previous,
compliance: {
...previous.compliance,
documentedProcesses: checked,
},
}))
}
/>
</CardContent>
</Card>
<Card className="border-dashed">
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Documentos de Evidencia - Organizacion</h3>
<p className="text-sm text-[#60718e]">Sube organigrama, politicas o manuales de procesos</p>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2">
{STRATEGIC_EVIDENCE_CATEGORY_OPTIONS.organization.map((category) => (
<Chip
key={category}
label={category}
selected={selectedEvidenceCategory.organization === category}
onClick={() =>
setSelectedEvidenceCategory((previous) => ({
...previous,
organization: category,
}))
}
/>
))}
</div>
<label className="inline-flex cursor-pointer items-center gap-2 rounded-xl border border-[#c6d1df] px-3 py-2 text-sm font-semibold text-[#1d2d4b]">
<input
type="file"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) {
void uploadEvidence("organization", file);
}
event.target.value = "";
}}
/>
{uploadingSection === "organization" ? "Subiendo..." : `Subir ${selectedEvidenceCategory.organization}`}
</label>
<p className="text-sm text-[#60718e]">PDF, DOC, DOCX, JPG, PNG (max. 10MB)</p>
<ul className="space-y-2">
{evidenceBySection.organization.map((document) => (
<li key={document.id} className="rounded-lg border border-[#d9e2ee] bg-white px-3 py-2 text-sm text-[#33455f]">
<span className="font-semibold">{document.category}:</span> {document.fileName}
</li>
))}
</ul>
</CardContent>
</Card>
<div className="flex justify-end">
<Button disabled={isSaving} onClick={() => void saveAndContinue("organization")}>
{isSaving && savingSection === "organization" ? "Guardando..." : "Guardar y continuar"}
</Button>
</div>
</div>
) : null}
{activeTab === "publicProcurement" ? (
<div className="space-y-5">
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Conocimiento del Proceso de Licitacion</h3>
<p className="text-sm text-[#60718e]">Que tan familiarizado esta tu equipo con los procesos de contratacion publica?</p>
</CardHeader>
<CardContent className="space-y-4">
<div>
<div className="mb-2 flex items-center justify-between">
<Label>Nivel de conocimiento</Label>
<span className="rounded-md bg-[#ef4444] px-2 py-1 text-sm font-semibold text-white">
{getKnowledgeLabel(data.publicProcurement.processKnowledgeLevel)}
</span>
</div>
<input
type="range"
min={1}
max={5}
value={data.publicProcurement.processKnowledgeLevel}
onChange={(event) =>
updateSection("publicProcurement", (previous) => ({
...previous,
processKnowledgeLevel: Number.parseInt(event.target.value, 10),
}))
}
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-[#26a773]"
/>
<p className="mt-2 text-sm text-[#60718e]">1 = Sin experiencia, 5 = Experto en licitaciones</p>
</div>
<ToggleCard
title="Experiencia leyendo bases de licitacion"
description="Han leido y analizado bases de licitacion antes?"
checked={data.publicProcurement.hasTenderBasesExperience}
onChange={(checked) =>
updateSection("publicProcurement", (previous) => ({
...previous,
hasTenderBasesExperience: checked,
}))
}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Capacidad de Preparacion de Propuestas</h3>
</CardHeader>
<CardContent className="space-y-5">
<div>
<div className="mb-2 flex items-center justify-between">
<Label>Capacidad para preparar propuestas tecnicas</Label>
<span className="rounded-md bg-[#ef4444] px-2 py-1 text-sm font-semibold text-white">
{getKnowledgeLabel(data.publicProcurement.technicalProposalCapacityLevel)}
</span>
</div>
<input
type="range"
min={1}
max={5}
value={data.publicProcurement.technicalProposalCapacityLevel}
onChange={(event) =>
updateSection("publicProcurement", (previous) => ({
...previous,
technicalProposalCapacityLevel: Number.parseInt(event.target.value, 10),
}))
}
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-[#26a773]"
/>
<p className="mt-2 text-sm text-[#60718e]">Incluye metodologia, plan de trabajo y curriculums del equipo</p>
</div>
<div>
<div className="mb-2 flex items-center justify-between">
<Label>Capacidad para cumplir requisitos administrativos</Label>
<span className="rounded-md bg-[#ef4444] px-2 py-1 text-sm font-semibold text-white">
{getKnowledgeLabel(data.publicProcurement.administrativeRequirementsCapacityLevel)}
</span>
</div>
<input
type="range"
min={1}
max={5}
value={data.publicProcurement.administrativeRequirementsCapacityLevel}
onChange={(event) =>
updateSection("publicProcurement", (previous) => ({
...previous,
administrativeRequirementsCapacityLevel: Number.parseInt(event.target.value, 10),
}))
}
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-[#26a773]"
/>
<p className="mt-2 text-sm text-[#60718e]">Documentos legales, poderes, opiniones de cumplimiento y fianzas</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Equipo de Licitaciones</h3>
<p className="text-sm text-[#60718e]">Cuentan con personal dedicado a procesos de licitacion?</p>
</CardHeader>
<CardContent className="space-y-4">
<ToggleCard
title="Tienen equipo de licitaciones?"
description="Personal asignado a identificar y preparar licitaciones"
checked={data.publicProcurement.hasBiddingTeam}
onChange={(checked) =>
updateSection("publicProcurement", (previous) => ({
...previous,
hasBiddingTeam: checked,
}))
}
/>
<div>
<Label>Disponibilidad del equipo</Label>
<select
value={data.publicProcurement.biddingTeamAvailability}
onChange={(event) =>
updateSection("publicProcurement", (previous) => ({
...previous,
biddingTeamAvailability: event.target.value,
}))
}
className="mt-1 h-11 w-full rounded-xl border border-[#c9d3e3] px-3 text-sm text-[#1f2a3d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0f2a5f]"
>
{TEAM_AVAILABILITY_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
</CardContent>
</Card>
<Card className="border-dashed">
<CardHeader>
<h3 className="text-3xl font-semibold text-[#162545]">Documentos de Evidencia - Contratacion Publica</h3>
<p className="text-sm text-[#60718e]">Sube propuestas previas, bases analizadas o materiales de capacitacion</p>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2">
{STRATEGIC_EVIDENCE_CATEGORY_OPTIONS.publicProcurement.map((category) => (
<Chip
key={category}
label={category}
selected={selectedEvidenceCategory.publicProcurement === category}
onClick={() =>
setSelectedEvidenceCategory((previous) => ({
...previous,
publicProcurement: category,
}))
}
/>
))}
</div>
<label className="inline-flex cursor-pointer items-center gap-2 rounded-xl border border-[#c6d1df] px-3 py-2 text-sm font-semibold text-[#1d2d4b]">
<input
type="file"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) {
void uploadEvidence("publicProcurement", file);
}
event.target.value = "";
}}
/>
{uploadingSection === "publicProcurement" ? "Subiendo..." : `Subir ${selectedEvidenceCategory.publicProcurement}`}
</label>
<p className="text-sm text-[#60718e]">PDF, DOC, DOCX, JPG, PNG (max. 10MB)</p>
<ul className="space-y-2">
{evidenceBySection.publicProcurement.map((document) => (
<li key={document.id} className="rounded-lg border border-[#d9e2ee] bg-white px-3 py-2 text-sm text-[#33455f]">
<span className="font-semibold">{document.category}:</span> {document.fileName}
</li>
))}
</ul>
</CardContent>
</Card>
<div className="flex justify-end">
<Button disabled={isSaving} onClick={() => void saveAndContinue("publicProcurement", true)}>
{isSaving && savingSection === "publicProcurement" ? "Guardando..." : "Guardar y finalizar"}
</Button>
</div>
</div>
) : null}
{activeTab === "results" ? (
<div className="space-y-5">
<Card>
<CardHeader>
<h3 className="text-4xl font-semibold text-[#162545]">Perfil Competitivo General</h3>
<p className="text-sm text-[#60718e]">Resumen de tu preparacion para participar en licitaciones publicas</p>
</CardHeader>
<CardContent className="grid gap-5 md:grid-cols-2">
<SectionScoreBar
title="Capacidades Tecnicas"
subtitle="Infraestructura, certificaciones y cobertura"
score={scores.sectionScores.technical}
/>
<SectionScoreBar
title="Experiencia"
subtitle="Trayectoria en sector publico y privado"
score={scores.sectionScores.experience}
/>
<SectionScoreBar
title="Organizacion"
subtitle="Estructura, cumplimiento y sistemas"
score={scores.sectionScores.organization}
/>
<SectionScoreBar
title="Contratacion Publica"
subtitle="Conocimiento y preparacion para licitar"
score={scores.sectionScores.publicProcurement}
/>
</CardContent>
</Card>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<h4 className="text-3xl font-semibold text-[#17a56f]">Fortalezas Identificadas</h4>
</CardHeader>
<CardContent className="text-sm text-[#617089]">
{scores.strengths.length ? (
<ul className="space-y-2">
{scores.strengths.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
) : (
<p>Completa todas las secciones para ver fortalezas.</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<h4 className="text-3xl font-semibold text-[#1f2a40]">Brechas por Cerrar</h4>
</CardHeader>
<CardContent className="text-sm text-[#617089]">
{scores.gaps.length ? (
<ul className="space-y-2">
{scores.gaps.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
) : (
<p>No se identifican brechas criticas.</p>
)}
</CardContent>
</Card>
</div>
<Card className="border-[#efc2c2] bg-[#fffdfd]">
<CardHeader>
<h4 className="text-3xl font-semibold text-[#ef4444]">Riesgos Potenciales</h4>
<p className="text-sm text-[#60718e]">Factores que podrian afectar tu participacion en licitaciones</p>
</CardHeader>
<CardContent className="text-sm text-[#617089]">
{scores.risks.length ? (
<ul className="space-y-2">
{scores.risks.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
) : (
<p>No se detectan riesgos inmediatos.</p>
)}
</CardContent>
</Card>
<Card className="border-[#cfdcf4] bg-[#f7f9ff]">
<CardHeader>
<h4 className="text-3xl font-semibold text-[#1b2a46]">Plan sugerido por IA</h4>
<p className="text-sm text-[#60718e]">Asistencia opcional. Nunca se aplican cambios automaticamente.</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
<Button type="button" variant="secondary" disabled={isLoadingAiInsights} onClick={() => void generateAiInsights()}>
{isLoadingAiInsights ? "Generando plan IA..." : "Generar plan IA"}
</Button>
{aiInsights ? (
<Button
type="button"
variant="secondary"
onClick={() => {
void setAiDecision("dismiss");
setAiInsights(null);
setSuccessMessage("Plan IA descartado.");
}}
>
Descartar plan IA
</Button>
) : null}
{aiInsights ? (
<p className="text-xs font-semibold text-[#405578]">Confianza IA: {aiInsights.confidence}</p>
) : null}
</div>
{!aiInsights ? <p className="text-sm text-[#60718e]">Genera un plan IA para ver brechas y acciones priorizadas.</p> : null}
{aiInsights ? (
<div className="space-y-4">
<div>
<p className="text-lg font-semibold text-[#1d335a]">Brechas por seccion</p>
<ul className="mt-2 space-y-2 text-sm text-[#4f6385]">
{aiInsights.sectionGaps.map((item) => (
<li key={`${item.sectionKey}-${item.gap}`} className="rounded-lg border border-[#d8e1ef] bg-white px-3 py-2">
<p className="font-semibold">
{item.sectionKey} · {item.urgency}
</p>
<p>{item.gap}</p>
<p className="text-xs">{item.impact}</p>
</li>
))}
</ul>
</div>
<div>
<p className="text-lg font-semibold text-[#1d335a]">Acciones prioritarias</p>
<ul className="mt-2 space-y-2 text-sm text-[#4f6385]">
{aiInsights.priorityActions.map((item) => (
<li key={`${item.title}-${item.priority}`} className="rounded-lg border border-[#d8e1ef] bg-white px-3 py-2">
<p className="font-semibold">{item.title}</p>
<p>{item.description}</p>
<p className="text-xs">
Prioridad: {item.priority} · Responsable sugerido: {item.ownerSuggestion} · Fecha objetivo: {item.targetDateSuggestion}
</p>
</li>
))}
</ul>
</div>
<div>
<p className="text-lg font-semibold text-[#1d335a]">Campos sugeridos para aplicar</p>
<ul className="mt-2 space-y-2 text-sm text-[#4f6385]">
{aiInsights.suggestedFieldValues.map((item) => (
<li key={`${item.fieldPath}-${item.suggestedValue}`} className="rounded-lg border border-[#d8e1ef] bg-white px-3 py-2">
<p className="font-semibold">{item.fieldPath}</p>
<p className="mt-1 text-xs">{item.rationale}</p>
<p className="mt-1 rounded bg-[#f5f8ff] px-2 py-1 text-xs font-semibold text-[#1d335a]">{item.suggestedValue}</p>
<div className="mt-2">
<Button
type="button"
size="sm"
disabled={isApplyingAiField === item.fieldPath}
onClick={() => void applySuggestedFieldValue(item.fieldPath, item.suggestedValue)}
>
{isApplyingAiField === item.fieldPath ? "Aplicando..." : "Aplicar valor"}
</Button>
</div>
</li>
))}
</ul>
</div>
</div>
) : null}
</CardContent>
</Card>
<Card className="border-[#9bd7bf] bg-[#f6fffb]">
<CardHeader>
<h4 className="text-3xl font-semibold text-[#1b2a46]">Modulo 2 Completado! Siguiente paso recomendado</h4>
<p className="text-sm text-[#60718e]">
Tu perfil competitivo esta listo. El siguiente paso es descubrir oportunidades de licitacion compatibles con tu empresa.
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-xl bg-[#0f2a5f] px-4 py-5 text-center text-white">
<p className="text-3xl font-semibold">Modulo 3: Deteccion de Oportunidades</p>
<p className="mt-1 text-sm text-[#d8e4ff]">Encuentra licitaciones compatibles con tu perfil empresarial</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Link href="/licitations">
<Button variant="secondary">Ir a Modulo 3</Button>
</Link>
<Link href="/normative-analysis">
<Button variant="secondary">Ir a Modulo 4: Analisis Normativo</Button>
</Link>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h4 className="text-3xl font-semibold text-[#1b2a46]">Recomendaciones para Mejorar tu Perfil</h4>
</CardHeader>
<CardContent className="grid gap-3 md:grid-cols-3">
<div className="rounded-xl border border-[#ced7e5] p-4">
<p className="text-lg font-semibold text-[#1b2a46]">1. Completa tu perfil</p>
<p className="mt-1 text-sm text-[#60718e]">Asegurate de llenar todas las secciones del diagnostico.</p>
</div>
<div className="rounded-xl border border-[#ced7e5] p-4">
<p className="text-lg font-semibold text-[#1b2a46]">2. Sube evidencia</p>
<p className="mt-1 text-sm text-[#60718e]">Adjunta certificaciones, contratos y documentos de respaldo.</p>
</div>
<div className="rounded-xl border border-[#ced7e5] p-4">
<p className="text-lg font-semibold text-[#1b2a46]">3. Actua en las brechas</p>
<p className="mt-1 text-sm text-[#60718e]">Trabaja en puntos debiles antes de tu primera licitacion.</p>
</div>
</CardContent>
</Card>
</div>
) : null}
{errorMessage ? <p className="rounded-lg border border-[#f2c6c6] bg-[#fff3f3] px-3 py-2 text-sm text-[#b63d3d]">{errorMessage}</p> : null}
{successMessage ? (
<p className="rounded-lg border border-[#bce4cd] bg-[#edf9f2] px-3 py-2 text-sm text-[#1f7c4d]">
{successMessage}
{completedAt ? ` Ultima finalizacion: ${new Date(completedAt).toLocaleString()}.` : ""}
</p>
) : null}
<p className="text-sm text-[#60718e]">
Puntaje ponderado actual: <span className="font-semibold text-[#1b2a46]">{Math.round(scores.overallScore)}%</span>
</p>
</div>
);
}