trello_fake/components/task-form/task-form-sheet.tsx

410 lines
13 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useEffect } from "react";
2026-02-06 19:06:25 +00:00
import { Loader2, X, Link2 } 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";
2026-02-06 19:06:25 +00:00
import { Badge } from "@/components/ui/badge";
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";
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
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: [],
};
export function TaskFormSheet({
open,
onOpenChange,
mode,
projectId,
task,
2026-02-06 19:06:25 +00:00
availableTasks = [],
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)
);
// 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;
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>
)}
{/* 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>
);
}