base con autenticacion, registro, modulo encuestas
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
// Modal para configurar cada pregunta individual
|
||||
// Funcionalidades:
|
||||
// - Configuración específica según el tipo de pregunta
|
||||
// - Para títulos: solo contenido
|
||||
// - Para preguntas simples: texto de la pregunta
|
||||
// - Para preguntas con opciones: texto y lista de opciones
|
||||
// - Switch para hacer la pregunta obligatoria/opcional
|
||||
'use client';
|
||||
|
||||
import { Button } from '@repo/shadcn/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@repo/shadcn/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@repo/shadcn/form';
|
||||
import { Input } from '@repo/shadcn/input';
|
||||
import { Switch } from '@repo/shadcn/switch';
|
||||
import { useEffect } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { QuestionType } from '../../schemas/survey';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
interface QuestionConfigModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
question: any;
|
||||
onSave: (config: any) => void;
|
||||
}
|
||||
|
||||
export function QuestionConfigModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
question,
|
||||
onSave,
|
||||
}: QuestionConfigModalProps) {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
content: '',
|
||||
question: '',
|
||||
required: false,
|
||||
options: [{ id: '1', text: '' }],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'options',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (question) {
|
||||
form.reset({
|
||||
content: question.content || '',
|
||||
question: question.question || '',
|
||||
required: question.required || false,
|
||||
options: question.options || [{ id: '1', text: '' }],
|
||||
});
|
||||
}
|
||||
}, [question, form]);
|
||||
|
||||
const handleSubmit = (data: any) => {
|
||||
const config = {
|
||||
...question,
|
||||
...data,
|
||||
};
|
||||
|
||||
// Remove options if not needed
|
||||
if (![
|
||||
QuestionType.MULTIPLE_CHOICE,
|
||||
QuestionType.SINGLE_CHOICE,
|
||||
QuestionType.SELECT
|
||||
].includes(question.type)) {
|
||||
delete config.options;
|
||||
}
|
||||
|
||||
// Remove content if not a title
|
||||
if (question.type !== QuestionType.TITLE) {
|
||||
delete config.content;
|
||||
}
|
||||
|
||||
onSave(config);
|
||||
};
|
||||
|
||||
const renderFields = () => {
|
||||
switch (question?.type) {
|
||||
case QuestionType.TITLE:
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Contenido del Título</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
case QuestionType.SIMPLE:
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="question"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Pregunta</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
case QuestionType.MULTIPLE_CHOICE:
|
||||
case QuestionType.SINGLE_CHOICE:
|
||||
case QuestionType.SELECT:
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="question"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Pregunta</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<FormLabel>Opciones</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => append({ id: `${fields.length + 1}`, text: '' })}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Agregar Opción
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[200px] overflow-y-auto pr-2 space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`options.${index}.text`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={`Opción ${index + 1}`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{fields.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
className="max-w-2xl"
|
||||
aria-describedby="question-config-description"
|
||||
>
|
||||
<div id="question-config-description" className="sr-only">
|
||||
Configuración de la pregunta de la encuesta
|
||||
</div>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Configurar Pregunta</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||
{renderFields()}
|
||||
|
||||
{question?.type !== QuestionType.TITLE && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="required"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Respuesta Obligatoria</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit">Guardar</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Caja de herramientas con tipos de preguntas disponibles
|
||||
// Funcionalidades:
|
||||
// - Lista de elementos arrastrables
|
||||
// - Tipos disponibles: Título, Pregunta Simple, Opción Múltiple, Opción Única, Selección
|
||||
// - Cada elemento es arrastrable al área de construcción
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent } from '@repo/shadcn/card';
|
||||
import { QuestionType } from '../../schemas/survey';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
const questionTypes = [
|
||||
{
|
||||
type: QuestionType.TITLE,
|
||||
label: 'Título',
|
||||
icon: '📝',
|
||||
},
|
||||
{
|
||||
type: QuestionType.SIMPLE,
|
||||
label: 'Pregunta Simple',
|
||||
icon: '✏️',
|
||||
},
|
||||
{
|
||||
type: QuestionType.MULTIPLE_CHOICE,
|
||||
label: 'Opción Múltiple',
|
||||
icon: '☑️',
|
||||
},
|
||||
{
|
||||
type: QuestionType.SINGLE_CHOICE,
|
||||
label: 'Opción Única',
|
||||
icon: '⭕',
|
||||
},
|
||||
{
|
||||
type: QuestionType.SELECT,
|
||||
label: 'Selección',
|
||||
icon: '📋',
|
||||
},
|
||||
];
|
||||
|
||||
function DraggableItem({ type, label, icon }: { type: string; label: string; icon: string }) {
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: type,
|
||||
data: {
|
||||
type,
|
||||
isTemplate: true,
|
||||
},
|
||||
});
|
||||
|
||||
const style = transform ? {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
} : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
className="p-3 bg-background border rounded-lg cursor-move hover:bg-accent touch-none"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{icon}</span>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function QuestionToolbox() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<h3 className="font-semibold mb-4">Elementos Disponibles</h3>
|
||||
<div className="space-y-2">
|
||||
{questionTypes.map((item) => (
|
||||
<DraggableItem
|
||||
key={item.type}
|
||||
type={item.type}
|
||||
label={item.label}
|
||||
icon={item.icon}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { DataTable } from '@repo/shadcn/table/data-table';
|
||||
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
||||
import { columns } from './surveys-tables/columns';
|
||||
import { useSurveysQuery } from '../../hooks/use-query-surveys';
|
||||
|
||||
interface SurveysAdminListProps {
|
||||
initialPage: number;
|
||||
initialSearch?: string | null;
|
||||
initialLimit: number;
|
||||
initialType?: string | null;
|
||||
}
|
||||
|
||||
export default function SurveysAdminList({
|
||||
initialPage,
|
||||
initialSearch,
|
||||
initialLimit,
|
||||
initialType,
|
||||
}: SurveysAdminListProps) {
|
||||
const filters = {
|
||||
page: initialPage,
|
||||
limit: initialLimit,
|
||||
...(initialSearch && { search: initialSearch }),
|
||||
...(initialType && { type: initialType }),
|
||||
};
|
||||
|
||||
const {data, isLoading} = useSurveysQuery(filters)
|
||||
|
||||
if (isLoading) {
|
||||
return <DataTableSkeleton columnCount={6} rowCount={initialLimit} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data?.data || []}
|
||||
totalItems={data?.meta.totalCount || 0}
|
||||
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@repo/shadcn/button';
|
||||
import { Heading } from '@repo/shadcn/heading';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
|
||||
export function SurveysHeader() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start justify-between">
|
||||
<Heading
|
||||
title="Administración de Encuestas"
|
||||
description="Gestiona las encuestas disponibles en la plataforma"
|
||||
/>
|
||||
<Button onClick={() => router.push(`/dashboard/administracion/encuestas/crear`)} size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" /> Agregar Encuesta
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AlertModal } from '@/components/modal/alert-modal';
|
||||
import { Button } from '@repo/shadcn/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@repo/shadcn/tooltip';
|
||||
import { Edit, Trash } from 'lucide-react';
|
||||
import { SurveyTable } from '@/feactures/surveys/schemas/survey';
|
||||
import { useDeleteSurvey } from '@/feactures/surveys/hooks/use-mutation-surveys';
|
||||
|
||||
|
||||
interface CellActionProps {
|
||||
data: SurveyTable;
|
||||
}
|
||||
|
||||
export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const { mutate: deleteSurvey } = useDeleteSurvey();
|
||||
const router = useRouter();
|
||||
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
deleteSurvey(data.id!);
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onConfirm={onConfirm}
|
||||
loading={loading}
|
||||
title="¿Estás seguro que desea eliminar la encuesta?"
|
||||
description="Esta acción no se puede deshacer."
|
||||
/>
|
||||
|
||||
|
||||
<div className="flex gap-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => router.push(`/dashboard/administracion/encuestas/editar/${data.id!}`)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Editar</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Eliminar</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Badge } from "@repo/shadcn/badge";
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { CellAction } from './cell-action';
|
||||
import { SurveyTable } from '@/feactures/surveys/schemas/survey';
|
||||
|
||||
export const columns: ColumnDef<SurveyTable>[] = [
|
||||
{
|
||||
accessorKey: 'title',
|
||||
header: 'Título',
|
||||
},
|
||||
{
|
||||
accessorKey: "published",
|
||||
header: "Estado",
|
||||
cell: ({ row }) => {
|
||||
const published = row.getValue("published");
|
||||
return (
|
||||
<Badge variant={published == 'Publicada' ? "default" : "secondary"}>
|
||||
{published == 'Publicada' ? 'Publicada' : 'Borrador'}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => <CellAction data={row.original} />,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { DataTableFilterBox } from '@repo/shadcn/table/data-table-filter-box';
|
||||
import { DataTableSearch } from '@repo/shadcn/table/data-table-search';
|
||||
import {
|
||||
TYPE_OPTIONS,
|
||||
useSurveyTableFilters,
|
||||
} from './use-survey-table-filters';
|
||||
|
||||
export default function SurveysTableAction() {
|
||||
const {
|
||||
typeFilter,
|
||||
searchQuery,
|
||||
setPage,
|
||||
setTypeFilter,
|
||||
setSearchQuery,
|
||||
} = useSurveyTableFilters();
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-4 pt-2">
|
||||
<DataTableSearch
|
||||
searchKey={searchQuery}
|
||||
searchQuery={searchQuery || ''}
|
||||
setSearchQuery={setSearchQuery}
|
||||
setPage={setPage}
|
||||
/>
|
||||
<DataTableFilterBox
|
||||
filterKey="type"
|
||||
title="Estado"
|
||||
options={TYPE_OPTIONS}
|
||||
setFilterValue={setTypeFilter}
|
||||
filterValue={typeFilter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { PUBLISHED_TYPES } from '@/feactures/surveys/schemas/surveys-options';
|
||||
import { searchParams } from '@repo/shadcn/lib/searchparams';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
|
||||
export const TYPE_OPTIONS = Object.entries(PUBLISHED_TYPES).map(
|
||||
([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
export function useSurveyTableFilters() {
|
||||
const [searchQuery, setSearchQuery] = useQueryState(
|
||||
'q',
|
||||
searchParams.q
|
||||
.withOptions({
|
||||
shallow: false,
|
||||
throttleMs: 500, // Add 500ms delay
|
||||
// Removed dedupingInterval as it's not a valid option
|
||||
})
|
||||
.withDefault(''),
|
||||
);
|
||||
|
||||
const [typeFilter, setTypeFilter] = useQueryState(
|
||||
'published',
|
||||
searchParams.q.withOptions({ shallow: false }).withDefault(''),
|
||||
);
|
||||
|
||||
const [page, setPage] = useQueryState(
|
||||
'page',
|
||||
searchParams.page.withDefault(1),
|
||||
);
|
||||
|
||||
const resetFilters = useCallback(() => {
|
||||
setSearchQuery(null);
|
||||
setTypeFilter(null);
|
||||
setPage(1);
|
||||
}, [setSearchQuery, setPage]);
|
||||
|
||||
const isAnyFilterActive = useMemo(() => {
|
||||
return !!searchQuery || !!typeFilter;
|
||||
}, [searchQuery]);
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
page,
|
||||
setPage,
|
||||
resetFilters,
|
||||
isAnyFilterActive,
|
||||
typeFilter,
|
||||
setTypeFilter
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user