+>(({ className, ...props }, ref) => (
+ | [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+))
+TableCell.displayName = "TableCell"
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableCaption.displayName = "TableCaption"
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/lib/dolibarrClient.ts b/lib/dolibarrClient.ts
index 92ea63e..0edb452 100644
--- a/lib/dolibarrClient.ts
+++ b/lib/dolibarrClient.ts
@@ -1,10 +1,50 @@
/**
* Cliente para la API de Dolibarr
- * Ahora usa la API Route de Next.js (/api/dolibarr) en lugar de llamar directamente
- * Esto mantiene la API key segura en el servidor
+ *
+ * - En el CLIENTE: usa la API Route de Next.js (/api/dolibarr) para mantener la API key segura
+ * - En el SERVIDOR: llama directamente a Dolibarr con las credenciales del entorno
*/
-export async function dolibarrFetch(endpoint: string, options: RequestInit = {}) {
- // Usar la API Route en lugar de llamar directamente a Dolibarr
+
+// Detectar si estamos en el servidor o cliente
+const isServer = typeof window === 'undefined';
+
+/**
+ * Fetch directo a Dolibarr (usado en el servidor)
+ */
+async function dolibarrDirectFetch(endpoint: string, options: RequestInit = {}) {
+ const apiUrl = process.env.DOLIBARR_API_URL || process.env.NEXT_PUBLIC_API_URL;
+ const apiKey = process.env.DOLIBARR_API_KEY || process.env.NEXT_PUBLIC_DOLIBARR_API_KEY;
+
+ if (!apiUrl || !apiKey) {
+ throw new Error('Dolibarr configuration missing (DOLIBARR_API_URL or DOLIBARR_API_KEY)');
+ }
+
+ const url = `${apiUrl}/${endpoint}?DOLAPIKEY=${apiKey}`;
+
+ const res = await fetch(url, {
+ ...options,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ },
+ // Cache durante 60 segundos en servidor
+ next: { revalidate: 60 }
+ });
+
+ if (!res.ok) {
+ const errorText = await res.text();
+ console.error("Error en la llamada directa a Dolibarr:", res.status, errorText);
+ throw new Error(`Dolibarr API error: ${res.status}`);
+ }
+
+ return res.json();
+}
+
+/**
+ * Fetch via API Route (usado en el cliente)
+ */
+async function dolibarrProxyFetch(endpoint: string, options: RequestInit = {}) {
const url = `/api/dolibarr/${endpoint}`;
const res = await fetch(url, {
@@ -17,9 +57,35 @@ export async function dolibarrFetch(endpoint: string, options: RequestInit = {})
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Unknown error' }));
- console.error("Error en la llamada Dolibarr:", res.status, errorData);
+ console.error("Error en la llamada Dolibarr (proxy):", res.status, errorData);
throw new Error(errorData.error || "Dolibarr API error");
}
return res.json();
-}
\ No newline at end of file
+}
+
+/**
+ * Cliente principal de Dolibarr
+ * Automáticamente detecta el entorno y usa el método apropiado
+ */
+export async function dolibarrFetch(endpoint: string, options: RequestInit = {}) {
+ if (isServer) {
+ return dolibarrDirectFetch(endpoint, options);
+ } else {
+ return dolibarrProxyFetch(endpoint, options);
+ }
+}
+
+/**
+ * Forzar fetch directo (útil para Server Components)
+ */
+export async function dolibarrServerFetch(endpoint: string, options: RequestInit = {}) {
+ return dolibarrDirectFetch(endpoint, options);
+}
+
+/**
+ * Forzar fetch via proxy (útil para Client Components)
+ */
+export async function dolibarrClientFetch(endpoint: string, options: RequestInit = {}) {
+ return dolibarrProxyFetch(endpoint, options);
+}
diff --git a/lib/tasksService.ts b/lib/tasksService.ts
new file mode 100644
index 0000000..c7ab92e
--- /dev/null
+++ b/lib/tasksService.ts
@@ -0,0 +1,118 @@
+// lib/tasksService.ts
+import { dolibarrFetch } from "./dolibarrClient";
+import { DolibarrTask, Task, mapDolibarrTask } from "@/types/task";
+
+/**
+ * Obtener todas las tareas de un proyecto específico
+ *
+ * Nota: El endpoint /projects/{id}/tasks de Dolibarr no devuelve tareas correctamente,
+ * por lo que obtenemos todas las tareas y filtramos por fk_project en el cliente.
+ */
+export async function getTasksByProjectId(projectId: number): Promise {
+ try {
+ // Obtener todas las tareas (Dolibarr no filtra bien por proyecto)
+ const dolibarrTasks: DolibarrTask[] = await dolibarrFetch('tasks');
+
+ // Si no hay tareas, devolver array vacío
+ if (!dolibarrTasks || !Array.isArray(dolibarrTasks)) {
+ return [];
+ }
+
+ // Filtrar tareas que pertenecen a este proyecto
+ const projectTasks = dolibarrTasks.filter(
+ task => String(task.fk_project) === String(projectId)
+ );
+
+ // Mapear las tareas al formato de la UI
+ return projectTasks.map(mapDolibarrTask);
+ } catch (error) {
+ console.error('Error fetching tasks for project:', projectId, error);
+ // Si el error es 404 (no hay tareas), devolver array vacío
+ if (error instanceof Error && error.message.includes('404')) {
+ return [];
+ }
+ throw error;
+ }
+}
+
+/**
+ * Obtener todas las tareas (sin filtro de proyecto)
+ * Endpoint: /tasks
+ */
+export async function getAllTasks(): Promise {
+ try {
+ const dolibarrTasks: DolibarrTask[] = await dolibarrFetch('tasks');
+
+ if (!dolibarrTasks || !Array.isArray(dolibarrTasks)) {
+ return [];
+ }
+
+ return dolibarrTasks.map(mapDolibarrTask);
+ } catch (error) {
+ console.error('Error fetching all tasks:', error);
+ throw error;
+ }
+}
+
+/**
+ * Obtener una tarea específica por ID
+ * Endpoint: /tasks/{id}
+ */
+export async function getTaskById(taskId: number): Promise {
+ try {
+ const dolibarrTask: DolibarrTask = await dolibarrFetch(`tasks/${taskId}`);
+ return mapDolibarrTask(dolibarrTask);
+ } catch (error) {
+ console.error('Error fetching task:', taskId, error);
+ return null;
+ }
+}
+
+/**
+ * Obtener datos crudos de Dolibarr para una tarea
+ */
+export async function getDolibarrTaskById(taskId: number): Promise {
+ try {
+ return await dolibarrFetch(`tasks/${taskId}`);
+ } catch (error) {
+ console.error('Error fetching Dolibarr task:', taskId, error);
+ return null;
+ }
+}
+
+/**
+ * Calcular estadísticas de las tareas de un proyecto
+ */
+export function calculateTaskStats(tasks: Task[]) {
+ const total = tasks.length;
+ const completed = tasks.filter(t => t.status === '2').length;
+ const inProgress = tasks.filter(t => t.status === '1').length;
+ const draft = tasks.filter(t => t.status === '0').length;
+
+ const totalPlannedHours = tasks.reduce((sum, t) => sum + t.plannedHours, 0);
+ const totalWorkedHours = tasks.reduce((sum, t) => sum + t.workedHours, 0);
+
+ const avgProgress = total > 0
+ ? Math.round(tasks.reduce((sum, t) => sum + t.progress, 0) / total)
+ : 0;
+
+ const highPriority = tasks.filter(t => t.priority === '3').length;
+ const overdue = tasks.filter(t => {
+ if (!t.endDate && !t.plannedEndDate) return false;
+ const endDate = t.endDate || t.plannedEndDate;
+ return endDate && new Date(endDate) < new Date() && t.status !== '2';
+ }).length;
+
+ return {
+ total,
+ completed,
+ inProgress,
+ draft,
+ totalPlannedHours,
+ totalWorkedHours,
+ avgProgress,
+ highPriority,
+ overdue,
+ completionRate: total > 0 ? Math.round((completed / total) * 100) : 0,
+ };
+}
diff --git a/package-lock.json b/package-lock.json
index 1f9d506..b7e682a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,13 +9,16 @@
"version": "0.1.0",
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.2.6",
"@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",
+ "@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.556.0",
@@ -23,7 +26,6 @@
"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"
},
@@ -1376,6 +1378,92 @@
}
}
},
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
+ "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "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-checkbox/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-checkbox/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-checkbox/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-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -2143,6 +2231,30 @@
}
}
},
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz",
+ "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "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-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
@@ -2770,42 +2882,6 @@
"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",
@@ -2813,18 +2889,6 @@
"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",
@@ -3119,6 +3183,39 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@tanstack/react-table": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
+ "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/table-core": "8.21.3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/@tanstack/table-core": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
+ "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -3130,69 +3227,6 @@
"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",
@@ -3244,12 +3278,6 @@
"@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",
@@ -4482,127 +4510,6 @@
"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",
@@ -4682,12 +4589,6 @@
}
}
},
- "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",
@@ -4992,16 +4893,6 @@
"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",
@@ -5449,12 +5340,6 @@
"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",
@@ -5950,16 +5835,6 @@
"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",
@@ -6002,15 +5877,6 @@
"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",
@@ -7694,31 +7560,9 @@
"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",
@@ -7809,51 +7653,6 @@
"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",
@@ -7898,12 +7697,6 @@
"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",
@@ -8618,12 +8411,6 @@
"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",
@@ -9021,28 +8808,6 @@
"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 39dbb88..446e0e8 100644
--- a/package.json
+++ b/package.json
@@ -8,18 +8,22 @@
"start": "next start",
"lint": "eslint",
"seed": "node scripts/seed-projects-via-api.js",
+ "seed:tasks": "node scripts/seed-tasks.js",
"seed:direct": "node scripts/seed-projects.js",
"diagnose": "node scripts/diagnose.js"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.2.6",
"@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",
+ "@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.556.0",
@@ -43,4 +47,4 @@
"tailwindcss": "^3.4.18",
"typescript": "^5"
}
-}
+}
\ No newline at end of file
diff --git a/scripts/seed-tasks.js b/scripts/seed-tasks.js
new file mode 100644
index 0000000..b7cf992
--- /dev/null
+++ b/scripts/seed-tasks.js
@@ -0,0 +1,334 @@
+#!/usr/bin/env node
+
+/**
+ * Script para crear tareas de prueba en proyectos de Dolibarr
+ *
+ * REQUISITO: El servidor de Next.js debe estar corriendo (npm run dev)
+ * Uso: node scripts/seed-tasks.js
+ */
+
+const NEXT_API_URL = 'http://localhost:3000/api/dolibarr';
+
+// Plantillas de tareas variadas para cada proyecto
+const taskTemplates = [
+ // Tareas de análisis/planificación
+ {
+ label: "Análisis de requisitos",
+ description: "Documentar todos los requisitos funcionales y no funcionales del proyecto con el cliente.",
+ planned_workload: 28800, // 8 horas en segundos
+ progress: 100,
+ priority: 2,
+ },
+ {
+ label: "Diseño de arquitectura",
+ description: "Definir la arquitectura técnica, selección de tecnologías y patrones de diseño a utilizar.",
+ planned_workload: 36000, // 10 horas
+ progress: 100,
+ priority: 3,
+ },
+ {
+ label: "Creación de wireframes",
+ description: "Diseñar los wireframes y mockups de las principales pantallas de la aplicación.",
+ planned_workload: 21600, // 6 horas
+ progress: 80,
+ priority: 2,
+ },
+ // Tareas de desarrollo
+ {
+ label: "Configuración del entorno de desarrollo",
+ description: "Preparar repositorio, CI/CD, entornos de desarrollo y staging.",
+ planned_workload: 14400, // 4 horas
+ progress: 100,
+ priority: 3,
+ },
+ {
+ label: "Desarrollo del backend",
+ description: "Implementar la API REST, modelos de datos y lógica de negocio del servidor.",
+ planned_workload: 72000, // 20 horas
+ progress: 60,
+ priority: 3,
+ },
+ {
+ label: "Desarrollo del frontend",
+ description: "Implementar la interfaz de usuario con componentes, estados y conexión con API.",
+ planned_workload: 64800, // 18 horas
+ progress: 45,
+ priority: 3,
+ },
+ {
+ label: "Integración con servicios externos",
+ description: "Conectar con APIs de terceros, pasarelas de pago y servicios cloud.",
+ planned_workload: 28800, // 8 horas
+ progress: 30,
+ priority: 2,
+ },
+ // Tareas de testing
+ {
+ label: "Pruebas unitarias",
+ description: "Escribir y ejecutar tests unitarios para componentes críticos del sistema.",
+ planned_workload: 21600, // 6 horas
+ progress: 25,
+ priority: 2,
+ },
+ {
+ label: "Pruebas de integración",
+ description: "Realizar pruebas de integración entre módulos y con servicios externos.",
+ planned_workload: 18000, // 5 horas
+ progress: 10,
+ priority: 2,
+ },
+ {
+ label: "QA y corrección de bugs",
+ description: "Ejecutar plan de QA, documentar bugs encontrados y corregirlos.",
+ planned_workload: 36000, // 10 horas
+ progress: 0,
+ priority: 1,
+ },
+ // Tareas de documentación
+ {
+ label: "Documentación técnica",
+ description: "Crear documentación de API, guías de instalación y arquitectura del sistema.",
+ planned_workload: 14400, // 4 horas
+ progress: 15,
+ priority: 1,
+ },
+ {
+ label: "Manual de usuario",
+ description: "Redactar el manual de usuario con capturas y tutoriales paso a paso.",
+ planned_workload: 10800, // 3 horas
+ progress: 0,
+ priority: 1,
+ },
+ // Tareas de despliegue
+ {
+ label: "Configuración de producción",
+ description: "Preparar servidores, certificados SSL, dominios y configuraciones de producción.",
+ planned_workload: 18000, // 5 horas
+ progress: 0,
+ priority: 3,
+ },
+ {
+ label: "Despliegue inicial",
+ description: "Realizar el despliegue a producción y verificar el correcto funcionamiento.",
+ planned_workload: 7200, // 2 horas
+ progress: 0,
+ priority: 3,
+ },
+ {
+ label: "Formación al cliente",
+ description: "Sesión de formación al equipo del cliente sobre el uso del sistema.",
+ planned_workload: 10800, // 3 horas
+ progress: 0,
+ priority: 2,
+ },
+];
+
+/**
+ * Obtener todos los proyectos existentes
+ */
+async function getProjects() {
+ const url = `${NEXT_API_URL}/projects`;
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Error obteniendo proyectos: ${response.status}`);
+ }
+
+ return response.json();
+}
+
+// Contador global para generar refs únicos
+let taskCounter = 0;
+
+/**
+ * Generar referencia única para una tarea
+ */
+function generateTaskRef(projectId) {
+ taskCounter++;
+ const timestamp = Date.now().toString(36).toUpperCase();
+ return `TASK-P${projectId}-${String(taskCounter).padStart(3, '0')}`;
+}
+
+/**
+ * Crear una tarea en un proyecto
+ */
+async function createTask(projectId, taskData) {
+ const url = `${NEXT_API_URL}/tasks`;
+
+ // Calcular fechas basadas en la fecha actual
+ const now = new Date();
+ const startDate = new Date(now);
+ startDate.setDate(startDate.getDate() - Math.floor(Math.random() * 30)); // Hace 0-30 días
+
+ const endDate = new Date(startDate);
+ endDate.setDate(endDate.getDate() + Math.floor(Math.random() * 30) + 7); // 7-37 días después
+
+ const payload = {
+ ref: generateTaskRef(projectId), // Campo requerido por Dolibarr
+ fk_project: String(projectId), // Dolibarr espera string
+ label: taskData.label,
+ description: taskData.description,
+ planned_workload: taskData.planned_workload,
+ progress: taskData.progress,
+ priority: taskData.priority,
+ dateo: Math.floor(startDate.getTime() / 1000), // Fecha inicio planificada
+ datee: Math.floor(endDate.getTime() / 1000), // Fecha fin planificada
+ date_start: taskData.progress > 0 ? Math.floor(startDate.getTime() / 1000) : null,
+ date_end: taskData.progress >= 100 ? Math.floor(new Date().getTime() / 1000) : null,
+ };
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(`Error ${response.status}: ${errorData.error || 'Unknown error'}`);
+ }
+
+ return response.json();
+}
+
+/**
+ * Seleccionar tareas aleatorias para un proyecto
+ */
+function selectTasksForProject(projectProgress, count = 6) {
+ // Mezclar las tareas aleatoriamente
+ const shuffled = [...taskTemplates].sort(() => Math.random() - 0.5);
+
+ // Seleccionar las primeras 'count' tareas
+ const selected = shuffled.slice(0, count);
+
+ // Ajustar el progreso de las tareas según el progreso del proyecto
+ return selected.map((task, index) => {
+ let adjustedProgress = task.progress;
+
+ // Si el proyecto tiene poco progreso, reducir el progreso de las tareas
+ if (projectProgress < 30) {
+ adjustedProgress = Math.min(task.progress, 30 + Math.random() * 20);
+ } else if (projectProgress >= 100) {
+ // Si el proyecto está completo, completar más tareas
+ adjustedProgress = index < count - 1 ? 100 : Math.max(task.progress, 80);
+ } else {
+ // Ajustar proporcionalmente
+ const factor = projectProgress / 50;
+ adjustedProgress = Math.min(100, Math.round(task.progress * factor));
+ }
+
+ return {
+ ...task,
+ progress: Math.round(adjustedProgress),
+ };
+ });
+}
+
+/**
+ * Script principal
+ */
+async function main() {
+ console.log('\n📋 SEED DE TAREAS PARA PROYECTOS DE DOLIBARR\n');
+ console.log('═'.repeat(60));
+ console.log(`📡 API Next.js: ${NEXT_API_URL}`);
+ console.log(`📝 Plantillas de tareas: ${taskTemplates.length}`);
+ console.log('═'.repeat(60));
+ console.log('');
+
+ // Verificar conexión con Next.js
+ console.log('🔍 Verificando conexión con Next.js...');
+ try {
+ const testResponse = await fetch('http://localhost:3000');
+ if (!testResponse.ok && testResponse.status !== 404) {
+ throw new Error('Server not responding');
+ }
+ console.log('✅ Servidor Next.js está corriendo\n');
+ } catch (error) {
+ console.error('❌ ERROR: No se puede conectar al servidor Next.js');
+ console.error(' Por favor, ejecuta "npm run dev" en otra terminal primero.\n');
+ process.exit(1);
+ }
+
+ // Obtener proyectos existentes
+ console.log('📂 Obteniendo proyectos existentes...');
+ let projects;
+ try {
+ projects = await getProjects();
+ console.log(`✅ Encontrados ${projects.length} proyectos\n`);
+ } catch (error) {
+ console.error('❌ Error obteniendo proyectos:', error.message);
+ process.exit(1);
+ }
+
+ if (projects.length === 0) {
+ console.log('⚠️ No hay proyectos. Ejecuta primero: npm run seed\n');
+ process.exit(0);
+ }
+
+ let totalTasks = 0;
+ let totalErrors = 0;
+ const TASKS_PER_PROJECT = 6;
+
+ for (let i = 0; i < projects.length; i++) {
+ const project = projects[i];
+ const projectId = project.id;
+ const projectTitle = project.title || project.ref || `Proyecto ${projectId}`;
+ const projectProgress = parseFloat(project.opp_percent || '0');
+
+ console.log(`\n[${i + 1}/${projects.length}] 📁 ${projectTitle}`);
+ console.log(` ID: ${projectId} | Progreso: ${projectProgress}%`);
+ console.log(' Creando tareas:');
+
+ // Seleccionar tareas para este proyecto
+ const tasksToCreate = selectTasksForProject(projectProgress, TASKS_PER_PROJECT);
+
+ for (let j = 0; j < tasksToCreate.length; j++) {
+ const task = tasksToCreate[j];
+
+ try {
+ const taskId = await createTask(projectId, task);
+ const progressBar = '█'.repeat(Math.floor(task.progress / 10)) + '░'.repeat(10 - Math.floor(task.progress / 10));
+ console.log(` ✅ [${progressBar}] ${task.progress}% - ${task.label}`);
+ totalTasks++;
+
+ // Pequeña pausa para no saturar la API
+ await new Promise(resolve => setTimeout(resolve, 200));
+ } catch (error) {
+ console.log(` ❌ Error: ${task.label} - ${error.message}`);
+ totalErrors++;
+ }
+ }
+ }
+
+ // Resumen final
+ console.log('\n');
+ console.log('═'.repeat(60));
+ console.log('✨ PROCESO COMPLETADO\n');
+ console.log(`📊 Proyectos procesados: ${projects.length}`);
+ console.log(`✅ Tareas creadas: ${totalTasks}`);
+ console.log(`❌ Errores: ${totalErrors}`);
+ console.log('═'.repeat(60));
+
+ if (totalTasks > 0) {
+ console.log('\n💡 ¡Recarga tu aplicación para ver las tareas!');
+ console.log(' Haz clic en cualquier proyecto para ver sus tareas.\n');
+ }
+}
+
+// Ejecutar
+main().catch(error => {
+ console.error('\n❌ ERROR FATAL:', error.message);
+ console.error('\nAsegúrate de que:');
+ console.error(' 1. El servidor Next.js está corriendo (npm run dev)');
+ console.error(' 2. Dolibarr está accesible');
+ console.error(' 3. Existen proyectos (ejecuta "npm run seed" primero)\n');
+ process.exit(1);
+});
diff --git a/types/task.ts b/types/task.ts
new file mode 100644
index 0000000..6e25db3
--- /dev/null
+++ b/types/task.ts
@@ -0,0 +1,176 @@
+// types/task.ts
+
+// Estado de la tarea en Dolibarr
+export type TaskStatus = '0' | '1' | '2'; // 0: borrador, 1: validada, 2: cerrada/completada
+
+// Prioridad de la tarea
+export type TaskPriority = '0' | '1' | '2' | '3'; // 0: ninguna, 1: baja, 2: media, 3: alta
+
+// Interface para los datos crudos que vienen de Dolibarr
+export interface DolibarrTask {
+ id: string | number;
+ ref: string;
+ label: string;
+ description: string;
+ fk_project: string | number;
+ fk_task_parent: string | number;
+ date_start: number | null;
+ date_end: number | null;
+ dateo: number | null; // fecha planificada inicio
+ datee: number | null; // fecha planificada fin
+ date_c: number | null;
+ date_m: number | null;
+ duration_effective: number; // segundos trabajados
+ planned_workload: number; // segundos planificados
+ progress: number | string;
+ priority: string | number;
+ budget_amount: string | number;
+ rang: number;
+ status: string;
+ note_public: string;
+ note_private: string;
+ fk_user_creat: string | number;
+ fk_user_valid: string | number;
+ // Campos adicionales que puede devolver la API
+ timespent?: number;
+ array_options?: Record;
+}
+
+// Interface normalizada para la UI
+export interface Task {
+ id: number;
+ ref: string;
+ title: string;
+ description: string;
+ projectId: number;
+ parentTaskId: number | null;
+ status: TaskStatus;
+ priority: TaskPriority;
+ progress: number;
+ plannedHours: number;
+ workedHours: number;
+ budget: number;
+ startDate: string | null;
+ endDate: string | null;
+ plannedStartDate: string | null;
+ plannedEndDate: string | null;
+ createdAt: string;
+ updatedAt: string;
+ createdBy: number;
+ order: number;
+}
+
+// Configuración de estados de tarea
+export const TASK_STATUS_CONFIG: Record = {
+ '0': {
+ label: 'Borrador',
+ color: 'gray',
+ bgClass: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
+ },
+ '1': {
+ label: 'Validada',
+ color: 'blue',
+ bgClass: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
+ },
+ '2': {
+ label: 'Completada',
+ color: 'green',
+ bgClass: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
+ },
+};
+
+// Configuración de prioridades
+export const TASK_PRIORITY_CONFIG: Record = {
+ '0': {
+ label: 'Sin prioridad',
+ color: 'gray',
+ bgClass: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'
+ },
+ '1': {
+ label: 'Baja',
+ color: 'blue',
+ bgClass: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
+ },
+ '2': {
+ label: 'Media',
+ color: 'yellow',
+ bgClass: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
+ },
+ '3': {
+ label: 'Alta',
+ color: 'red',
+ bgClass: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
+ },
+};
+
+// Helper para convertir timestamp a fecha ISO
+function timestampToDateString(timestamp: number | null | undefined): string | null {
+ if (!timestamp || timestamp === 0) {
+ return null;
+ }
+ return new Date(timestamp * 1000).toISOString().split('T')[0];
+}
+
+// Helper para convertir segundos a horas
+function secondsToHours(seconds: number | null | undefined): number {
+ if (!seconds) return 0;
+ return Math.round((seconds / 3600) * 10) / 10; // Redondear a 1 decimal
+}
+
+// Función para mapear tarea de Dolibarr al formato UI
+export function mapDolibarrTask(dolibarr: DolibarrTask): Task {
+ const progress = typeof dolibarr.progress === 'string'
+ ? parseFloat(dolibarr.progress)
+ : (dolibarr.progress || 0);
+
+ const priority = String(dolibarr.priority || '0') as TaskPriority;
+ const validPriorities: TaskPriority[] = ['0', '1', '2', '3'];
+ const safePriority: TaskPriority = validPriorities.includes(priority) ? priority : '0';
+
+ const status = String(dolibarr.status || '0') as TaskStatus;
+ const validStatuses: TaskStatus[] = ['0', '1', '2'];
+ const safeStatus: TaskStatus = validStatuses.includes(status) ? status : '0';
+
+ return {
+ id: typeof dolibarr.id === 'string' ? parseInt(dolibarr.id) : dolibarr.id,
+ ref: dolibarr.ref || '',
+ title: dolibarr.label || 'Sin título',
+ description: dolibarr.description || '',
+ projectId: typeof dolibarr.fk_project === 'string'
+ ? parseInt(dolibarr.fk_project)
+ : dolibarr.fk_project,
+ parentTaskId: dolibarr.fk_task_parent
+ ? (typeof dolibarr.fk_task_parent === 'string'
+ ? parseInt(dolibarr.fk_task_parent)
+ : dolibarr.fk_task_parent)
+ : null,
+ status: safeStatus,
+ priority: safePriority,
+ progress: Math.min(Math.max(progress, 0), 100), // Asegurar entre 0-100
+ plannedHours: secondsToHours(dolibarr.planned_workload),
+ workedHours: secondsToHours(dolibarr.duration_effective || dolibarr.timespent),
+ budget: typeof dolibarr.budget_amount === 'string'
+ ? parseFloat(dolibarr.budget_amount) || 0
+ : (dolibarr.budget_amount || 0),
+ startDate: timestampToDateString(dolibarr.date_start),
+ endDate: timestampToDateString(dolibarr.date_end),
+ plannedStartDate: timestampToDateString(dolibarr.dateo),
+ plannedEndDate: timestampToDateString(dolibarr.datee),
+ createdAt: timestampToDateString(dolibarr.date_c) || new Date().toISOString().split('T')[0],
+ updatedAt: timestampToDateString(dolibarr.date_m) || new Date().toISOString().split('T')[0],
+ createdBy: typeof dolibarr.fk_user_creat === 'string'
+ ? parseInt(dolibarr.fk_user_creat)
+ : (dolibarr.fk_user_creat || 0),
+ order: dolibarr.rang || 0,
+ };
+}
+
+// Helper para obtener label de estado
+export function getTaskStatusLabel(status: TaskStatus): string {
+ return TASK_STATUS_CONFIG[status]?.label || 'Desconocido';
+}
+
+// Helper para obtener label de prioridad
+export function getTaskPriorityLabel(priority: TaskPriority): string {
+ return TASK_PRIORITY_CONFIG[priority]?.label || 'Sin prioridad';
+}
|