base con autenticacion, registro, modulo encuestas
This commit is contained in:
454
apps/web/feactures/surveys/components/admin/survey-builder.tsx
Normal file
454
apps/web/feactures/surveys/components/admin/survey-builder.tsx
Normal file
@@ -0,0 +1,454 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user