+
+ Consumido
+ {spentPercentage}%
+
+
+
90 ? "bg-red-500" : spentPercentage > 70 ? "bg-yellow-500" : "bg-blue-500"
+ }`}
+ style={{ width: `${Math.min(spentPercentage, 100)}%` }}
+ />
+
+
+
+
+ );
+}
+
+// Funciones helper para calcular estadísticas
+function calculateStats(projects: Project[]) {
+ const total = projects.length;
+
+ if (total === 0) {
+ return {
+ total: 0,
+ avgProgress: 0,
+ totalBudget: 0,
+ totalSpent: 0,
+ avgBudget: 0,
+ budgetConsumed: 0,
+ minBudget: 0,
+ maxBudget: 0,
+ uniqueClients: 0,
+ oldestProject: null as string | null,
+ newestProject: null as string | null,
+ avgDuration: 0,
+ };
+ }
+
+ const totalBudget = projects.reduce((acc, p) => acc + p.budget, 0);
+ const totalSpent = projects.reduce((acc, p) => acc + p.spent, 0);
+ const avgProgress = Math.round(projects.reduce((acc, p) => acc + p.progress, 0) / total);
+ const avgBudget = Math.round(totalBudget / total);
+ const budgetConsumed = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0;
+
+ const budgets = projects.map((p) => p.budget);
+ const minBudget = Math.min(...budgets);
+ const maxBudget = Math.max(...budgets);
+
+ const uniqueClients = new Set(projects.map((p) => p.client)).size;
+
+ const dates = projects.map((p) => new Date(p.startDate).getTime()).filter((d) => !isNaN(d));
+ const oldestProject = dates.length > 0 ? new Date(Math.min(...dates)).toLocaleDateString("es-ES") : null;
+ const newestProject = dates.length > 0 ? new Date(Math.max(...dates)).toLocaleDateString("es-ES") : null;
+
+ // Calcular duracion promedio en dias
+ const durations = projects
+ .map((p) => {
+ const start = new Date(p.startDate).getTime();
+ const end = new Date(p.endDate).getTime();
+ if (isNaN(start) || isNaN(end)) return null;
+ return Math.ceil((end - start) / (1000 * 60 * 60 * 24));
+ })
+ .filter((d): d is number => d !== null && d > 0);
+
+ const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
+
+ return {
+ total,
+ avgProgress,
+ totalBudget,
+ totalSpent,
+ avgBudget,
+ budgetConsumed,
+ minBudget,
+ maxBudget,
+ uniqueClients,
+ oldestProject,
+ newestProject,
+ avgDuration,
+ };
+}
+
+// Panel de estadísticas para "Todos los proyectos"
+interface AllProjectsStatsProps {
+ projects: Project[];
+}
+
+function AllProjectsStats({ projects }: AllProjectsStatsProps) {
+ const stats = calculateStats(projects);
+ const activeProjects = projects.filter((p) => p.status !== "2" && p.progress < 100);
+ const completedProjects = projects.filter((p) => p.status === "2" || p.progress >= 100);
+
+ return (
+
+ {/* Cards principales */}
+
+ }
+ highlight
+ />
+ 0 ? Math.round((activeProjects.length / stats.total) * 100) : 0}% del total`}
+ icon={}
+ />
+ 0 ? Math.round((completedProjects.length / stats.total) * 100) : 0}% del total`}
+ icon={}
+ />
+ }
+ />
+
+
+ {/* Segunda fila: Presupuesto */}
+
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+ {/* Tercera fila: Fechas y distribucion */}
+
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+ {/* Cuarta fila: Graficos/Distribuciones */}
+
+
+
+
+
+ {/* Info adicional: Rango de presupuestos */}
+
+
+
+
+
+ Rango de Presupuestos
+
+
+
+
+
+
Minimo
+
{stats.minBudget.toLocaleString("es-ES")}
+
+
+
+
Maximo
+
{stats.maxBudget.toLocaleString("es-ES")}
+
+
+
+
+
+
+
+
+
+ Proyectos por Progreso
+
+
+
+
+
+
+ {projects.filter((p) => p.progress < 25).length}
+
+
0-25%
+
+
+
+ {projects.filter((p) => p.progress >= 25 && p.progress < 50).length}
+
+
25-50%
+
+
+
+ {projects.filter((p) => p.progress >= 50 && p.progress < 75).length}
+
+
50-75%
+
+
+
+ {projects.filter((p) => p.progress >= 75).length}
+
+
75-100%
+
+
+
+
+
+
+ );
+}
+
+// Panel de estadísticas para "Proyectos Activos"
+interface ActiveProjectsStatsProps {
+ projects: Project[];
+}
+
+function ActiveProjectsStats({ projects }: ActiveProjectsStatsProps) {
+ // Filtrar solo proyectos activos (no cerrados y no completados al 100%)
+ const activeProjects = projects.filter((p) => p.status !== "2" && p.progress < 100);
+ const stats = calculateStats(activeProjects);
+
+ if (activeProjects.length === 0) {
+ return (
+
+
+
No hay proyectos activos
+
Todos los proyectos estan completados o cerrados
+
+ );
+ }
+
+ // Proyectos que necesitan atencion (bajo progreso o alto consumo de presupuesto)
+ const needsAttention = activeProjects.filter((p) => {
+ const spentPercentage = p.budget > 0 ? (p.spent / p.budget) * 100 : 0;
+ return p.progress < 25 || spentPercentage > 80;
+ });
+
+ // Proyectos proximos a terminar
+ const nearCompletion = activeProjects.filter((p) => p.progress >= 75);
+
+ return (
+
+ {/* Cards principales */}
+
+ }
+ highlight
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+ {/* Alertas */}
+
+
+
+
+
+ Requieren Atencion
+
+
+
+ {needsAttention.length}
+
+ Proyectos con bajo progreso o alto consumo de presupuesto
+
+
+
+
+
+
+
+
+ Proximos a Completar
+
+
+
+ {nearCompletion.length}
+
+ Proyectos con 75% o mas de progreso
+
+
+
+
+
+ {/* Estadísticas adicionales */}
+
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+ {/* Distribucion y Budget */}
+
+
+
+
+
+ {/* Proyectos por progreso */}
+
+
+
+
+ Distribucion por Progreso
+
+
+
+
+
+
+ {activeProjects.filter((p) => p.progress < 25).length}
+
+
Inicio (0-25%)
+
+
+
+ {activeProjects.filter((p) => p.progress >= 25 && p.progress < 50).length}
+
+
En curso (25-50%)
+
+
+
+ {activeProjects.filter((p) => p.progress >= 50 && p.progress < 75).length}
+
+
Avanzado (50-75%)
+
+
+
+ {activeProjects.filter((p) => p.progress >= 75).length}
+
+
Casi listo (75-99%)
+
+
+
+
+
+ );
+}
+
+// Panel de estadísticas para "Proyectos Completados"
+interface CompletedProjectsStatsProps {
+ projects: Project[];
+}
+
+function CompletedProjectsStats({ projects }: CompletedProjectsStatsProps) {
+ // Filtrar proyectos completados (cerrados o 100% progreso)
+ const completedProjects = projects.filter((p) => p.status === "2" || p.progress >= 100);
+ const stats = calculateStats(completedProjects);
+
+ if (completedProjects.length === 0) {
+ return (
+
+
+
No hay proyectos completados
+
Aun no se ha completado ningun proyecto
+
+ );
+ }
+
+ // Proyectos que terminaron dentro del presupuesto
+ const withinBudget = completedProjects.filter((p) => p.spent <= p.budget);
+ const overBudget = completedProjects.filter((p) => p.spent > p.budget);
+
+ // Eficiencia promedio (presupuesto restante / presupuesto total)
+ const avgEfficiency =
+ completedProjects.length > 0
+ ? Math.round(
+ completedProjects.reduce((acc, p) => {
+ if (p.budget === 0) return acc;
+ return acc + ((p.budget - p.spent) / p.budget) * 100;
+ }, 0) / completedProjects.length
+ )
+ : 0;
+
+ return (
+
+ {/* Cards principales */}
+
+ }
+ highlight
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+ {/* Cumplimiento de presupuesto */}
+
+
+
+
+
+ Dentro del Presupuesto
+
+
+
+ {withinBudget.length}
+
+ {stats.total > 0 ? Math.round((withinBudget.length / stats.total) * 100) : 0}% de los proyectos completados
+
+
+
+
+
+
+
+
+ Excedieron Presupuesto
+
+
+
+ {overBudget.length}
+
+ {stats.total > 0 ? Math.round((overBudget.length / stats.total) * 100) : 0}% de los proyectos completados
+
+
+
+
+
+ {/* Estadísticas adicionales */}
+
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+ {/* Distribucion y Budget */}
+
+
+
+
+
+
+
+ Rango de Presupuestos Completados
+
+
+
+
+
+
Minimo
+
{stats.minBudget.toLocaleString("es-ES")}
+
+
+
+
Maximo
+
{stats.maxBudget.toLocaleString("es-ES")}
+
+
+
+
+
+
+ {/* Timeline */}
+
+
+
+
+ Rango de Fechas
+
+
+
+
+
+
Primer Proyecto
+
{stats.oldestProject || "N/A"}
+
+
+
+
Ultimo Proyecto
+
{stats.newestProject || "N/A"}
+
+
+
+
+
+ );
+}
+
+// Componente de skeleton para carga
+function StatisticsSkeleton() {
+ return (
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
+
+// Componente principal de la página
+export default function StatisticsPage() {
+ const [projects, setProjects] = useState
([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ async function loadProjects() {
+ try {
+ setLoading(true);
+ const data = await getProjects();
+ setProjects(data);
+ setError(null);
+ } catch (err) {
+ console.error("Error loading projects:", err);
+ setError("Error al cargar los proyectos");
+ } finally {
+ setLoading(false);
+ }
+ }
+ loadProjects();
+ }, []);
+
+ if (error) {
+ return (
+
+
+
+
{error}
+
+
+
+ );
+ }
+
+ const activeCount = projects.filter((p) => p.status !== "2" && p.progress < 100).length;
+ const completedCount = projects.filter((p) => p.status === "2" || p.progress >= 100).length;
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
Estadisticas
+
+ Analisis detallado de {projects.length} proyectos
+
+
+
+
+
+
+
+ {/* Main content */}
+
+ {loading ? (
+
+ ) : (
+
+
+
+
+ Todos
+
+ {projects.length}
+
+
+
+
+ Activos
+
+ {activeCount}
+
+
+
+
+ Completados
+
+ {completedCount}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
new file mode 100644
index 0000000..26eb109
--- /dev/null
+++ b/components/ui/tabs.tsx
@@ -0,0 +1,55 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/package-lock.json b/package-lock.json
index 2175811..28724ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -21,6 +22,7 @@
"next-themes": "^0.4.6",
"react": "19.2.0",
"react-dom": "19.2.0",
+ "recharts": "^3.7.0",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
@@ -2262,6 +2264,92 @@
}
}
},
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
@@ -2561,6 +2649,42 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
+ "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2568,6 +2692,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -2873,6 +3009,69 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -2924,6 +3123,12 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz",
@@ -4156,6 +4361,127 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -4235,6 +4561,12 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -4539,6 +4871,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
+ "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -4986,6 +5328,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5481,6 +5829,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -5523,6 +5881,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -7206,9 +7573,31 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-remove-scroll": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
@@ -7299,6 +7688,51 @@
"node": ">=8.10.0"
}
},
+ "node_modules/recharts": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz",
+ "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "1.x.x || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -7343,6 +7777,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -8057,6 +8497,12 @@
"node": ">=0.8"
}
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -8454,6 +8900,28 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 5e8afe9..abfeb4d 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -25,6 +26,7 @@
"next-themes": "^0.4.6",
"react": "19.2.0",
"react-dom": "19.2.0",
+ "recharts": "^3.7.0",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
diff --git a/scripts/seed-projects-direct.js b/scripts/seed-projects-direct.js
new file mode 100644
index 0000000..da8877f
--- /dev/null
+++ b/scripts/seed-projects-direct.js
@@ -0,0 +1,385 @@
+#!/usr/bin/env node
+
+/**
+ * Script para crear proyectos de prueba en Dolibarr llamando directamente a la API
+ * NO requiere que Next.js esté corriendo
+ *
+ * Uso: node scripts/seed-projects-direct.js
+ */
+
+// Configuración - lee de .env.local o usa valores por defecto
+const fs = require('fs');
+const path = require('path');
+
+// Leer .env.local
+function loadEnv() {
+ const envPath = path.join(__dirname, '..', '.env.local');
+ const env = {};
+
+ try {
+ const content = fs.readFileSync(envPath, 'utf8');
+ content.split('\n').forEach(line => {
+ const [key, ...valueParts] = line.split('=');
+ if (key && valueParts.length > 0) {
+ env[key.trim()] = valueParts.join('=').trim();
+ }
+ });
+ } catch (e) {
+ console.error('Error leyendo .env.local:', e.message);
+ }
+
+ return env;
+}
+
+const env = loadEnv();
+const DOLIBARR_API_URL = env.NEXT_PUBLIC_API_URL || 'http://localhost:8200/api/index.php';
+const DOLIBARR_API_KEY = env.NEXT_PUBLIC_DOLIBARR_API_KEY || '';
+
+if (!DOLIBARR_API_KEY) {
+ console.error('❌ ERROR: No se encontró NEXT_PUBLIC_DOLIBARR_API_KEY en .env.local');
+ process.exit(1);
+}
+
+// Nombres de clientes para asignar aleatoriamente
+const clientNames = [
+ "TechCorp Solutions",
+ "Innovatech S.L.",
+ "Digital Factory",
+ "CloudBase Systems",
+ "DataPrime Analytics",
+ "WebMasters Pro",
+ "AppDev Studio",
+ "CyberTech Security",
+ "SmartBiz Solutions",
+ "NextGen Software"
+];
+
+// Datos de proyectos de ejemplo con variedad de estados y presupuestos
+const projectsData = [
+ {
+ title: "Desarrollo de App Movil",
+ ref: "PROJ-2025-001",
+ description: "Aplicacion movil para gestion de inventario con funcionalidades de escaneo QR y sincronizacion en tiempo real.",
+ opp_amount: 45000,
+ opp_percent: 65,
+ date_start: Math.floor(new Date('2025-01-15').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-06-30').getTime() / 1000),
+ },
+ {
+ title: "Rediseno Web Corporativo",
+ ref: "PROJ-2025-002",
+ description: "Renovacion completa del sitio web corporativo con nuevo diseno responsive y optimizacion SEO.",
+ opp_amount: 28000,
+ opp_percent: 90,
+ date_start: Math.floor(new Date('2024-11-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-02-28').getTime() / 1000),
+ },
+ {
+ title: "Sistema de Facturacion Automatica",
+ ref: "PROJ-2025-003",
+ description: "Desarrollo de sistema automatizado para generacion y envio de facturas con integracion a contabilidad.",
+ opp_amount: 62000,
+ opp_percent: 35,
+ date_start: Math.floor(new Date('2025-02-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-08-31').getTime() / 1000),
+ },
+ {
+ title: "Migracion a Cloud AWS",
+ ref: "PROJ-2025-004",
+ description: "Migracion completa de infraestructura on-premise a AWS con configuracion de alta disponibilidad.",
+ opp_amount: 85000,
+ opp_percent: 20,
+ date_start: Math.floor(new Date('2025-03-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-12-31').getTime() / 1000),
+ },
+ {
+ title: "Dashboard Analitico BI",
+ ref: "PROJ-2025-005",
+ description: "Creacion de dashboard de Business Intelligence para analisis de ventas y KPIs en tiempo real.",
+ opp_amount: 38000,
+ opp_percent: 100,
+ date_start: Math.floor(new Date('2024-09-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2024-12-15').getTime() / 1000),
+ status: 2, // Cerrado
+ },
+ {
+ title: "API REST Microservicios",
+ ref: "PROJ-2025-006",
+ description: "Desarrollo de arquitectura de microservicios con API REST para integracion de sistemas legacy.",
+ opp_amount: 72000,
+ opp_percent: 50,
+ date_start: Math.floor(new Date('2025-01-20').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-07-31').getTime() / 1000),
+ },
+ {
+ title: "App de Gestion de Proyectos",
+ ref: "PROJ-2025-007",
+ description: "Plataforma web para gestion agil de proyectos con tableros Kanban, Gantt y reporting automatico.",
+ opp_amount: 55000,
+ opp_percent: 75,
+ date_start: Math.floor(new Date('2024-12-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-05-31').getTime() / 1000),
+ },
+ {
+ title: "E-commerce con Marketplace",
+ ref: "PROJ-2025-008",
+ description: "Plataforma de e-commerce completa con funcionalidad de marketplace multivendedor y pasarela de pagos.",
+ opp_amount: 120000,
+ opp_percent: 15,
+ date_start: Math.floor(new Date('2025-04-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2026-03-31').getTime() / 1000),
+ },
+ {
+ title: "Sistema CRM Personalizado",
+ ref: "PROJ-2025-009",
+ description: "CRM a medida con automatizacion de marketing, gestion de leads y pipeline de ventas.",
+ opp_amount: 48000,
+ opp_percent: 0,
+ date_start: Math.floor(new Date('2025-05-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-11-30').getTime() / 1000),
+ },
+ {
+ title: "Portal de Empleados",
+ ref: "PROJ-2025-010",
+ description: "Portal interno para empleados con gestion de vacaciones, nominas, formacion y comunicacion interna.",
+ opp_amount: 35000,
+ opp_percent: 100,
+ date_start: Math.floor(new Date('2024-10-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2024-12-31').getTime() / 1000),
+ status: 2, // Cerrado
+ },
+ {
+ title: "Sistema de Reservas Online",
+ ref: "PROJ-2025-011",
+ description: "Plataforma de reservas con calendario interactivo, confirmacion automatica y pasarela de pago.",
+ opp_amount: 42000,
+ opp_percent: 45,
+ date_start: Math.floor(new Date('2025-02-15').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-07-15').getTime() / 1000),
+ },
+ {
+ title: "App de Formacion Online",
+ ref: "PROJ-2025-012",
+ description: "Plataforma LMS para formacion online con seguimiento de progreso, certificados y evaluaciones.",
+ opp_amount: 68000,
+ opp_percent: 30,
+ date_start: Math.floor(new Date('2025-03-10').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-10-31').getTime() / 1000),
+ },
+ {
+ title: "Automatizacion de Procesos RPA",
+ ref: "PROJ-2025-013",
+ description: "Implementacion de robots de automatizacion para procesos administrativos repetitivos.",
+ opp_amount: 95000,
+ opp_percent: 10,
+ date_start: Math.floor(new Date('2025-06-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2026-02-28').getTime() / 1000),
+ },
+ {
+ title: "Plataforma IoT Industrial",
+ ref: "PROJ-2025-014",
+ description: "Sistema de monitoreo IoT para maquinaria industrial con alertas predictivas y mantenimiento.",
+ opp_amount: 150000,
+ opp_percent: 5,
+ date_start: Math.floor(new Date('2025-07-01').getTime() / 1000),
+ date_end: Math.floor(new Date('2026-06-30').getTime() / 1000),
+ },
+ {
+ title: "Chatbot con IA",
+ ref: "PROJ-2025-015",
+ description: "Desarrollo de chatbot inteligente para atencion al cliente con procesamiento de lenguaje natural.",
+ opp_amount: 32000,
+ opp_percent: 80,
+ date_start: Math.floor(new Date('2024-11-15').getTime() / 1000),
+ date_end: Math.floor(new Date('2025-03-15').getTime() / 1000),
+ },
+];
+
+/**
+ * Llamar a la API de Dolibarr directamente
+ */
+async function dolibarrFetch(endpoint, options = {}) {
+ const url = `${DOLIBARR_API_URL}/${endpoint}`;
+
+ const response = await fetch(url, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'DOLAPIKEY': DOLIBARR_API_KEY,
+ ...options.headers,
+ },
+ });
+
+ if (!response.ok) {
+ const text = await response.text();
+ throw new Error(`HTTP ${response.status}: ${text}`);
+ }
+
+ return response.json();
+}
+
+/**
+ * Crear un proyecto
+ */
+async function createProject(projectData) {
+ return dolibarrFetch('projects', {
+ method: 'POST',
+ body: JSON.stringify(projectData),
+ });
+}
+
+/**
+ * Validar proyecto (cambiar estado a abierto)
+ */
+async function validateProject(projectId) {
+ try {
+ await dolibarrFetch(`projects/${projectId}/validate`, {
+ method: 'POST',
+ body: JSON.stringify({ notrigger: 0 }),
+ });
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+/**
+ * Cerrar proyecto
+ */
+async function closeProject(projectId) {
+ try {
+ await dolibarrFetch(`projects/${projectId}/close`, {
+ method: 'POST',
+ body: JSON.stringify({ notrigger: 0 }),
+ });
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+/**
+ * Script principal
+ */
+async function main() {
+ console.log('\n🚀 SEED DE PROYECTOS DE PRUEBA PARA DOLIBARR\n');
+ console.log('═'.repeat(60));
+ console.log(`📡 API Dolibarr: ${DOLIBARR_API_URL}`);
+ console.log(`🔑 API Key: ${DOLIBARR_API_KEY.substring(0, 10)}...`);
+ console.log(`📊 Proyectos a crear: ${projectsData.length}`);
+ console.log('═'.repeat(60));
+ console.log('');
+
+ // Verificar conexión con Dolibarr
+ console.log('🔍 Verificando conexion con Dolibarr...');
+ try {
+ await dolibarrFetch('status');
+ console.log('✅ Conexion exitosa con Dolibarr\n');
+ } catch (error) {
+ console.error('❌ ERROR: No se puede conectar a Dolibarr');
+ console.error(` ${error.message}`);
+ console.error('\n Verifica que:');
+ console.error(' 1. Docker está corriendo');
+ console.error(' 2. La URL en .env.local es correcta');
+ console.error(' 3. La API key es válida\n');
+ process.exit(1);
+ }
+
+ let successCount = 0;
+ let errorCount = 0;
+ const errors = [];
+
+ for (let i = 0; i < projectsData.length; i++) {
+ const project = projectsData[i];
+ const num = i + 1;
+ const percentage = project.opp_percent;
+ const shouldClose = project.status === 2;
+
+ // Asignar cliente aleatorio
+ const clientName = clientNames[i % clientNames.length];
+
+ // Indicador visual del progreso
+ const progressBar = '▓'.repeat(Math.floor(percentage / 10)) + '░'.repeat(10 - Math.floor(percentage / 10));
+
+ console.log(`[${num}/${projectsData.length}] ${project.title}`);
+ console.log(` 📈 Progreso: [${progressBar}] ${percentage}%`);
+ console.log(` 💰 Presupuesto: €${project.opp_amount.toLocaleString()}`);
+ console.log(` 👤 Cliente: ${clientName}`);
+
+ try {
+ // Crear proyecto
+ const projectPayload = {
+ ref: project.ref,
+ title: project.title,
+ description: project.description,
+ opp_amount: project.opp_amount.toString(),
+ opp_percent: project.opp_percent.toString(),
+ date_start: project.date_start,
+ date_end: project.date_end,
+ usage_opportunity: 1,
+ usage_task: 1,
+ public: 1,
+ };
+
+ const projectId = await createProject(projectPayload);
+ console.log(` ✅ Creado con ID: ${projectId}`);
+
+ // Validar proyecto si tiene progreso > 0
+ if (percentage > 0) {
+ const validated = await validateProject(projectId);
+ if (validated) {
+ console.log(` ✓ Validado (estado: abierto)`);
+
+ // Cerrar si debe estar cerrado
+ if (shouldClose) {
+ const closed = await closeProject(projectId);
+ if (closed) {
+ console.log(` ✓ Cerrado (completado)`);
+ }
+ }
+ } else {
+ console.log(` ⚠ Creado pero no validado`);
+ }
+ } else {
+ console.log(` 📋 Dejado como borrador`);
+ }
+
+ successCount++;
+
+ // Pequeña pausa para no saturar la API
+ await new Promise(resolve => setTimeout(resolve, 300));
+ } catch (error) {
+ console.log(` ❌ Error: ${error.message}`);
+ errors.push({ project: project.title, error: error.message });
+ errorCount++;
+ }
+
+ console.log('');
+ }
+
+ // Resumen final
+ console.log('═'.repeat(60));
+ console.log('✨ PROCESO COMPLETADO\n');
+ console.log(`✅ Proyectos creados exitosamente: ${successCount}`);
+ console.log(`❌ Proyectos con error: ${errorCount}`);
+
+ if (errors.length > 0) {
+ console.log('\n📋 Errores encontrados:');
+ errors.forEach(({ project, error }) => {
+ console.log(` • ${project}: ${error}`);
+ });
+ }
+
+ console.log('═'.repeat(60));
+
+ if (successCount > 0) {
+ console.log('\n💡 Ahora puedes iniciar tu app con "npm run dev"');
+ console.log(' y ver los proyectos en http://localhost:3000\n');
+ }
+}
+
+// Ejecutar
+main().catch(error => {
+ console.error('\n❌ ERROR FATAL:', error.message);
+ process.exit(1);
+});