Files
formularios_front/src/containers/create-formulario.tsx
T
miguel d3e1441f87 feat: add bulk user upload functionality and form success page
- Implemented bulk upload for students and workers with file validation and error handling.
- Added Excel template download feature for user uploads.
- Created success page for form submissions with QR code display.
- Established context and reducer for form state management, including user search and response handling.
- Added mermaid diagrams for event registration documentation.
2026-03-08 19:18:23 -06:00

504 lines
16 KiB
TypeScript

import React from 'react';
import {
FormularioCreacion,
TipoPregunta,
TiposValidacion,
} from '@/types/create-formulario';
import SimpleInput from '@/components/input';
import Select, { Option } from '@/components/select';
import Button from '@/components/button';
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
import { formatearRangoFechas } from '@/utils/date-utils';
interface Props {
evento?: GetEventoWithCuestionariosWithCupos;
formulario: FormularioCreacion;
onChange: (formularioActualizado: FormularioCreacion) => void;
tipo_eventos?: Option[];
}
const tipoPreguntaOpciones: TipoPregunta[] = [
'Abierta (Respuesta corta)',
'Abierta (Parrafo)',
'Cerrada',
'Multiple',
];
const tiposValidacion: TiposValidacion[] = [
'correo',
'correo_institucional',
'telefono',
'nombre',
'apellidos',
'genero',
'entero',
'decimal',
'comunidad_alumno',
'cuenta_alumno',
'comunidad_trabajador',
'cuenta_trabajador',
'rfc',
];
type CampoPreguntaSimple =
| 'titulo'
| 'tipo'
| 'validacion'
| 'obligatoria'
| 'limite';
type CampoPreguntaOpciones = 'opciones';
const tiposCuestionario = [
{ label: 'Registro', value: 1 },
{ label: 'Evaluación', value: 2 },
];
export default function FormularioEditor({
formulario,
onChange,
evento,
tipo_eventos,
}: Props) {
function actualizarCampo<K extends keyof FormularioCreacion>(
campo: K,
valor: FormularioCreacion[K]
): void {
const actualizado = { ...formulario, [campo]: valor };
onChange(actualizado);
}
function handleUpdatePregunta(
seccionIdx: number,
preguntaIdx: number,
campo: CampoPreguntaSimple | CampoPreguntaOpciones,
valor:
| string
| boolean
| TipoPregunta
| TiposValidacion
| number
| { valor: string }[]
| undefined
) {
const actualizado = { ...formulario };
const pregunta = actualizado.secciones[seccionIdx].preguntas[preguntaIdx];
if (campo === 'opciones') {
pregunta.opciones = valor as { valor: string }[];
} else if (campo === 'titulo' && typeof valor === 'string') {
pregunta.titulo = valor;
} else if (campo === 'tipo' && typeof valor === 'string') {
pregunta.tipo = valor as TipoPregunta;
} else if (campo === 'validacion' || valor === undefined) {
pregunta.validacion = valor as TiposValidacion | undefined;
} else if (campo === 'obligatoria' && typeof valor === 'boolean') {
pregunta.obligatoria = valor;
} else if (campo === 'limite' && typeof valor === 'number') {
pregunta.limite = valor;
}
onChange(actualizado);
}
const handleUpdateSeccion = (
index: number,
campo: 'titulo' | 'descripcion',
valor: string
) => {
const actualizado = { ...formulario };
actualizado.secciones = actualizado.secciones.map((seccion, idx) =>
idx === index ? { ...seccion, [campo]: valor } : seccion
);
onChange(actualizado);
};
const agregarSeccion = () => {
const nuevaSeccion = {
titulo: '',
descripcion: '',
preguntas: [],
};
onChange({
...formulario,
secciones: [...formulario.secciones, nuevaSeccion],
});
};
const eliminarSeccion = (index: number) => {
const nuevas = [...formulario.secciones];
nuevas.splice(index, 1);
onChange({ ...formulario, secciones: nuevas });
};
const agregarPregunta = (seccionIdx: number) => {
const nuevaPregunta = {
titulo: '',
tipo: 'Abierta (Respuesta corta)' as TipoPregunta,
obligatoria: false,
opciones: [],
};
const actualizado = { ...formulario };
actualizado.secciones[seccionIdx].preguntas.push(nuevaPregunta);
onChange(actualizado);
};
const eliminarPregunta = (seccionIdx: number, preguntaIdx: number) => {
const actualizado = { ...formulario };
actualizado.secciones[seccionIdx].preguntas.splice(preguntaIdx, 1);
onChange(actualizado);
};
// Renderizar solo información básica del formulario
const renderInformacionBasica = () => (
<div className="p-4">
<h4 className="mb-3">Información del Formulario</h4>
<div className="row">
<div className="col-md-8">
<SimpleInput
label="Nombre del Formulario"
value={formulario.nombre_form}
onChange={(e) => actualizarCampo('nombre_form', e.target.value)}
/>
</div>
<div className="col-md-4">
<SimpleInput
label="Cupo máximo"
type="number"
placeholder="Opcional"
value={formulario.cupo_maximo || ''}
onChange={(e) =>
actualizarCampo('cupo_maximo', Number(e.target.value) || 0)
}
/>
</div>
</div>
<SimpleInput
label="Descripción del Formulario"
value={formulario.descripcion}
onChange={(e) => actualizarCampo('descripcion', e.target.value)}
/>
<div className="row">
<div className="col-md-6">
<SimpleInput
label="Fecha de Inicio"
type="datetime-local"
value={formulario.fecha_inicio.slice(0, 16)}
onChange={(e) => actualizarCampo('fecha_inicio', e.target.value)}
/>
</div>
<div className="col-md-6">
<SimpleInput
label="Fecha de Fin"
type="datetime-local"
value={formulario.fecha_fin.slice(0, 16)}
onChange={(e) => actualizarCampo('fecha_fin', e.target.value)}
/>
</div>
</div>
{evento && (
<div className="alert alert-primary d-flex align-items-center">
<i className="bi bi-info-circle-fill me-2"></i>
<div>
<strong>Evento:</strong> {evento.nombre_evento}
<br />
<small>
Se realizará{' '}
{formatearRangoFechas(evento.fecha_inicio, evento.fecha_fin)}
</small>
</div>
</div>
)}
<SimpleInput
label="Mensaje de correo"
value={formulario.mensaje_correo || ''}
onChange={(e) => actualizarCampo('mensaje_correo', e.target.value)}
placeholder="Mensaje que se enviará al correo del participante al registrarse"
/>
<div className="row">
<div className="col-md-6">
<Select
label="Tipo de Cuestionario"
value={formulario.id_tipo_cuestionario}
onChange={(e) =>
actualizarCampo('id_tipo_cuestionario', Number(e.target.value))
}
options={tiposCuestionario}
placeholder="Selecciona un tipo"
/>
</div>
<div className="col-md-6">
<Select
label="Tipo de evento"
value={formulario.id_tipo_evento}
onChange={(e) =>
actualizarCampo('id_tipo_evento', Number(e.target.value))
}
options={tipo_eventos!}
placeholder="Selecciona un tipo"
/>
</div>
</div>
</div>
);
// Renderizar secciones y preguntas
const renderSeccionesYPreguntas = () => (
<div className="mt-4">
<div className="d-flex justify-content-between align-items-center mb-4">
<h4 className="mb-0">Secciones y Preguntas</h4>
<Button
className="btn btn-success"
onClick={agregarSeccion}
icon="plus"
>
Agregar sección
</Button>
</div>
{formulario.secciones.map((seccion, seccionIdx) => (
<div key={seccionIdx} className="mb-4 p-3 border rounded bg-light">
<div className="d-flex justify-content-between align-items-center mb-2">
<h5 className="mb-0">Sección {seccionIdx + 1}</h5>
<Button
variant="danger"
icon="trash"
size="sm"
outline
onClick={() => eliminarSeccion(seccionIdx)}
>
Eliminar sección
</Button>
</div>
<SimpleInput
label="Título de la sección"
value={seccion.titulo}
onChange={(e) =>
handleUpdateSeccion(seccionIdx, 'titulo', e.target.value)
}
/>
<SimpleInput
label="Descripción de la sección"
value={seccion.descripcion}
onChange={(e) =>
handleUpdateSeccion(seccionIdx, 'descripcion', e.target.value)
}
/>
{/* Preguntas */}
{seccion.preguntas.map((pregunta, preguntaIdx) => (
<div key={preguntaIdx} className="mb-3 p-3 border rounded bg-white">
<div className="d-flex justify-content-between align-items-center mb-2">
<h6 className="mb-0">Pregunta {preguntaIdx + 1}</h6>
<Button
variant="danger"
icon="trash"
size="sm"
outline
onClick={() => eliminarPregunta(seccionIdx, preguntaIdx)}
>
Eliminar
</Button>
</div>
<SimpleInput
label="Título de la pregunta"
value={pregunta.titulo}
onChange={(e) =>
handleUpdatePregunta(
seccionIdx,
preguntaIdx,
'titulo',
e.target.value
)
}
/>
<div className="row">
<div className="col-md-6">
<Select
label="Tipo de pregunta"
value={pregunta.tipo}
onChange={(e) => {
const nuevoTipo = e.target.value as TipoPregunta;
handleUpdatePregunta(
seccionIdx,
preguntaIdx,
'tipo',
nuevoTipo
);
// Si cambia a un tipo cerrado y no tiene opciones, agregar opciones por defecto
if (
(nuevoTipo === 'Cerrada' || nuevoTipo === 'Multiple') &&
(!pregunta.opciones || pregunta.opciones.length === 0)
) {
handleUpdatePregunta(
seccionIdx,
preguntaIdx,
'opciones',
[{ valor: '' }, { valor: '' }]
);
}
}}
options={tipoPreguntaOpciones.map((tipo) => ({
label: tipo,
value: tipo,
}))}
/>
</div>
<div className="col-md-6">
<Select
label="Validación"
value={pregunta.validacion || ''}
onChange={(e) =>
handleUpdatePregunta(
seccionIdx,
preguntaIdx,
'validacion',
e.target.value || undefined
)
}
options={[
{ label: 'Ninguna', value: '' },
...tiposValidacion.map((v) => ({
label: v,
value: v,
})),
]}
/>
</div>
</div>
<div className="form-check mb-2">
<input
className="form-check-input"
type="checkbox"
checked={pregunta.obligatoria}
id={`obligatoria-${seccionIdx}-${preguntaIdx}`}
onChange={(e) =>
handleUpdatePregunta(
seccionIdx,
preguntaIdx,
'obligatoria',
e.target.checked
)
}
/>
<label
className="form-check-label"
htmlFor={`obligatoria-${seccionIdx}-${preguntaIdx}`}
>
¿Es obligatoria?
</label>
</div>
{(pregunta.tipo === 'Cerrada' ||
pregunta.tipo === 'Multiple') && (
<div className="mb-2">
<div className="d-flex justify-content-between align-items-center mb-2">
<label className="form-label mb-0">Opciones</label>
<Button
variant="success"
size="sm"
icon="plus"
outline
onClick={() => {
const nuevasOpciones = [...(pregunta.opciones || [])];
nuevasOpciones.push({ valor: '' });
handleUpdatePregunta(
seccionIdx,
preguntaIdx,
'opciones',
nuevasOpciones
);
}}
>
Agregar opción
</Button>
</div>
{(pregunta.opciones || []).map((op, opIdx) => (
<div key={opIdx} className="d-flex align-items-center mb-2">
<SimpleInput
value={op.valor}
onChange={(e) => {
const nuevasOpciones = [...(pregunta.opciones || [])];
nuevasOpciones[opIdx].valor = e.target.value;
handleUpdatePregunta(
seccionIdx,
preguntaIdx,
'opciones',
nuevasOpciones
);
}}
placeholder={`Opción ${opIdx + 1}`}
className={{
container: 'me-2 flex-grow-1 mb-0',
input: 'form-control',
}}
/>
<Button
variant="danger"
size="sm"
icon="trash"
outline
onClick={() => {
const nuevasOpciones = [...(pregunta.opciones || [])];
nuevasOpciones.splice(opIdx, 1);
handleUpdatePregunta(
seccionIdx,
preguntaIdx,
'opciones',
nuevasOpciones
);
}}
disabled={pregunta.opciones?.length === 1}
title={
pregunta.opciones?.length === 1
? 'Una pregunta cerrada debe tener al menos una opción'
: 'Eliminar opción'
}
/>
</div>
))}
{(!pregunta.opciones || pregunta.opciones.length === 0) && (
<div className="text-muted small">
<i className="bi bi-info-circle me-1"></i>
Esta pregunta cerrada no tiene opciones. Agrega al menos
una opción.
</div>
)}
</div>
)}
</div>
))}
<Button
outline
className="mt-2"
size="sm"
icon="plus"
onClick={() => agregarPregunta(seccionIdx)}
>
Agregar pregunta
</Button>
</div>
))}
</div>
);
return {
informacionBasica: renderInformacionBasica,
seccionesYPreguntas: renderSeccionesYPreguntas,
};
}