454 lines
15 KiB
TypeScript
454 lines
15 KiB
TypeScript
// Componente principal para crear/editar encuestas
|
|
// Funcionalidades:
|
|
// - Formulario para datos básicos (título, descripción, fecha de cierre)
|
|
// - Sistema de drag & drop para agregar preguntas
|
|
// - Reordenamiento de preguntas existentes
|
|
// - Guardado como borrador o publicación directa'use client';
|
|
'use client';
|
|
|
|
import { Button } from '@repo/shadcn/button';
|
|
import { Card, CardContent } from '@repo/shadcn/card';
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@repo/shadcn/form';
|
|
import { Input } from '@repo/shadcn/input';
|
|
import { Textarea } from '@repo/shadcn/textarea';
|
|
import { CalendarIcon } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { toast } from 'sonner';
|
|
import { QuestionType, Survey } from '../../schemas/survey';
|
|
import { QuestionConfigModal } from './question-config-modal';
|
|
import { QuestionToolbox } from './question-toolbox';
|
|
import { cn } from '@repo/shadcn/lib/utils';
|
|
import { Calendar } from '@repo/shadcn/calendar';
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@repo/shadcn/popover';
|
|
import { format } from 'date-fns';
|
|
import { DndContext, DragEndEvent, useSensor, useSensors, PointerSensor } from '@dnd-kit/core';
|
|
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
|
|
import { CSS } from '@dnd-kit/utilities';
|
|
import { useDroppable } from '@dnd-kit/core';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@repo/shadcn/select";
|
|
import { useParams, useRouter } from 'next/navigation';
|
|
|
|
// Añade el import de Trash2
|
|
import { Trash2 } from 'lucide-react';
|
|
import { useSurveysByIdQuery } from '../../hooks/use-query-surveys';
|
|
import { useSurveyMutation } from '../../hooks/use-mutation-surveys';
|
|
|
|
|
|
function SortableQuestion({
|
|
question,
|
|
index,
|
|
onDelete,
|
|
onEdit
|
|
}: {
|
|
question: any;
|
|
index: number;
|
|
onDelete: (id: string) => void;
|
|
onEdit: (question: any) => void;
|
|
}) {
|
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
|
id: question.id,
|
|
});
|
|
|
|
const style = {
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
};
|
|
|
|
return (
|
|
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
|
<Card className="mb-4">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div
|
|
className="flex-1 cursor-pointer"
|
|
onClick={() => onEdit(question)}
|
|
>
|
|
<span>{question.question || question.content}</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDelete(question.id);
|
|
}}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DroppableArea({ children }: { children: React.ReactNode }) {
|
|
const { setNodeRef } = useDroppable({
|
|
id: 'questions-container',
|
|
});
|
|
|
|
return (
|
|
<div
|
|
ref={setNodeRef}
|
|
className="min-h-[200px] border-2 border-dashed rounded-lg p-4 mt-6"
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SurveyBuilder() {
|
|
const [questions, setQuestions] = useState<any[]>([]);
|
|
const [selectedQuestion, setSelectedQuestion] = useState<any>(null);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const params = useParams();
|
|
const router = useRouter();
|
|
const surveyId = params?.id as string;
|
|
const isEditing = Boolean(surveyId);
|
|
|
|
|
|
const form = useForm({
|
|
defaultValues: {
|
|
title: '',
|
|
description: '',
|
|
closingDate: undefined as Date | undefined,
|
|
targetAudience: '', // Nuevo campo
|
|
},
|
|
});
|
|
|
|
const {
|
|
mutate: MutateSurvey,
|
|
} = useSurveyMutation()
|
|
|
|
|
|
// Remove the loadSurvey function and use the query hook at component level
|
|
if (isEditing) {
|
|
const { data: surveyById, isLoading } = useSurveysByIdQuery(parseInt(surveyId))
|
|
|
|
// Use useEffect to handle the form reset when data is available
|
|
useEffect(() => {
|
|
// console.log(isEditing ? parseInt(surveyId) : 0);
|
|
if (surveyById?.data && !isLoading) {
|
|
form.reset({
|
|
title: surveyById.data.title,
|
|
description: surveyById.data.description,
|
|
closingDate: surveyById.data.closingDate || undefined,
|
|
targetAudience: surveyById.data.targetAudience,
|
|
});
|
|
// Fix: Set the questions directly without wrapping in array
|
|
setQuestions(surveyById.data.questions || []);
|
|
}
|
|
}, [surveyById, isLoading, form]);
|
|
}
|
|
|
|
|
|
// Remove the loadSurvey() call from the component body
|
|
|
|
|
|
|
|
// Procesa la configuración de una pregunta después de cerrar el modal
|
|
// Actualiza o agrega la pregunta al listado
|
|
const handleQuestionConfig = (questionConfig: any) => {
|
|
if (selectedQuestion) {
|
|
const updatedQuestions = [...questions];
|
|
const index = updatedQuestions.findIndex(q => q.id === selectedQuestion.id);
|
|
|
|
if (index === -1) {
|
|
updatedQuestions.push({
|
|
...selectedQuestion,
|
|
...questionConfig,
|
|
});
|
|
} else {
|
|
updatedQuestions[index] = {
|
|
...selectedQuestion,
|
|
...questionConfig,
|
|
};
|
|
}
|
|
|
|
setQuestions(updatedQuestions);
|
|
}
|
|
setIsModalOpen(false);
|
|
};
|
|
|
|
// Maneja el guardado de la encuesta
|
|
// Valida campos requeridos y guarda como borrador o publicada
|
|
const handleSave = async (status: 'draft' | 'published') => {
|
|
const formData = form.getValues();
|
|
|
|
// validar que los campos no esten vacíos
|
|
if (!formData.title) return toast.error('El título es obligatorio')
|
|
if (!formData.description) return toast.error('La descripción es obligatorio')
|
|
if (!formData.targetAudience) return toast.error('El público objetivo es obligatorio')
|
|
if (!formData.closingDate) return toast.error('La fecha de cierre es obligatorio')
|
|
if (questions.length === 0) return toast.error('Debe agregar al menos una pregunta');
|
|
|
|
const surveyData: Omit<Survey, 'created_at'> = {
|
|
title: formData.title,
|
|
description: formData.description,
|
|
closingDate: formData.closingDate,
|
|
targetAudience: formData.targetAudience,
|
|
published: status === 'published',
|
|
questions: questions.map((q, index) => ({ ...q, position: index })),
|
|
};
|
|
|
|
try {
|
|
await MutateSurvey({
|
|
...surveyData,
|
|
id: isEditing ? parseInt(surveyId) : undefined,
|
|
}, {
|
|
onSuccess: () => {
|
|
toast.success(
|
|
isEditing
|
|
? 'Encuesta actualizada exitosamente'
|
|
: status === 'published'
|
|
? 'Encuesta publicada'
|
|
: 'Encuesta guardada como borrador'
|
|
);
|
|
router.push('/dashboard/administracion/encuestas');
|
|
},
|
|
onError: (e) => {
|
|
toast.error(e.message)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
toast.error( `Error al ${isEditing ? 'actualizar' : 'guardar'} la encuesta`)
|
|
}
|
|
};
|
|
|
|
|
|
// Configuración de los sensores para el drag and drop
|
|
// Define la distancia mínima para activar el arrastre
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 8,
|
|
},
|
|
})
|
|
);
|
|
|
|
// Manejador del evento cuando se termina de arrastrar un elemento
|
|
// Gestiona tanto nuevas preguntas como reordenamiento
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
const { active, over } = event;
|
|
|
|
if (!over) return;
|
|
|
|
if (active.data.current?.isTemplate) {
|
|
// Handle new question from toolbox
|
|
const questionType = active.data.current.type;
|
|
const newQuestion = {
|
|
id: `q-${questions.length + 1}`,
|
|
type: questionType as QuestionType,
|
|
position: questions.length,
|
|
required: false,
|
|
};
|
|
setSelectedQuestion(newQuestion);
|
|
setIsModalOpen(true);
|
|
} else {
|
|
// Handle reordering of existing questions
|
|
const oldIndex = questions.findIndex(q => q.id === active.id);
|
|
const newIndex = questions.findIndex(q => q.id === over.id);
|
|
|
|
if (oldIndex !== newIndex) {
|
|
const updatedQuestions = [...questions];
|
|
const [movedQuestion] = updatedQuestions.splice(oldIndex, 1);
|
|
updatedQuestions.splice(newIndex, 0, movedQuestion);
|
|
setQuestions(updatedQuestions);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Añade estas funciones manejadoras
|
|
const handleDeleteQuestion = (id: string) => {
|
|
setQuestions(questions.filter(q => q.id !== id));
|
|
};
|
|
|
|
const handleEditQuestion = (question: any) => {
|
|
setSelectedQuestion(question);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
return (
|
|
<DndContext
|
|
sensors={sensors}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<div className="flex gap-6">
|
|
<div className="w-64">
|
|
<QuestionToolbox />
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<Form {...form}>
|
|
<form className="space-y-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="title"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Título de la Encuesta</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="description"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Descripción</FormLabel>
|
|
<FormControl>
|
|
<Textarea {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="closingDate"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Fecha de Cierre</FormLabel>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<FormControl>
|
|
<Button
|
|
variant="outline"
|
|
className={cn(
|
|
"w-full pl-3 text-left font-normal",
|
|
!field.value && "text-muted-foreground"
|
|
)}
|
|
>
|
|
{field.value ? (
|
|
format(field.value, "PPP")
|
|
) : (
|
|
<span>Seleccione una fecha</span>
|
|
)}
|
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
</Button>
|
|
</FormControl>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-0" align="start">
|
|
<Calendar
|
|
mode="single"
|
|
selected={field.value}
|
|
onSelect={field.onChange}
|
|
disabled={(date) =>
|
|
date < new Date()
|
|
}
|
|
initialFocus
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="targetAudience"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Dirigido a</FormLabel>
|
|
<Select
|
|
onValueChange={field.onChange}
|
|
value={field.value}
|
|
defaultValue={field.value}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Seleccione el público objetivo" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="producers">Productores</SelectItem>
|
|
<SelectItem value="organization">Organización</SelectItem>
|
|
<SelectItem value="all">Todos</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
|
|
<DroppableArea>
|
|
<SortableContext
|
|
items={questions.map(q => q.id)}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
{questions.map((question, index) => (
|
|
<SortableQuestion
|
|
key={question.id}
|
|
question={question}
|
|
index={index}
|
|
onDelete={handleDeleteQuestion}
|
|
onEdit={handleEditQuestion}
|
|
/>
|
|
))}
|
|
{questions.length === 0 && (
|
|
<div className="text-center text-muted-foreground p-4">
|
|
Arrastra elementos aquí para crear la encuesta
|
|
</div>
|
|
)}
|
|
</SortableContext>
|
|
</DroppableArea>
|
|
|
|
<div className="flex justify-end gap-4 mt-6">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => router.push('/dashboard/administracion/encuestas')}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button variant="outline" onClick={() => handleSave('draft')}>
|
|
Guardar como Borrador
|
|
</Button>
|
|
<Button onClick={() => handleSave('published')}>
|
|
Publicar
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<QuestionConfigModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => setIsModalOpen(false)}
|
|
question={selectedQuestion}
|
|
onSave={handleQuestionConfig}
|
|
/>
|
|
</div>
|
|
</DndContext>
|
|
);
|
|
} |