diff --git a/app/api/dolibarr/[...path]/route.ts b/app/api/dolibarr/[...path]/route.ts index a9fb10a..23ac735 100644 --- a/app/api/dolibarr/[...path]/route.ts +++ b/app/api/dolibarr/[...path]/route.ts @@ -97,17 +97,33 @@ export async function POST( body: JSON.stringify(body), }); + const responseText = await response.text(); + if (!response.ok) { - const errorText = await response.text(); - console.error('Dolibarr API error:', response.status, errorText); + console.error('Dolibarr API error:', response.status, responseText); + + // Intentar parsear como JSON para obtener detalles del error + let errorDetails = responseText; + try { + const errorJson = JSON.parse(responseText); + errorDetails = errorJson.error?.message || errorJson.error || responseText; + } catch { + // Mantener el texto original si no es JSON + } return NextResponse.json( - { error: 'Error al comunicarse con Dolibarr', details: errorText }, + { error: errorDetails }, { status: response.status } ); } - const data = await response.json(); + // Parsear respuesta exitosa + let data; + try { + data = JSON.parse(responseText); + } catch { + data = responseText; + } return NextResponse.json(data); } catch (error) { diff --git a/app/tareas/[id]/loading.tsx b/app/tareas/[id]/loading.tsx new file mode 100644 index 0000000..8583e1c --- /dev/null +++ b/app/tareas/[id]/loading.tsx @@ -0,0 +1,116 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; + +export default function TaskDetailLoading() { + return ( +
+ {/* Header skeleton */} +
+
+ {/* Breadcrumb */} +
+ + + + + +
+ + {/* Header principal */} +
+
+
+ + + +
+
+ + +
+
+
+ + +
+
+
+
+ + {/* Contenido */} +
+ {/* Stats skeleton */} +
+ {Array.from({ length: 4 }).map((_, i) => ( + + + + + + + + + + + + ))} +
+ + {/* Info skeleton */} +
+ {/* Descripción */} + + + + + +
+ + + +
+
+
+ + {/* Info General */} + + + + + + + {Array.from({ length: 5 }).map((_, i) => ( +
+ +
+ + +
+
+ ))} +
+
+ + {/* Fechas y Tiempos */} + + + + + + + {Array.from({ length: 6 }).map((_, i) => ( +
+ +
+ + +
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/app/tareas/[id]/not-found.tsx b/app/tareas/[id]/not-found.tsx new file mode 100644 index 0000000..b6324bb --- /dev/null +++ b/app/tareas/[id]/not-found.tsx @@ -0,0 +1,36 @@ +import Link from "next/link"; +import { FileQuestion, ArrowLeft, Home } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +export default function TaskNotFound() { + return ( +
+
+ +
+ +

Tarea no encontrada

+ +

+ La tarea que buscas no existe o ha sido eliminada. + Verifica el ID de la tarea o vuelve a la lista de proyectos. +

+ +
+ + + +
+
+ ); +} diff --git a/app/tareas/[id]/page.tsx b/app/tareas/[id]/page.tsx new file mode 100644 index 0000000..8efdd67 --- /dev/null +++ b/app/tareas/[id]/page.tsx @@ -0,0 +1,76 @@ +import { notFound } from "next/navigation"; +import { getTaskById } from "@/lib/tasksService"; +import { getProjectById } from "@/lib/projectsService"; +import { TaskDetailView } from "@/components/task-detail"; + +interface PageProps { + params: Promise<{ + id: string; + }>; +} + +/** + * Página de detalle de tarea + * Ruta dinámica: /tareas/[id] + * + * Muestra información completa de la tarea incluyendo: + * - Header con breadcrumb, título, estado y prioridad + * - Estadísticas (progreso, tiempo, fecha límite, rendimiento) + * - Información detallada (descripción, fechas, metadatos) + * - Navegación al proyecto padre + */ +export default async function TaskDetailPage({ params }: PageProps) { + // Await params en Next.js 16 + const resolvedParams = await params; + const taskId = parseInt(resolvedParams.id); + + if (isNaN(taskId)) { + notFound(); + } + + // Obtener la tarea + const task = await getTaskById(taskId); + + if (!task) { + notFound(); + } + + // Obtener el proyecto asociado (puede ser null) + let project = null; + if (task.projectId) { + try { + const fetchedProject = await getProjectById(task.projectId); + project = fetchedProject ?? null; + } catch (error) { + console.error('Error fetching project for task:', error); + // Continuar sin proyecto + } + } + + return ; +} + +// Metadata dinámica para SEO +export async function generateMetadata({ params }: PageProps) { + const resolvedParams = await params; + const taskId = parseInt(resolvedParams.id); + + if (isNaN(taskId)) { + return { title: "Tarea no encontrada" }; + } + + try { + const task = await getTaskById(taskId); + + if (!task) { + return { title: "Tarea no encontrada" }; + } + + return { + title: `${task.title} - Tarea`, + description: task.description || `Detalles de la tarea ${task.ref}`, + }; + } catch { + return { title: "Tarea" }; + } +} diff --git a/components/dashboard/dashboard-header.tsx b/components/dashboard/dashboard-header.tsx index cc579a1..6f21684 100644 --- a/components/dashboard/dashboard-header.tsx +++ b/components/dashboard/dashboard-header.tsx @@ -17,37 +17,37 @@ export default function DashboardHeader({ onViewModeChange }: DashboardHeaderProps) { return ( -
-
+
+
-

Dashboard de Proyectos

-

Gestiona y visualiza todos tus proyectos

+

Dashboard de Proyectos

+

Gestiona y visualiza todos tus proyectos

- + onSearchChange(e.target.value)} - className="pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-64 text-sm" + className="pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-64 text-sm bg-background text-foreground" />
-
+
diff --git a/components/dashboard/project-card.tsx b/components/dashboard/project-card.tsx index e829e13..934e4d0 100644 --- a/components/dashboard/project-card.tsx +++ b/components/dashboard/project-card.tsx @@ -3,9 +3,9 @@ import Link from 'next/link'; import { Calendar, DollarSign, TrendingUp, FileText } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { Project, ProjectStatus } from '@/types/project'; +import { ProjectStatusBadge } from '@/components/ui/project-status-badge'; +import { Project } from '@/types/project'; interface ProjectCardProps { project: Project; @@ -21,46 +21,12 @@ function getInitials(name: string): string { .slice(0, 2); } -// Componente para el badge de estado con estilos directos -function StatusBadge({ status }: { status: ProjectStatus }) { - if (status === '0') { - return ( - - Borrador - - ); - } - - if (status === '1') { - return ( - - Abierto - - ); - } - - if (status === '2') { - return ( - - Cerrado - - ); - } - - // Fallback - return ( - - Desconocido - - ); -} - export default function ProjectCard({ project }: ProjectCardProps) { const initials = getInitials(project.name); return ( - +
@@ -70,32 +36,32 @@ export default function ProjectCard({ project }: ProjectCardProps) {
- {project.name} + {project.name} {project.ref}
- +
{/* Description */} {project.description && (
-
+
Descripción
-

{project.description}

+

{project.description}

)} {/* Progress Bar */}
- Progreso - {project.progress}% + Progreso + {project.progress}%
-
+
-
+
Presupuesto
-

+

€{project.budget.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}

-
+
Estimado
-

+

€{project.spent.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}

{/* Footer Info */} -
-
+
+
{new Date(project.startDate).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })}
-
+
{new Date(project.endDate).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })} @@ -143,9 +109,9 @@ export default function ProjectCard({ project }: ProjectCardProps) { {/* Cliente */} {project.client !== 'Sin cliente' && ( -
- Cliente: - {project.client} +
+ Cliente: + {project.client}
)} diff --git a/components/dashboard/project-dashboard.tsx b/components/dashboard/project-dashboard.tsx index 39f8a02..5f1a29f 100644 --- a/components/dashboard/project-dashboard.tsx +++ b/components/dashboard/project-dashboard.tsx @@ -41,7 +41,7 @@ export default function ProjectDashboard() { if (loading) { return ( -
+
{}} @@ -49,11 +49,11 @@ export default function ProjectDashboard() { onViewModeChange={setViewMode} /> -
+
{/* Stats skeleton */}
{Array.from({ length: 4 }).map((_, i) => ( -
+
@@ -73,9 +73,9 @@ export default function ProjectDashboard() { if (error) { return ( -
+
-

{error}

+

{error}

@@ -113,7 +80,7 @@ export default function ProjectDetail({ project }: ProjectDetailProps) { -

{project.description}

+

{project.description}

)} @@ -129,25 +96,25 @@ export default function ProjectDetail({ project }: ProjectDetailProps) {
- Avance Total - {project.progress}% + Avance Total + {project.progress}%
-
+
-
-

Estado

-

+

+

Estado

+

{project.status === '0' ? 'Borrador' : project.status === '1' ? 'En Progreso' : 'Finalizado'}

-
-

Completado

-

+

+

Completado

+

{project.progress === 100 ? 'Sí' : 'No'}

@@ -167,22 +134,22 @@ export default function ProjectDetail({ project }: ProjectDetailProps) {
-

Presupuesto Total

-

+

Presupuesto Total

+

€{project.budget.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

-

Estimado Gastado

-

+

Estimado Gastado

+

€{project.spent.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

-
- Restante - +
+ Restante + €{(project.budget - project.spent).toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
@@ -201,10 +168,10 @@ export default function ProjectDetail({ project }: ProjectDetailProps) { {/* Cliente */} {project.client !== 'Sin cliente' && (
- +
-

Cliente

-

{project.client}

+

Cliente

+

{project.client}

)} @@ -213,10 +180,10 @@ export default function ProjectDetail({ project }: ProjectDetailProps) { {/* Fecha de Inicio */}
- +
-

Fecha de Inicio

-

+

Fecha de Inicio

+

{new Date(project.startDate).toLocaleDateString('es-ES', { day: 'numeric', month: 'long', @@ -230,10 +197,10 @@ export default function ProjectDetail({ project }: ProjectDetailProps) { {/* Fecha de Fin */}

- +
-

Fecha de Fin

-

+

Fecha de Fin

+

{new Date(project.endDate).toLocaleDateString('es-ES', { day: 'numeric', month: 'long', @@ -247,10 +214,10 @@ export default function ProjectDetail({ project }: ProjectDetailProps) { {/* Duración */}

- +
-

Duración

-

+

Duración

+

{Math.ceil((new Date(project.endDate).getTime() - new Date(project.startDate).getTime()) / (1000 * 60 * 60 * 24))} días

@@ -265,18 +232,18 @@ export default function ProjectDetail({ project }: ProjectDetailProps) {
-

ID del Proyecto

-

{project.id}

+

ID del Proyecto

+

{project.id}

-

Referencia

-

{project.ref}

+

Referencia

+

{project.ref}

-

Creado

-

+

Creado

+

{new Date(project.createdAt).toLocaleDateString('es-ES', { day: 'numeric', month: 'short', @@ -286,8 +253,8 @@ export default function ProjectDetail({ project }: ProjectDetailProps) {

-

Última Actualización

-

+

Última Actualización

+

{new Date(project.updatedAt).toLocaleDateString('es-ES', { day: 'numeric', month: 'short', diff --git a/components/dashboard/project-grid.tsx b/components/dashboard/project-grid.tsx index 41a781f..dd7b2b8 100644 --- a/components/dashboard/project-grid.tsx +++ b/components/dashboard/project-grid.tsx @@ -13,7 +13,7 @@ export default function ProjectGrid({ projects, viewMode }: ProjectGridProps) { if (projects.length === 0) { return (

-

No se encontraron proyectos

+

No se encontraron proyectos

); } diff --git a/components/dashboard/project-list-item.tsx b/components/dashboard/project-list-item.tsx index f419f2e..7622c23 100644 --- a/components/dashboard/project-list-item.tsx +++ b/components/dashboard/project-list-item.tsx @@ -2,9 +2,9 @@ import Link from 'next/link'; import { Calendar, DollarSign, TrendingUp } from 'lucide-react'; -import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { Project, ProjectStatus } from '@/types/project'; +import { ProjectStatusBadge } from '@/components/ui/project-status-badge'; +import { Project } from '@/types/project'; interface ProjectListItemProps { project: Project; @@ -20,40 +20,6 @@ function getInitials(name: string): string { .slice(0, 2); } -// Componente para el badge de estado con estilos directos -function StatusBadge({ status }: { status: ProjectStatus }) { - if (status === '0') { - return ( - - Borrador - - ); - } - - if (status === '1') { - return ( - - Abierto - - ); - } - - if (status === '2') { - return ( - - Cerrado - - ); - } - - // Fallback - return ( - - Desconocido - - ); -} - /** * Componente de fila de proyecto para vista de lista * Diseño horizontal y compacto @@ -63,7 +29,7 @@ export default function ProjectListItem({ project }: ProjectListItemProps) { return ( -
+
{/* Avatar */} @@ -77,30 +43,30 @@ export default function ProjectListItem({ project }: ProjectListItemProps) {
-

+

{project.name}

- {project.ref} + {project.ref}
{project.client !== 'Sin cliente' && ( -

{project.client}

+

{project.client}

)}
{/* Estado */} - +
{/* Progreso */}
- +
- Progreso - {project.progress}% + Progreso + {project.progress}%
-
+
-
+
€{project.budget.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
-
Presupuesto
+
Presupuesto
{/* Fechas */}
- +
-
+
{new Date(project.startDate).toLocaleDateString('es-ES', { day: '2-digit', month: 'short' })}
-
Inicio
+
Inicio
diff --git a/components/dashboard/stats-overview.tsx b/components/dashboard/stats-overview.tsx index cd6bfdb..27e909d 100644 --- a/components/dashboard/stats-overview.tsx +++ b/components/dashboard/stats-overview.tsx @@ -20,49 +20,49 @@ export default function StatsOverview({ projects }: StatsOverviewProps) { return (
- + - Total Proyectos - + Total Proyectos + -
{stats.total}
-

Todos los proyectos

+
{stats.total}
+

Todos los proyectos

- + - Activos + Activos -
{stats.activos}
-

En progreso ahora

+
{stats.activos}
+

En progreso ahora

- + - Presupuesto Total + Presupuesto Total -
+
€{(stats.totalBudget / 1000).toFixed(0)}k
-

Suma de presupuestos

+

Suma de presupuestos

- + - Progreso Medio + Progreso Medio -
{stats.avgProgress}%
-

De todos los proyectos

+
{stats.avgProgress}%
+

De todos los proyectos

diff --git a/components/gantt/gantt-page.tsx b/components/gantt/gantt-page.tsx index aeb9d3c..ce3b323 100644 --- a/components/gantt/gantt-page.tsx +++ b/components/gantt/gantt-page.tsx @@ -265,10 +265,10 @@ export default function GanttPage() { if (error) { return ( -
+
- -

{error}

+ +

{error}

- + )} @@ -180,7 +189,11 @@ export function ProjectDetailView({ project }: ProjectDetailViewProps) { ) : tasksError ? ( ) : ( - + )} diff --git a/components/project-detail/project-header.tsx b/components/project-detail/project-header.tsx index 0705359..f4f9433 100644 --- a/components/project-detail/project-header.tsx +++ b/components/project-detail/project-header.tsx @@ -5,7 +5,6 @@ import { useRouter } from "next/navigation"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, @@ -14,7 +13,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Project, ProjectStatus } from "@/types/project"; +import { ProjectStatusBadge } from "@/components/ui/project-status-badge"; +import { Project } from "@/types/project"; interface ProjectHeaderProps { project: Project; @@ -30,31 +30,7 @@ function getInitials(name: string): string { .slice(0, 2); } -// Componente para el badge de estado -function StatusBadge({ status }: { status: ProjectStatus }) { - const statusConfig: Record = { - "0": { - label: "Borrador", - className: "bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700", - }, - "1": { - label: "Abierto", - className: "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800", - }, - "2": { - label: "Cerrado", - className: "bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800", - }, - }; - const config = statusConfig[status] || statusConfig["0"]; - - return ( - - {config.label} - - ); -} export function ProjectHeader({ project }: ProjectHeaderProps) { const router = useRouter(); @@ -90,7 +66,7 @@ export function ProjectHeader({ project }: ProjectHeaderProps) {

{project.name}

- +
diff --git a/components/project-detail/task-columns.tsx b/components/project-detail/task-columns.tsx index 5cb500e..5f22e05 100644 --- a/components/project-detail/task-columns.tsx +++ b/components/project-detail/task-columns.tsx @@ -11,6 +11,7 @@ import { AlertTriangle, CheckCircle2 } from "lucide-react"; +import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -132,7 +133,12 @@ export const taskColumns: ColumnDef[] = [ ), cell: ({ row }) => (
-

{row.getValue("title")}

+ + {row.getValue("title")} + {row.original.ref && (

{row.original.ref}

)} @@ -218,6 +224,8 @@ export const taskColumns: ColumnDef[] = [ id: "actions", enableHiding: false, cell: ({ row }) => { + const task = row.original; + return ( @@ -229,9 +237,11 @@ export const taskColumns: ColumnDef[] = [ Acciones - - - Ver detalles + + + + Ver detalles + diff --git a/components/project-detail/tasks-section.tsx b/components/project-detail/tasks-section.tsx index d3b39bf..47b53ca 100644 --- a/components/project-detail/tasks-section.tsx +++ b/components/project-detail/tasks-section.tsx @@ -12,6 +12,7 @@ import { LayoutGrid, List } from "lucide-react"; +import Link from "next/link"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -26,11 +27,14 @@ import { } from "@/components/ui/select"; import { DataTable } from "@/components/projects/data-table"; import { taskColumns } from "./task-columns"; +import { TaskFormSheet } from "@/components/task-form/task-form-sheet"; import { Task, TASK_STATUS_CONFIG, TASK_PRIORITY_CONFIG } from "@/types/task"; interface TasksSectionProps { tasks: Task[]; + projectId: number; isLoading?: boolean; + onTaskCreated?: (task: Task) => void; } // Tarjeta de tarea para vista grid @@ -44,68 +48,70 @@ function TaskCard({ task }: { task: Task }) { })(); return ( - - -
- {/* Header */} -
-
-

{task.title}

- {task.ref && ( -

{task.ref}

+ + + +
+ {/* Header */} +
+
+

{task.title}

+ {task.ref && ( +

{task.ref}

+ )} +
+ {task.priority !== '0' && ( + + {task.priority === '3' && } + {priorityConfig.label} + )}
- {task.priority !== '0' && ( - - {task.priority === '3' && } - {priorityConfig.label} + + {/* Progreso */} +
+
+ Progreso + {task.progress}% +
+
+
= 100 ? 'bg-green-500' : + task.progress >= 50 ? 'bg-blue-500' : 'bg-gray-400' + }`} + style={{ width: `${task.progress}%` }} + /> +
+
+ + {/* Footer */} +
+ + {task.status === '2' && } + {statusConfig.label} - )} -
- - {/* Progreso */} -
-
- Progreso - {task.progress}% -
-
-
= 100 ? 'bg-green-500' : - task.progress >= 50 ? 'bg-blue-500' : 'bg-gray-400' - }`} - style={{ width: `${task.progress}%` }} - /> + + {(task.endDate || task.plannedEndDate) && ( + + + {new Date(task.endDate || task.plannedEndDate!).toLocaleDateString('es-ES', { + day: '2-digit', + month: 'short', + })} + {isOverdue && } + + )}
- - {/* Footer */} -
- - {task.status === '2' && } - {statusConfig.label} - - - {(task.endDate || task.plannedEndDate) && ( - - - {new Date(task.endDate || task.plannedEndDate!).toLocaleDateString('es-ES', { - day: '2-digit', - month: 'short', - })} - {isOverdue && } - - )} -
-
- - + + + ); } // Estado vacío -function EmptyTasks() { +function EmptyTasks({ onCreateClick }: { onCreateClick: () => void }) { return (
@@ -115,7 +121,7 @@ function EmptyTasks() {

Este proyecto aún no tiene tareas asignadas. Crea la primera tarea para comenzar.

- @@ -123,11 +129,17 @@ function EmptyTasks() { ); } -export function TasksSection({ tasks, isLoading }: TasksSectionProps) { +export function TasksSection({ tasks, projectId, isLoading, onTaskCreated }: TasksSectionProps) { const [searchValue, setSearchValue] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [priorityFilter, setPriorityFilter] = useState("all"); const [viewMode, setViewMode] = useState<"table" | "grid">("table"); + const [isCreateSheetOpen, setIsCreateSheetOpen] = useState(false); + + // Handler para cuando se crea una tarea exitosamente + const handleCreateSuccess = (task: Task) => { + onTaskCreated?.(task); + }; // Filtrar tareas const filteredTasks = useMemo(() => { @@ -175,7 +187,18 @@ export function TasksSection({ tasks, isLoading }: TasksSectionProps) { const isFiltered = searchValue !== "" || statusFilter !== "all" || priorityFilter !== "all"; if (tasks.length === 0 && !isLoading) { - return ; + return ( + <> + setIsCreateSheetOpen(true)} /> + + + ); } return ( @@ -278,7 +301,7 @@ export function TasksSection({ tasks, isLoading }: TasksSectionProps) {
{/* Nueva tarea */} - @@ -307,6 +330,15 @@ export function TasksSection({ tasks, isLoading }: TasksSectionProps) {
)} + + {/* Sheet de creación de tarea */} +
); } diff --git a/components/projects/columns.tsx b/components/projects/columns.tsx index bc3b428..023b845 100644 --- a/components/projects/columns.tsx +++ b/components/projects/columns.tsx @@ -5,7 +5,6 @@ import { ArrowUpDown, MoreHorizontal, Eye, Pencil, Trash2 } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { DropdownMenu, @@ -15,24 +14,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Project, ProjectStatus, STATUS_CONFIG } from "@/types/project"; - -// Componente para el badge de estado -function StatusBadge({ status }: { status: ProjectStatus }) { - const config = STATUS_CONFIG[status]; - - const colorClasses: Record = { - '0': 'bg-gray-100 text-gray-700 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-300', - '1': 'bg-blue-100 text-blue-700 hover:bg-blue-100/80 dark:bg-blue-900/30 dark:text-blue-400', - '2': 'bg-green-100 text-green-700 hover:bg-green-100/80 dark:bg-green-900/30 dark:text-green-400', - }; - - return ( - - {config?.label || 'Desconocido'} - - ); -} +import { ProjectStatusBadge } from "@/components/ui/project-status-badge"; +import { Project, ProjectStatus } from "@/types/project"; // Componente para la barra de progreso function ProgressBar({ progress }: { progress: number }) { @@ -45,7 +28,7 @@ function ProgressBar({ progress }: { progress: number }) { return (
-
+
[] = [ { accessorKey: "status", header: "Estado", - cell: ({ row }) => , + cell: ({ row }) => , filterFn: (row, id, value) => { return value.includes(row.getValue(id)); }, diff --git a/components/statistics/statistics-page.tsx b/components/statistics/statistics-page.tsx index a66144f..dd4a1c4 100644 --- a/components/statistics/statistics-page.tsx +++ b/components/statistics/statistics-page.tsx @@ -83,14 +83,14 @@ interface StatCardProps { function StatCard({ title, value, subtitle, icon, highlight }: StatCardProps) { return ( - + - {title} + {title} {icon} -
{value}
- {subtitle &&

{subtitle}

} +
{value}
+ {subtitle &&

{subtitle}

}
); @@ -115,9 +115,9 @@ function StatusPieChart({ borradores, abiertos, cerrados }: StatusPieChartProps) const total = borradores + abiertos + cerrados; return ( - + - Distribucion por Estado + Distribucion por Estado
@@ -144,8 +144,8 @@ function StatusPieChart({ borradores, abiertos, cerrados }: StatusPieChartProps)
-

{total}

-

Total

+

{total}

+

Total

@@ -153,7 +153,7 @@ function StatusPieChart({ borradores, abiertos, cerrados }: StatusPieChartProps) {data.map((entry) => (
- {entry.name}: {entry.value} + {entry.name}: {entry.value}
))}
@@ -172,9 +172,9 @@ interface ProgressDonutProps { function ProgressDonut({ data, title, centerValue, centerLabel }: ProgressDonutProps) { return ( - + - {title} + {title}
@@ -202,8 +202,8 @@ function ProgressDonut({ data, title, centerValue, centerLabel }: ProgressDonutP {centerValue !== undefined && (
-

{centerValue}

- {centerLabel &&

{centerLabel}

} +

{centerValue}

+ {centerLabel &&

{centerLabel}

}
)} @@ -212,7 +212,7 @@ function ProgressDonut({ data, title, centerValue, centerLabel }: ProgressDonutP {data.map((entry) => (
- {entry.name} + {entry.name}
))}
@@ -233,9 +233,9 @@ function RadialMetric({ value, title, subtitle, color = COLORS.primary }: Radial const data = [{ name: title, value: Math.min(value, 100), fill: color }]; return ( - + - {title} + {title}
@@ -250,7 +250,7 @@ function RadialMetric({ value, title, subtitle, color = COLORS.primary }: Radial endAngle={0} > @@ -258,8 +258,8 @@ function RadialMetric({ value, title, subtitle, color = COLORS.primary }: Radial
-

{value}%

- {subtitle &&

{subtitle}

} +

{value}%

+ {subtitle &&

{subtitle}

}
@@ -287,9 +287,9 @@ function BudgetBarChart({ projects, title, maxItems = 6 }: BudgetBarChartProps) })); return ( - + - {title} + {title}
@@ -334,9 +334,9 @@ interface ComparisonBarChartProps { function ComparisonBarChart({ data, title, formatter }: ComparisonBarChartProps) { return ( - + - {title} + {title}
@@ -377,9 +377,9 @@ function MultiRadialChart({ data, title, valueLabel = "%" }: MultiRadialProps) { })); return ( - + - {title} + {title}
@@ -394,7 +394,7 @@ function MultiRadialChart({ data, title, valueLabel = "%" }: MultiRadialProps) { endAngle={-270} > @@ -527,19 +527,19 @@ function AllProjectsStats({ projects }: { projects: Project[] }) { {(overdueProjects.length > 0 || upcomingDeadlines.length > 0) && (
{overdueProjects.length > 0 && ( - +
-

+

Proyectos Retrasados

-

{overdueProjects.length}

-

Fecha límite superada

+

{overdueProjects.length}

+

Fecha límite superada

{overdueProjects.slice(0, 2).map(p => ( -

{p.name}

+

{p.name}

))}
@@ -547,19 +547,19 @@ function AllProjectsStats({ projects }: { projects: Project[] }) {
)} {upcomingDeadlines.length > 0 && ( - +
-

+

Próximos a Vencer

-

{upcomingDeadlines.length}

-

En los próximos 14 días

+

{upcomingDeadlines.length}

+

En los próximos 14 días

{upcomingDeadlines.slice(0, 2).map(p => ( -

+

{p.name} ({getDaysRemaining(p.endDate)}d)

))} @@ -586,9 +586,9 @@ function AllProjectsStats({ projects }: { projects: Project[] }) { {/* Timeline de fechas - Próximos vencimientos y retrasados */} {(upcomingDeadlines.length > 0 || overdueProjects.length > 0) && ( - + - + Timeline de Proyectos @@ -597,20 +597,20 @@ function AllProjectsStats({ projects }: { projects: Project[] }) { {/* Proyectos retrasados */} {overdueProjects.length > 0 && (
-

+

Retrasados ({overdueProjects.length})

{overdueProjects.slice(0, 6).map((p, index) => { const daysOverdue = Math.abs(getDaysRemaining(p.endDate)); return ( -
+
-

{p.name}

-

{p.progress}% completado

+

{p.name}

+

{p.progress}% completado

- + -{daysOverdue}d
@@ -623,25 +623,25 @@ function AllProjectsStats({ projects }: { projects: Project[] }) { {/* Próximos vencimientos */} {upcomingDeadlines.length > 0 && (
-

+

Próximos 14 días ({upcomingDeadlines.length})

{upcomingDeadlines.slice(0, 6).map((p, index) => { const days = getDaysRemaining(p.endDate); return ( -
+
-

{p.name}

-

{p.progress}% completado

+

{p.name}

+

{p.progress}% completado

{days}d @@ -654,8 +654,8 @@ function AllProjectsStats({ projects }: { projects: Project[] }) { )} {/* Resumen de fechas */} -
-
+
+
Retrasados: {overdueProjects.length} @@ -689,9 +689,9 @@ function ActiveProjectsStats({ projects }: { projects: Project[] }) { if (activeProjects.length === 0) { return (
- -

No hay proyectos activos

-

Todos los proyectos están cerrados o en borrador

+ +

No hay proyectos activos

+

Todos los proyectos están cerrados o en borrador

); } @@ -730,41 +730,41 @@ function ActiveProjectsStats({ projects }: { projects: Project[] }) { {/* Alertas */}
- 0 ? 'bg-red-50' : 'bg-gray-50 border-gray-200'}`}> + 0 ? 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-950/30' : 'border bg-muted/30'}`}>
-

0 ? 'text-red-700' : 'text-gray-500'}`}> +

0 ? 'text-red-700 dark:text-red-400' : 'text-muted-foreground'}`}> Retrasados

-

0 ? 'text-red-600' : 'text-gray-400'}`}>{overdueProjects.length}

-

0 ? 'text-red-600' : 'text-gray-400'}`}>Fecha superada

+

0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50'}`}>{overdueProjects.length}

+

0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50'}`}>Fecha superada

- 0 ? 'bg-orange-50' : 'bg-gray-50 border-gray-200'}`}> + 0 ? 'border-orange-200 dark:border-orange-900 bg-orange-50 dark:bg-orange-950/30' : 'border bg-muted/30'}`}>
-

0 ? 'text-orange-700' : 'text-gray-500'}`}> +

0 ? 'text-orange-700 dark:text-orange-400' : 'text-muted-foreground'}`}> Requieren Atención

-

0 ? 'text-orange-600' : 'text-gray-400'}`}>{needsAttention}

-

0 ? 'text-orange-600' : 'text-gray-400'}`}>Menos del 25%

+

0 ? 'text-orange-600 dark:text-orange-400' : 'text-muted-foreground/50'}`}>{needsAttention}

+

0 ? 'text-orange-600 dark:text-orange-400' : 'text-muted-foreground/50'}`}>Menos del 25%

- +
-

+

Próximos a Completar

-

{nearCompletion}

-

75% o más

+

{nearCompletion}

+

75% o más

@@ -779,9 +779,9 @@ function ActiveProjectsStats({ projects }: { projects: Project[] }) { {/* Próximos vencimientos */} {upcomingDeadlines.length > 0 && ( - + - + Próximos Vencimientos (14 días) @@ -790,13 +790,13 @@ function ActiveProjectsStats({ projects }: { projects: Project[] }) { {upcomingDeadlines.slice(0, 6).map((p, index) => { const days = getDaysRemaining(p.endDate); return ( -
+
-

{p.name}

-

{p.progress}% completado

+

{p.name}

+

{p.progress}% completado

- + {days}d
@@ -821,9 +821,9 @@ function ClosedProjectsStats({ projects }: { projects: Project[] }) { if (closedProjects.length === 0) { return (
- -

No hay proyectos cerrados

-

Aún no se ha cerrado ningún proyecto

+ +

No hay proyectos cerrados

+

Aún no se ha cerrado ningún proyecto

); } @@ -867,30 +867,30 @@ function ClosedProjectsStats({ projects }: { projects: Project[] }) { {/* Cumplimiento visual */}
- +
-
- +
+
-

Dentro del Presupuesto

-

{withinBudget}

-

{stats.total > 0 ? Math.round((withinBudget / stats.total) * 100) : 0}% de los cerrados

+

Dentro del Presupuesto

+

{withinBudget}

+

{stats.total > 0 ? Math.round((withinBudget / stats.total) * 100) : 0}% de los cerrados

- 0 ? 'bg-gradient-to-br from-red-50 to-orange-50' : 'bg-gray-50 border-gray-200'}`}> + 0 ? 'border-red-200 dark:border-red-900 bg-gradient-to-br from-red-50 to-orange-50 dark:from-red-950/30 dark:to-orange-950/30' : 'border bg-muted/30'}`}>
-
0 ? 'bg-red-100' : 'bg-gray-100'}`}> - 0 ? 'text-red-600' : 'text-gray-400'}`} /> +
0 ? 'bg-red-100 dark:bg-red-900/50' : 'bg-muted'}`}> + 0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50'}`} />
-

0 ? 'text-red-700' : 'text-gray-500'}`}>Excedieron Presupuesto

-

0 ? 'text-red-600' : 'text-gray-400'}`}>{overBudget}

-

0 ? 'text-red-600' : 'text-gray-400'}`}>{stats.total > 0 ? Math.round((overBudget / stats.total) * 100) : 0}% de los cerrados

+

0 ? 'text-red-700 dark:text-red-400' : 'text-muted-foreground'}`}>Excedieron Presupuesto

+

0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50'}`}>{overBudget}

+

0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50'}`}>{stats.total > 0 ? Math.round((overBudget / stats.total) * 100) : 0}% de los cerrados

@@ -921,9 +921,9 @@ function CompletedProjectsStats({ projects }: { projects: Project[] }) { if (completedProjects.length === 0) { return (
- -

No hay proyectos completados

-

Aún no se ha completado ningún proyecto al 100%

+ +

No hay proyectos completados

+

Aún no se ha completado ningún proyecto al 100%

); } @@ -997,7 +997,7 @@ function StatisticsSkeleton() {
{Array.from({ length: 4 }).map((_, i) => ( - + @@ -1005,7 +1005,7 @@ function StatisticsSkeleton() {
{Array.from({ length: 3 }).map((_, i) => ( - + @@ -1040,10 +1040,10 @@ export default function StatisticsPage() { if (error) { return ( -
+
- -

{error}

+ +

{error}

@@ -1057,16 +1057,16 @@ export default function StatisticsPage() { const completedCount = projects.filter(p => p.progress >= 100).length; return ( -
-
+
+
-

Estadisticas

-

Analisis de {projects.length} proyectos en Dolibarr

+

Estadisticas

+

Analisis de {projects.length} proyectos en Dolibarr

diff --git a/components/task-detail/index.ts b/components/task-detail/index.ts new file mode 100644 index 0000000..b2fb406 --- /dev/null +++ b/components/task-detail/index.ts @@ -0,0 +1,5 @@ +// components/task-detail/index.ts +export { TaskHeader } from "./task-header"; +export { TaskStats } from "./task-stats"; +export { TaskInfo } from "./task-info"; +export { TaskDetailView } from "./task-detail-view"; diff --git a/components/task-detail/task-detail-view.tsx b/components/task-detail/task-detail-view.tsx new file mode 100644 index 0000000..ea3d33c --- /dev/null +++ b/components/task-detail/task-detail-view.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useState } from "react"; +import { Task } from "@/types/task"; +import { Project } from "@/types/project"; +import { TaskHeader } from "./task-header"; +import { TaskStats } from "./task-stats"; +import { TaskInfo } from "./task-info"; + +interface TaskDetailViewProps { + initialTask: Task; + project: Project | null; +} + +export function TaskDetailView({ initialTask, project }: TaskDetailViewProps) { + const [task, setTask] = useState(initialTask); + + const handleTaskUpdated = (updatedTask: Task) => { + setTask(updatedTask); + }; + + return ( +
+ {/* Header con breadcrumb, título, badges y acciones */} + + + {/* Contenido principal */} +
+ {/* Estadísticas rápidas */} + + + {/* Información detallada */} + +
+
+ ); +} diff --git a/components/task-detail/task-header.tsx b/components/task-detail/task-header.tsx new file mode 100644 index 0000000..8b3785f --- /dev/null +++ b/components/task-detail/task-header.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { useState } from "react"; +import { ArrowLeft, MoreHorizontal, Pencil, Trash2, Clock, Copy, ExternalLink } from "lucide-react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { TaskFormSheet } from "@/components/task-form/task-form-sheet"; +import { + Task, + TaskStatus, + TaskPriority, + TASK_STATUS_CONFIG, + TASK_PRIORITY_CONFIG +} from "@/types/task"; +import { Project } from "@/types/project"; + +interface TaskHeaderProps { + task: Task; + project: Project | null; + onTaskUpdated?: (task: Task) => void; +} + +// Badge de estado +function TaskStatusBadge({ status }: { status: TaskStatus }) { + const config = TASK_STATUS_CONFIG[status]; + return ( + + {config.label} + + ); +} + +// Badge de prioridad +function TaskPriorityBadge({ priority }: { priority: TaskPriority }) { + const config = TASK_PRIORITY_CONFIG[priority]; + if (priority === '0') return null; + + return ( + + {config.label} + + ); +} + +export function TaskHeader({ task, project, onTaskUpdated }: TaskHeaderProps) { + const router = useRouter(); + const [isEditSheetOpen, setIsEditSheetOpen] = useState(false); + + const handleCopyRef = () => { + navigator.clipboard.writeText(task.ref); + }; + + const handleEditSuccess = (updatedTask: Task) => { + onTaskUpdated?.(updatedTask); + }; + + return ( +
+
+ {/* Breadcrumb */} + + + {/* Header principal */} +
+
+ {/* Título y badges */} +
+

{task.title}

+ + +
+ + {/* Referencia y proyecto */} +
+ + + {project && ( + <> + | + + Proyecto: {project.name} + + + + )} +
+
+ + {/* Acciones */} +
+ + + + + + + + setIsEditSheetOpen(true)}> + + Editar tarea + + + + Registrar tiempo + + + + + Eliminar + + + +
+
+
+ + {/* Sheet de edición */} + +
+ ); +} diff --git a/components/task-detail/task-info.tsx b/components/task-detail/task-info.tsx new file mode 100644 index 0000000..1af7411 --- /dev/null +++ b/components/task-detail/task-info.tsx @@ -0,0 +1,303 @@ +"use client"; + +import { + FileText, + Calendar, + User, + FolderOpen, + Tag, + Clock, + AlertTriangle, + CheckCircle2, + Info, + Hash +} from "lucide-react"; +import Link from "next/link"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { + Task, + TASK_STATUS_CONFIG, + TASK_PRIORITY_CONFIG +} from "@/types/task"; +import { Project } from "@/types/project"; + +interface TaskInfoProps { + task: Task; + project: Project | null; +} + +// Formatear fecha completa +function formatFullDate(dateString: string | null): string { + if (!dateString) return 'No definida'; + return new Date(dateString).toLocaleDateString('es-ES', { + weekday: 'long', + day: '2-digit', + month: 'long', + year: 'numeric', + }); +} + +// Verificar si la tarea está vencida +function isOverdue(task: Task): boolean { + const endDate = task.endDate || task.plannedEndDate; + if (!endDate) return false; + return new Date(endDate) < new Date() && task.status !== '2'; +} + +// Item de información +function InfoItem({ + icon: Icon, + label, + value, + className = "", + valueClassName = "" +}: { + icon: React.ElementType; + label: string; + value: React.ReactNode; + className?: string; + valueClassName?: string; +}) { + return ( +
+
+ +
+
+

{label}

+
{value}
+
+
+ ); +} + +export function TaskInfo({ task, project }: TaskInfoProps) { + const overdue = isOverdue(task); + const statusConfig = TASK_STATUS_CONFIG[task.status]; + const priorityConfig = TASK_PRIORITY_CONFIG[task.priority]; + + return ( +
+ {/* Descripción */} + + + + + Descripción + + + + {task.description ? ( +

+ {task.description} +

+ ) : ( +

+ Esta tarea no tiene descripción. +

+ )} +
+
+ + {/* Información General */} + + + + + Información General + + Detalles básicos de la tarea + + + {task.ref}} + /> + + + + + {task.status === '2' && } + {statusConfig.label} + + } + /> + + + {priorityConfig.label} + + ) : ( + Sin prioridad asignada + ) + } + /> + + + + + {project.name} + + ) : ( + Proyecto #{task.projectId} + ) + } + /> + + {task.parentTaskId && task.parentTaskId > 0 && ( + + Ver tarea padre #{task.parentTaskId} + + } + /> + )} + + + + {/* Fechas y Tiempos */} + + + + + Fechas y Tiempos + + Cronograma y dedicación + + + + + + + + + + + {(task.startDate || task.endDate) && ( + <> + + + {task.startDate && ( + + )} + + {task.endDate && ( + + )} + + )} + + + + 0 ? `${task.plannedHours} horas` : 'No estimado'} + /> + + 0 ? `${task.workedHours} horas` : 'Sin registrar'} + /> + + + + {/* Metadatos */} + + + + + Metadatos + + Información de seguimiento y auditoría + + +
+ 0 ? `Usuario #${task.createdBy}` : 'Sistema'} + /> + + + + + + 0 ? `Posición ${task.order}` : 'Sin ordenar'} + /> +
+ + {task.budget > 0 && ( + <> + + + + )} +
+
+
+ ); +} diff --git a/components/task-detail/task-stats.tsx b/components/task-detail/task-stats.tsx new file mode 100644 index 0000000..06354ac --- /dev/null +++ b/components/task-detail/task-stats.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { + Target, + Clock, + Calendar, + TrendingUp, + FileText, + AlertTriangle, + CheckCircle2 +} from "lucide-react"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Task } from "@/types/task"; + +interface TaskStatsProps { + task: Task; +} + +// Formatear fecha completa +function formatDate(dateString: string | null): string { + if (!dateString) return 'No definida'; + return new Date(dateString).toLocaleDateString('es-ES', { + day: '2-digit', + month: 'long', + year: 'numeric', + }); +} + +// Obtener clase de color según progreso +function getProgressColorClass(value: number): string { + if (value >= 100) return '[&>div]:bg-green-500'; + if (value >= 75) return '[&>div]:bg-blue-500'; + if (value >= 50) return '[&>div]:bg-yellow-500'; + if (value >= 25) return '[&>div]:bg-orange-500'; + return '[&>div]:bg-gray-400'; +} + +// Calcular si está vencida +function isOverdue(task: Task): boolean { + const endDate = task.endDate || task.plannedEndDate; + if (!endDate) return false; + return new Date(endDate) < new Date() && task.status !== '2'; +} + +// Calcular días restantes +function getDaysRemaining(task: Task): { days: number; label: string } | null { + const endDate = task.endDate || task.plannedEndDate; + if (!endDate) return null; + + const end = new Date(endDate); + const today = new Date(); + today.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + + const diffTime = end.getTime() - today.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays < 0) { + return { days: Math.abs(diffDays), label: `${Math.abs(diffDays)} días de retraso` }; + } else if (diffDays === 0) { + return { days: 0, label: 'Vence hoy' }; + } else if (diffDays === 1) { + return { days: 1, label: 'Vence mañana' }; + } else { + return { days: diffDays, label: `${diffDays} días restantes` }; + } +} + +export function TaskStats({ task }: TaskStatsProps) { + const overdue = isOverdue(task); + const daysInfo = getDaysRemaining(task); + const efficiency = task.plannedHours > 0 + ? Math.round((task.workedHours / task.plannedHours) * 100) + : 0; + + return ( +
+ {/* Progreso */} + + + Progreso + + + +
{task.progress}%
+ +

+ {task.progress >= 100 ? ( + + + Tarea completada + + ) : task.progress > 0 ? ( + 'En progreso' + ) : ( + 'Sin iniciar' + )} +

+
+
+ + {/* Tiempo */} + + + Tiempo + + + +
+ {task.workedHours > 0 ? `${task.workedHours}h` : '-'} +
+ {task.plannedHours > 0 && ( + 100 ? '[&>div]:bg-red-500' : '[&>div]:bg-blue-500'}`} + /> + )} +

+ {task.plannedHours > 0 ? ( + <> + de {task.plannedHours}h planificadas + {efficiency > 100 && ( + ({efficiency}%) + )} + + ) : ( + 'Sin estimación' + )} +

+
+
+ + {/* Fecha límite */} + + + Fecha límite + + + +
+ {formatDate(task.endDate || task.plannedEndDate).split(' de ')[0] || '-'} +
+

+ {daysInfo ? ( + + {overdue && } + {daysInfo.label} + + ) : ( + 'Sin fecha límite' + )} +

+
+
+ + {/* Eficiencia / Estado */} + + + Rendimiento + + + +
+ {task.plannedHours > 0 && task.workedHours > 0 ? ( + `${Math.round((task.progress / efficiency) * 100)}%` + ) : task.progress > 0 ? ( + 'Activa' + ) : ( + '-' + )} +
+

+ {task.plannedHours > 0 && task.workedHours > 0 ? ( + 'Progreso vs tiempo invertido' + ) : ( + + + {task.description ? 'Con descripción' : 'Sin descripción'} + + )} +

+
+
+
+ ); +} diff --git a/components/task-form/task-form-sheet.tsx b/components/task-form/task-form-sheet.tsx new file mode 100644 index 0000000..a7c7b01 --- /dev/null +++ b/components/task-form/task-form-sheet.tsx @@ -0,0 +1,327 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Loader2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; + +import { + Task, + TaskStatus, + TaskPriority, + TaskFormData, + TASK_STATUS_CONFIG, + TASK_PRIORITY_CONFIG, + taskToFormData, + formDataToCreateTask, + formDataToUpdateTask, +} from "@/types/task"; +import { createTask, updateTask } from "@/lib/tasksService"; + +interface TaskFormSheetProps { + open: boolean; + onOpenChange: (open: boolean) => void; + mode: "create" | "edit"; + projectId: number; + task?: Task | null; + onSuccess?: (task: Task) => void; +} + +const defaultFormData: TaskFormData = { + title: "", + description: "", + projectId: 0, + parentTaskId: null, + status: "0", + priority: "0", + progress: 0, + plannedHours: 0, + startDate: "", + endDate: "", + budget: 0, +}; + +export function TaskFormSheet({ + open, + onOpenChange, + mode, + projectId, + task, + onSuccess, +}: TaskFormSheetProps) { + const [formData, setFormData] = useState({ + ...defaultFormData, + projectId, + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + // Initialize form data when task changes (for edit mode) + useEffect(() => { + if (mode === "edit" && task) { + setFormData(taskToFormData(task)); + } else { + setFormData({ + ...defaultFormData, + projectId, + }); + } + setError(null); + }, [mode, task, projectId, open]); + + const handleInputChange = ( + e: React.ChangeEvent + ) => { + const { name, value, type } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: type === "number" ? parseFloat(value) || 0 : value, + })); + }; + + const handleSelectChange = (name: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setError(null); + + try { + let result: Task; + + if (mode === "create") { + const createData = formDataToCreateTask(formData); + result = await createTask(createData); + } else { + if (!task) { + throw new Error("No hay tarea para editar"); + } + const updateData = formDataToUpdateTask(formData); + result = await updateTask(task.id, updateData); + } + + onSuccess?.(result); + onOpenChange(false); + } catch (err) { + console.error("Error submitting task:", err); + setError( + err instanceof Error ? err.message : "Error al guardar la tarea" + ); + } finally { + setIsSubmitting(false); + } + }; + + const isFormValid = formData.title.trim().length > 0; + + return ( + + + + + {mode === "create" ? "Nueva Tarea" : "Editar Tarea"} + + + {mode === "create" + ? "Crea una nueva tarea para este proyecto." + : "Modifica los detalles de la tarea."} + + + +
+ {/* Título */} +
+ + +
+ + {/* Descripción */} +
+ +