1567 lines
63 KiB
TypeScript
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>
|
|
);
|
|
}
|