import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' import { useState, useEffect, useRef } from 'react' import SignatureCanvas from 'react-signature-canvas' import Sidebar from './Sidebar' import QuestionTypeEditor from './QuestionTypeEditor' import QuestionAnswerInput from './QuestionAnswerInput' function App() { const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { // Verificar si hay token guardado const token = localStorage.getItem('token') const userData = localStorage.getItem('user') if (token && userData) { setUser(JSON.parse(userData)) } setLoading(false) }, []) if (loading) { return (
Cargando...
) } return (
{!user ? ( ) : ( )}
) } // Componente de Login function LoginPage({ setUser }) { const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) const [logoUrl, setLogoUrl] = useState(null); useEffect(() => { const fetchLogo = async () => { try { const API_URL = import.meta.env.VITE_API_URL || ''; const res = await fetch(`${API_URL}/api/config/logo`); if (res.ok) { const data = await res.json(); setLogoUrl(data.logo_url); } } catch {} }; fetchLogo(); }, []); const handleLogin = async (e) => { e.preventDefault() setError('') setLoading(true) try { const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }), }) const data = await response.json() if (!response.ok) { throw new Error(data.detail || 'Error al iniciar sesión') } console.log('Login successful, token:', data.access_token.substring(0, 20) + '...') // Guardar token y usuario localStorage.setItem('token', data.access_token) localStorage.setItem('user', JSON.stringify(data.user)) setUser(data.user) } catch (err) { setError(err.message) } finally { setLoading(false) } } return (
{/* Header con Logo */}
{logoUrl ? ( Logo ) : (
Sin logo
)}

AYUTEC

Sistema Inteligente de Inspecciones

{/* Formulario */}
setUsername(e.target.value)} className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" placeholder="Ingresa tu usuario" required />
setPassword(e.target.value)} className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" placeholder="••••••••" required />
{error && (
⚠️ {error}
)}
) } // Componente Dashboard function DashboardPage({ user, setUser }) { const [checklists, setChecklists] = useState([]) const [inspections, setInspections] = useState([]) const [loading, setLoading] = useState(true) const [activeTab, setActiveTab] = useState('checklists') const [activeInspection, setActiveInspection] = useState(null) const [sidebarOpen, setSidebarOpen] = useState(true) const [logoUrl, setLogoUrl] = useState(null); useEffect(() => { const fetchLogo = async () => { try { const API_URL = import.meta.env.VITE_API_URL || ''; const res = await fetch(`${API_URL}/api/config/logo`); if (res.ok) { const data = await res.json(); setLogoUrl(data.logo_url); } } catch {} }; fetchLogo(); }, []); useEffect(() => { loadData() }, []) const loadData = async () => { try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' console.log('Token:', token ? 'exists' : 'missing') if (!token) { console.warn('No token found, redirecting to login') setUser(null) setLoading(false) return } // Cargar checklists const checklistsRes = await fetch(`${API_URL}/api/checklists?active_only=true`, { headers: { 'Authorization': `Bearer ${token}`, }, }) console.log('Checklists response:', checklistsRes.status) if (checklistsRes.status === 401) { console.warn('Token expired or invalid, logging out') localStorage.removeItem('token') localStorage.removeItem('user') setUser(null) setLoading(false) return } if (checklistsRes.ok) { const checklistsData = await checklistsRes.json() console.log('Checklists data:', checklistsData) // Ordenar por ID descendente para mantener orden consistente const sortedChecklists = Array.isArray(checklistsData) ? checklistsData.sort((a, b) => b.id - a.id) : [] setChecklists(sortedChecklists) } else { console.error('Error loading checklists:', checklistsRes.status) setChecklists([]) } // Cargar inspecciones const inspectionsRes = await fetch(`${API_URL}/api/inspections?limit=10`, { headers: { 'Authorization': `Bearer ${token}`, }, }) console.log('Inspections response:', inspectionsRes.status) if (inspectionsRes.ok) { const inspectionsData = await inspectionsRes.json() console.log('Inspections data:', inspectionsData) // Ordenar por ID descendente para mantener orden consistente const sortedInspections = Array.isArray(inspectionsData) ? inspectionsData.sort((a, b) => b.id - a.id) : [] setInspections(sortedInspections) } else { console.error('Error loading inspections:', inspectionsRes.status) setInspections([]) } } catch (error) { console.error('Error loading data:', error) setChecklists([]) setInspections([]) } finally { setLoading(false) } } const handleLogout = () => { localStorage.removeItem('token') localStorage.removeItem('user') setUser(null) } return (
{/* Sidebar */} {/* Main Content */}
{/* Header */}
{/* Logo y Nombre del Sistema */}
{logoUrl ? ( Logo ) : (
Sin logo
)}

AYUTEC

Sistema Inteligente de Inspecciones

{/* Sección Activa */}
{activeTab === 'checklists' && '📋'} {activeTab === 'inspections' && '🔍'} {activeTab === 'users' && '👥'} {activeTab === 'reports' && '📊'} {activeTab === 'settings' && '⚙️'} {activeTab === 'checklists' && 'Checklists'} {activeTab === 'inspections' && 'Inspecciones'} {activeTab === 'users' && 'Usuarios'} {activeTab === 'reports' && 'Reportes'} {activeTab === 'settings' && 'Configuración'}
{/* Content */}
{loading ? (
Cargando datos...
) : activeTab === 'checklists' ? ( ) : activeTab === 'inspections' ? ( ) : activeTab === 'settings' ? ( ) : activeTab === 'api-tokens' ? ( ) : activeTab === 'users' ? ( ) : activeTab === 'reports' ? ( ) : null}
{/* Modal de Inspección Activa */} {activeInspection && ( setActiveInspection(null)} onComplete={() => { setActiveInspection(null) loadData() }} /> )}
) } function SettingsTab({ user }) { // Estado para el logo const [logoUrl, setLogoUrl] = useState(null); const [logoUploading, setLogoUploading] = useState(false); const [aiConfig, setAiConfig] = useState(null); const [availableModels, setAvailableModels] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [formData, setFormData] = useState({ provider: 'openai', api_key: '', model_name: 'gpt-4o' }); useEffect(() => { const fetchLogo = async () => { try { const API_URL = import.meta.env.VITE_API_URL || ''; const token = localStorage.getItem('token'); const res = await fetch(`${API_URL}/api/config/logo`, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setLogoUrl(data.logo_url); } } catch {} }; fetchLogo(); }, []); useEffect(() => { loadSettings(); }, []); const loadSettings = async () => { try { const token = localStorage.getItem('token'); const API_URL = import.meta.env.VITE_API_URL || ''; // Cargar modelos disponibles const modelsRes = await fetch(`${API_URL}/api/ai/models`, { headers: { 'Authorization': `Bearer ${token}` } }); if (modelsRes.ok) { const models = await modelsRes.json(); setAvailableModels(models); } // Cargar configuración actual const configRes = await fetch(`${API_URL}/api/ai/configuration`, { headers: { 'Authorization': `Bearer ${token}` } }); if (configRes.ok) { const config = await configRes.json(); setAiConfig(config); setFormData({ provider: config.provider || 'openai', api_key: config.api_key || '', model_name: config.model_name || 'gpt-4o' }); } else if (configRes.status === 404) { // No hay configuración guardada, usar valores por defecto console.log('No hay configuración de IA guardada'); } } catch (error) { console.error('Error loading settings:', error); } finally { setLoading(false); } }; const handleLogoUpload = async (e) => { const file = e.target.files[0]; if (!file) return; setLogoUploading(true); try { const API_URL = import.meta.env.VITE_API_URL || ''; const token = localStorage.getItem('token'); const formDataLogo = new FormData(); formDataLogo.append('file', file); const res = await fetch(`${API_URL}/api/config/logo`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formDataLogo }); if (res.ok) { const data = await res.json(); setLogoUrl(data.logo_url); alert('Logo actualizado correctamente'); } else { alert('Error al subir el logo'); } } catch { alert('Error al subir el logo'); } finally { setLogoUploading(false); } }; const handleSave = async (e) => { e.preventDefault(); setSaving(true); try { const token = localStorage.getItem('token'); const API_URL = import.meta.env.VITE_API_URL || ''; const response = await fetch(`${API_URL}/api/ai/configuration`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(formData), }); if (response.ok) { alert('Configuración guardada correctamente'); loadSettings(); } else { alert('Error al guardar configuración'); } } catch (error) { console.error('Error:', error); alert('Error al guardar configuración'); } finally { setSaving(false); } }; const filteredModels = availableModels.filter(m => m.provider === formData.provider); return (

Logo del Sistema

{logoUrl ? ( Logo ) : (
Sin logo
)}
{logoUploading && Subiendo...}

El logo se mostrará en el login y en la página principal.

Configuración de IA

Configura el proveedor y modelo de IA para análisis de imágenes

API Key

setFormData({ ...formData, api_key: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder={formData.provider === 'openai' ? 'sk-...' : 'AIza...'} required />

{formData.provider === 'openai' ? ( <>Obtén tu API key en OpenAI Platform ) : ( <>Obtén tu API key en Google AI Studio )}

Modelo de IA

{loading ? (
Cargando modelos...
) : filteredModels.length === 0 ? (
No hay modelos disponibles para {formData.provider}
) : (
{filteredModels.map((model) => ( ))}
)}
); } function APITokensTab({ user }) { const [tokens, setTokens] = useState([]) const [loading, setLoading] = useState(true) const [showCreateForm, setShowCreateForm] = useState(false) const [newTokenDescription, setNewTokenDescription] = useState('') const [createdToken, setCreatedToken] = useState(null) const [creating, setCreating] = useState(false) useEffect(() => { loadTokens() }, []) const loadTokens = async () => { try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/users/me/tokens`, { headers: { 'Authorization': `Bearer ${token}` } }) if (response.ok) { const data = await response.json() setTokens(data) } setLoading(false) } catch (error) { console.error('Error loading tokens:', error) setLoading(false) } } const handleCreateToken = async (e) => { e.preventDefault() setCreating(true) try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/users/me/tokens`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ description: newTokenDescription || null }), }) if (response.ok) { const data = await response.json() setCreatedToken(data.token) setShowCreateForm(false) setNewTokenDescription('') loadTokens() } else { alert('Error al crear token') } } catch (error) { console.error('Error:', error) alert('Error al crear token') } finally { setCreating(false) } } const handleRevokeToken = async (tokenId) => { if (!confirm('¿Estás seguro de revocar este token? Esta acción no se puede deshacer.')) { return } try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/users/me/tokens/${tokenId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }) if (response.ok) { alert('Token revocado correctamente') loadTokens() } else { alert('Error al revocar token') } } catch (error) { console.error('Error:', error) alert('Error al revocar token') } } const copyToClipboard = (text) => { navigator.clipboard.writeText(text) alert('Token copiado al portapapeles') } return (

Mis API Tokens

Genera tokens para acceder a la API sin necesidad de login

{/* Modal de Token Creado */} {createdToken && (

Token Creado Exitosamente

⚠️ Guarda este token ahora. No podrás verlo de nuevo.

{createdToken}

Ejemplo de uso:

curl -H "Authorization: Bearer {createdToken}" http://tu-api.com/api/inspections
)} {/* Formulario de Crear Token */} {showCreateForm && (

Generar Nuevo Token

setNewTokenDescription(e.target.value)} placeholder="ej: Integración con sistema X" className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent" />

Te ayuda a identificar para qué usas este token

)} {/* Lista de Tokens */} {loading ? (
Cargando tokens...
) : tokens.length === 0 ? (
🔑

No tienes tokens API creados

Genera uno para acceder a la API sin login

) : (
{tokens.map((token) => (
🔑

{token.description || 'Token sin descripción'}

{token.is_active ? 'Activo' : 'Revocado'}
Creado:{' '} {new Date(token.created_at).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
{token.last_used_at && (
Último uso:{' '} {new Date(token.last_used_at).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
)} {!token.last_used_at && (
⚠️ Nunca usado
)}
{token.is_active && ( )}
))}
)} {/* Información de Ayuda */}
ℹ️

¿Cómo usar los tokens API?

Los tokens API te permiten acceder a todos los endpoints sin necesidad de hacer login. Son perfectos para integraciones, scripts automatizados o aplicaciones externas.

Incluye el token en el header Authorization de tus requests:

Authorization: Bearer AYUTEC_tu_token_aqui
) } function QuestionsManagerModal({ checklist, onClose }) { const [questions, setQuestions] = useState([]) const [loading, setLoading] = useState(true) const [showCreateForm, setShowCreateForm] = useState(false) const [editingQuestion, setEditingQuestion] = useState(null) const [viewingAudit, setViewingAudit] = useState(null) const [auditHistory, setAuditHistory] = useState([]) const [loadingAudit, setLoadingAudit] = useState(false) const [draggedQuestion, setDraggedQuestion] = useState(null) const [dragOverQuestion, setDragOverQuestion] = useState(null) const [formData, setFormData] = useState({ section: '', text: '', type: 'boolean', points: 1, options: { type: 'boolean', choices: [ { value: 'pass', label: 'Pasa', points: 1, status: 'ok' }, { value: 'fail', label: 'Falla', points: 0, status: 'critical' } ] }, allow_photos: true, max_photos: 3, requires_comment_on_fail: false, send_notification: false, parent_question_id: null, show_if_answer: '', ai_prompt: '' }) useEffect(() => { loadQuestions() }, []) const loadQuestions = async () => { try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/checklists/${checklist.id}`, { headers: { 'Authorization': `Bearer ${token}` } }) if (response.ok) { const data = await response.json() setQuestions(data.questions || []) } setLoading(false) } catch (error) { console.error('Error loading questions:', error) setLoading(false) } } const loadAuditHistory = async (questionId) => { setLoadingAudit(true) try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/questions/${questionId}/audit`, { headers: { 'Authorization': `Bearer ${token}` } }) if (response.ok) { const data = await response.json() setAuditHistory(data) setViewingAudit(questionId) } else { alert('Error al cargar historial') } } catch (error) { console.error('Error loading audit history:', error) alert('Error al cargar historial') } finally { setLoadingAudit(false) } } const handleCreateQuestion = async (e) => { e.preventDefault() try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/questions`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ ...formData, checklist_id: checklist.id }), }) if (response.ok) { setShowCreateForm(false) setFormData({ section: '', text: '', type: 'boolean', points: 1, options: { type: 'boolean', choices: [ { value: 'pass', label: 'Pasa', points: 1, status: 'ok' }, { value: 'fail', label: 'Falla', points: 0, status: 'critical' } ] }, allow_photos: true, max_photos: 3, requires_comment_on_fail: false, send_notification: false, parent_question_id: null, show_if_answer: '', ai_prompt: '' }) loadQuestions() } else { alert('Error al crear pregunta') } } catch (error) { console.error('Error:', error) alert('Error al crear pregunta') } } const handleEditQuestion = (question) => { setEditingQuestion(question) setShowCreateForm(false) setFormData({ section: question.section || '', text: question.text, type: question.type, points: question.points || 1, options: question.options || { type: question.type, choices: [ { value: 'pass', label: 'Pasa', points: 1, status: 'ok' }, { value: 'fail', label: 'Falla', points: 0, status: 'critical' } ] }, allow_photos: question.allow_photos ?? true, max_photos: question.max_photos || 3, requires_comment_on_fail: question.requires_comment_on_fail || false, send_notification: question.send_notification || false, parent_question_id: question.parent_question_id || null, show_if_answer: question.show_if_answer || '', ai_prompt: question.ai_prompt || '' }) } const handleUpdateQuestion = async (e) => { e.preventDefault() try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/questions/${editingQuestion.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ checklist_id: checklist.id, section: formData.section, text: formData.text, type: formData.type, points: parseInt(formData.points), options: formData.options, allow_photos: formData.allow_photos, max_photos: parseInt(formData.max_photos), requires_comment_on_fail: formData.requires_comment_on_fail, send_notification: formData.send_notification, parent_question_id: formData.parent_question_id || null, show_if_answer: formData.show_if_answer || null, ai_prompt: formData.ai_prompt || null }) }) if (response.ok) { setEditingQuestion(null) setFormData({ section: '', text: '', type: 'boolean', points: 1, options: { type: 'boolean', choices: [ { value: 'pass', label: 'Pasa', points: 1, status: 'ok' }, { value: 'fail', label: 'Falla', points: 0, status: 'critical' } ] }, allow_photos: true, max_photos: 3, requires_comment_on_fail: false, send_notification: false, parent_question_id: null, show_if_answer: '', ai_prompt: '' }) loadQuestions() } else { alert('Error al actualizar pregunta') } } catch (error) { console.error('Error:', error) alert('Error al actualizar pregunta') } } const handleDeleteQuestion = async (questionId) => { if (!confirm('¿Estás seguro de eliminar esta pregunta?')) return try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/questions/${questionId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }) if (response.ok) { loadQuestions() alert('✅ Pregunta eliminada exitosamente') } else { const errorData = await response.json().catch(() => ({ detail: 'Error desconocido' })) if (response.status === 400) { // Error de validación (pregunta con respuestas o subpreguntas) alert(`⚠️ ${errorData.detail}`) } else { alert('❌ Error al eliminar pregunta') } } } catch (error) { console.error('Error:', error) alert('❌ Error de conexión al eliminar pregunta') } } const moveQuestion = async (questionId, direction) => { const questionsList = Object.values(questionsBySection).flat() const currentIndex = questionsList.findIndex(q => q.id === questionId) if (currentIndex === -1) return const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1 if (newIndex < 0 || newIndex >= questionsList.length) return // Crear nueva lista con el orden actualizado const newList = [...questionsList] const [movedQuestion] = newList.splice(currentIndex, 1) newList.splice(newIndex, 0, movedQuestion) // Preparar datos para el backend const reorderData = newList.map((q, index) => ({ question_id: q.id, new_order: index })) try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/checklists/${checklist.id}/questions/reorder`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(reorderData) }) if (response.ok) { loadQuestions() } else { alert('Error al reordenar pregunta') } } catch (error) { console.error('Error:', error) alert('Error al reordenar pregunta') } } // Drag & Drop handlers const handleDragStart = (e, question) => { setDraggedQuestion(question) e.dataTransfer.effectAllowed = 'move' e.currentTarget.style.opacity = '0.5' } const handleDragEnd = (e) => { e.currentTarget.style.opacity = '1' setDraggedQuestion(null) setDragOverQuestion(null) } const handleDragOver = (e, question) => { e.preventDefault() // Validar que ambas preguntas sean del mismo nivel (padre-padre o hijo-hijo del mismo padre) if (draggedQuestion) { const draggedIsChild = !!draggedQuestion.parent_question_id const targetIsChild = !!question.parent_question_id // No permitir mezclar niveles if (draggedIsChild !== targetIsChild) { e.dataTransfer.dropEffect = 'none' return } // Si son hijos, deben ser del mismo padre if (draggedIsChild && draggedQuestion.parent_question_id !== question.parent_question_id) { e.dataTransfer.dropEffect = 'none' return } } e.dataTransfer.dropEffect = 'move' setDragOverQuestion(question) } const handleDragLeave = (e) => { setDragOverQuestion(null) } const handleDrop = async (e, targetQuestion) => { e.preventDefault() if (!draggedQuestion || draggedQuestion.id === targetQuestion.id) { setDraggedQuestion(null) setDragOverQuestion(null) return } // Validar que sean del mismo nivel const draggedIsChild = !!draggedQuestion.parent_question_id const targetIsChild = !!targetQuestion.parent_question_id if (draggedIsChild !== targetIsChild) { alert('⚠️ Solo puedes reordenar preguntas del mismo nivel') setDraggedQuestion(null) setDragOverQuestion(null) return } // Si son hijos, validar que sean del mismo padre if (draggedIsChild && draggedQuestion.parent_question_id !== targetQuestion.parent_question_id) { alert('⚠️ Solo puedes reordenar subpreguntas del mismo padre') setDraggedQuestion(null) setDragOverQuestion(null) return } // Filtrar solo las preguntas del mismo nivel/grupo let questionsList if (draggedIsChild) { // Solo subpreguntas del mismo padre questionsList = questions.filter(q => q.parent_question_id === draggedQuestion.parent_question_id ) } else { // Solo preguntas padre (sin parent_question_id) questionsList = questions.filter(q => !q.parent_question_id) } const draggedIndex = questionsList.findIndex(q => q.id === draggedQuestion.id) const targetIndex = questionsList.findIndex(q => q.id === targetQuestion.id) // Crear nueva lista con el orden actualizado const newList = [...questionsList] const [movedQuestion] = newList.splice(draggedIndex, 1) newList.splice(targetIndex, 0, movedQuestion) // Preparar datos para el backend (solo las preguntas afectadas) const reorderData = newList.map((q, index) => ({ question_id: q.id, new_order: index })) try { const token = localStorage.getItem('token') const API_URL = import.meta.env.VITE_API_URL || '' const response = await fetch(`${API_URL}/api/checklists/${checklist.id}/questions/reorder`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(reorderData) }) if (response.ok) { loadQuestions() } else { alert('Error al reordenar pregunta') } } catch (error) { console.error('Error:', error) alert('Error al reordenar pregunta') } setDraggedQuestion(null) setDragOverQuestion(null) } const questionsBySection = questions.reduce((acc, q) => { const section = q.section || 'Sin sección' if (!acc[section]) acc[section] = [] acc[section].push(q) return acc }, {}) return (
{/* Header */}

Gestionar Preguntas

{checklist.name}

{/* Content */}

Total de preguntas: {questions.length} | Puntuación máxima: {questions.reduce((sum, q) => sum + (q.points || 0), 0)}

{/* Create/Edit Form */} {(showCreateForm || editingQuestion) && (

{editingQuestion ? '✏️ Editar Pregunta' : '➕ Nueva Pregunta'}

setFormData({ ...formData, section: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" placeholder="Ej: Motor, Frenos, Documentación" />
setFormData({ ...formData, points: parseInt(e.target.value) || 1 })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" />
setFormData({ ...formData, text: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" placeholder="Ej: Estado de las pastillas de freno" required />
{/* Configuración del Tipo de Pregunta */}

📝 Configuración de la Pregunta

{ setFormData({ ...formData, type: config.type, options: config }) }} maxPoints={formData.points} />
{/* Pregunta Condicional - Subpreguntas Anidadas hasta 5 niveles */}

⚡ Pregunta Condicional - Subpreguntas Anidadas (hasta 5 niveles)

{/* Solo permitir condicionales para boolean y single_choice */} {(() => { const currentType = formData.options?.type || formData.type const allowConditional = ['boolean', 'single_choice'].includes(currentType) if (!allowConditional) { return (

ℹ️ Las preguntas condicionales solo están disponibles para tipos Boolean y Opción Única

) } return ( <>

Esta pregunta aparecerá solo si se cumple la condición de la pregunta padre

La pregunta solo se mostrará con esta respuesta específica

{/* Indicador de profundidad */} {formData.parent_question_id && (() => { const parentQ = questions.find(q => q.id === formData.parent_question_id) const parentDepth = parentQ?.depth_level || 0 const newDepth = parentDepth + 1 return (
= 5 ? 'bg-red-50 border border-red-200' : 'bg-blue-100'}`}>

📊 Profundidad: Nivel {newDepth} de 5 máximo {newDepth >= 5 && ' ⚠️ Máximo alcanzado'}

) })()} ) })()}
{/* AI Prompt - Solo visible si el checklist tiene IA habilitada */} {checklist.ai_mode !== 'off' && (

🤖 Prompt de IA (opcional)