2026-01-31 12:56:51 +00:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from "react";
|
2026-02-06 19:06:25 +00:00
|
|
|
import { Loader2, X, Link2 } from "lucide-react";
|
2026-01-31 12:56:51 +00:00
|
|
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Textarea } from "@/components/ui/textarea";
|
2026-02-06 19:06:25 +00:00
|
|
|
import { Badge } from "@/components/ui/badge";
|
2026-01-31 12:56:51 +00:00
|
|
|
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";
|
2026-02-06 19:06:25 +00:00
|
|
|
import { createTask, updateTask, setTaskDependencies } from "@/lib/tasksService";
|
2026-01-31 12:56:51 +00:00
|
|
|
|
|
|
|
|
interface TaskFormSheetProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
mode: "create" | "edit";
|
|
|
|
|
projectId: number;
|
|
|
|
|
task?: Task | null;
|
2026-02-06 19:06:25 +00:00
|
|
|
availableTasks?: Task[]; // Tareas del mismo proyecto para seleccionar como dependencias
|
2026-01-31 12:56:51 +00:00
|
|
|
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,
|
2026-02-06 19:06:25 +00:00
|
|
|
dependencies: [],
|
2026-01-31 12:56:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function TaskFormSheet({
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
mode,
|
|
|
|
|
projectId,
|
|
|
|
|
task,
|
2026-02-06 19:06:25 +00:00
|
|
|
availableTasks = [],
|
2026-01-31 12:56:51 +00:00
|
|
|
onSuccess,
|
|
|
|
|
}: TaskFormSheetProps) {
|
|
|
|
|
const [formData, setFormData] = useState<TaskFormData>({
|
|
|
|
|
...defaultFormData,
|
|
|
|
|
projectId,
|
|
|
|
|
});
|
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
2026-02-06 19:06:25 +00:00
|
|
|
// Tareas seleccionables como dependencia (excluir la tarea actual en modo edición)
|
|
|
|
|
const selectableTasks = availableTasks.filter(
|
|
|
|
|
(t) => !(mode === "edit" && task && t.id === task.id)
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-31 12:56:51 +00:00
|
|
|
// 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<HTMLInputElement | HTMLTextAreaElement>
|
|
|
|
|
) => {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 19:06:25 +00:00
|
|
|
// Guardar dependencias
|
|
|
|
|
setTaskDependencies(result.id, formData.dependencies);
|
|
|
|
|
result.dependencies = formData.dependencies;
|
|
|
|
|
|
2026-01-31 12:56:51 +00:00
|
|
|
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 (
|
|
|
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<SheetContent className="sm:max-w-lg overflow-y-auto">
|
|
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle>
|
|
|
|
|
{mode === "create" ? "Nueva Tarea" : "Editar Tarea"}
|
|
|
|
|
</SheetTitle>
|
|
|
|
|
<SheetDescription>
|
|
|
|
|
{mode === "create"
|
|
|
|
|
? "Crea una nueva tarea para este proyecto."
|
|
|
|
|
: "Modifica los detalles de la tarea."}
|
|
|
|
|
</SheetDescription>
|
|
|
|
|
</SheetHeader>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit} className="mt-6 space-y-6">
|
|
|
|
|
{/* Título */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="title">
|
|
|
|
|
Título <span className="text-red-500">*</span>
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="title"
|
|
|
|
|
name="title"
|
|
|
|
|
value={formData.title}
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
placeholder="Nombre de la tarea"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Descripción */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="description">Descripción</Label>
|
|
|
|
|
<Textarea
|
|
|
|
|
id="description"
|
|
|
|
|
name="description"
|
|
|
|
|
value={formData.description}
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
placeholder="Descripción detallada de la tarea..."
|
|
|
|
|
rows={3}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Estado y Prioridad - Grid de 2 columnas */}
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
{/* Estado */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="status">Estado</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={formData.status}
|
|
|
|
|
onValueChange={(value) => handleSelectChange("status", value)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Seleccionar estado" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{(Object.keys(TASK_STATUS_CONFIG) as TaskStatus[]).map(
|
|
|
|
|
(status) => (
|
|
|
|
|
<SelectItem key={status} value={status}>
|
|
|
|
|
{TASK_STATUS_CONFIG[status].label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Prioridad */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="priority">Prioridad</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={formData.priority}
|
|
|
|
|
onValueChange={(value) => handleSelectChange("priority", value)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Seleccionar prioridad" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{(Object.keys(TASK_PRIORITY_CONFIG) as TaskPriority[]).map(
|
|
|
|
|
(priority) => (
|
|
|
|
|
<SelectItem key={priority} value={priority}>
|
|
|
|
|
{TASK_PRIORITY_CONFIG[priority].label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Progreso */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="progress">Progreso (%)</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="progress"
|
|
|
|
|
name="progress"
|
|
|
|
|
type="number"
|
|
|
|
|
min="0"
|
|
|
|
|
max="100"
|
|
|
|
|
value={formData.progress}
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
placeholder="0"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Fechas - Grid de 2 columnas */}
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="startDate">Fecha inicio</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="startDate"
|
|
|
|
|
name="startDate"
|
|
|
|
|
type="date"
|
|
|
|
|
value={formData.startDate}
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="endDate">Fecha fin</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="endDate"
|
|
|
|
|
name="endDate"
|
|
|
|
|
type="date"
|
|
|
|
|
value={formData.endDate}
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Horas planificadas y Presupuesto - Grid de 2 columnas */}
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="plannedHours">Horas planificadas</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="plannedHours"
|
|
|
|
|
name="plannedHours"
|
|
|
|
|
type="number"
|
|
|
|
|
min="0"
|
|
|
|
|
step="0.5"
|
|
|
|
|
value={formData.plannedHours}
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
placeholder="0"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="budget">Presupuesto (€)</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="budget"
|
|
|
|
|
name="budget"
|
|
|
|
|
type="number"
|
|
|
|
|
min="0"
|
|
|
|
|
step="0.01"
|
|
|
|
|
value={formData.budget}
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
placeholder="0.00"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-06 19:06:25 +00:00
|
|
|
{/* Dependencias (tareas predecesoras) */}
|
|
|
|
|
{selectableTasks.length > 0 && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="flex items-center gap-2">
|
|
|
|
|
<Link2 className="h-4 w-4" />
|
|
|
|
|
Depende de (predecesoras)
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Selecciona las tareas que deben completarse antes de esta.
|
|
|
|
|
</p>
|
|
|
|
|
<Select
|
|
|
|
|
value=""
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
const taskId = parseInt(value);
|
|
|
|
|
if (!formData.dependencies.includes(taskId)) {
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
dependencies: [...prev.dependencies, taskId],
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Añadir tarea predecesora..." />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{selectableTasks
|
|
|
|
|
.filter(t => !formData.dependencies.includes(t.id))
|
|
|
|
|
.map((t) => (
|
|
|
|
|
<SelectItem key={t.id} value={String(t.id)}>
|
|
|
|
|
{t.title} ({t.ref})
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
{/* Lista de dependencias seleccionadas */}
|
|
|
|
|
{formData.dependencies.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
|
|
|
{formData.dependencies.map((depId) => {
|
|
|
|
|
const depTask = availableTasks.find(t => t.id === depId);
|
|
|
|
|
return (
|
|
|
|
|
<Badge
|
|
|
|
|
key={depId}
|
|
|
|
|
variant="secondary"
|
|
|
|
|
className="flex items-center gap-1 pl-2 pr-1"
|
|
|
|
|
>
|
|
|
|
|
<span className="text-xs truncate max-w-[150px]">
|
|
|
|
|
{depTask?.title || `Tarea #${depId}`}
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
dependencies: prev.dependencies.filter(id => id !== depId),
|
|
|
|
|
}));
|
|
|
|
|
}}
|
|
|
|
|
className="ml-1 rounded-full p-0.5 hover:bg-muted-foreground/20 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</Badge>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-31 12:56:51 +00:00
|
|
|
{/* Error message */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950/50 dark:text-red-400 rounded-md">
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Submit button */}
|
|
|
|
|
<SheetFooter className="pt-4">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onOpenChange(false)}
|
|
|
|
|
disabled={isSubmitting}
|
|
|
|
|
>
|
|
|
|
|
Cancelar
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="submit" disabled={!isFormValid || isSubmitting}>
|
|
|
|
|
{isSubmitting && (
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
)}
|
|
|
|
|
{mode === "create" ? "Crear tarea" : "Guardar cambios"}
|
|
|
|
|
</Button>
|
|
|
|
|
</SheetFooter>
|
|
|
|
|
</form>
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
|
|
|
|
);
|
|
|
|
|
}
|