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' 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 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 */}
{/* Logo S de Syntria */}

Syntria

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) 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) setChecklists(Array.isArray(checklistsData) ? checklistsData : []) } 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) setInspections(Array.isArray(inspectionsData) ? inspectionsData : []) } 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 */}

Syntria

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 }) { 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(() => { 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, api_key: config.api_key, model_name: config.model_name }) } setLoading(false) } catch (error) { console.error('Error loading settings:', error) setLoading(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 (

Configuración de IA

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

{loading ? (
Cargando configuración...
) : (
{/* Provider Selection */}

Proveedor de IA

{/* API Key */}

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 )}

{/* Model Selection */}

Modelo de IA

{filteredModels.map((model) => ( ))}
{/* Current Status */} {aiConfig && (
Configuración Activa
Proveedor: {aiConfig.provider} | Modelo: {aiConfig.model_name}
Configurado el {new Date(aiConfig.created_at).toLocaleDateString('es-ES')}
)} {/* Save Button */}
)}
) } 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 syntria_tu_token_aqui
) } function QuestionsManagerModal({ checklist, onClose }) { const [questions, setQuestions] = useState([]) const [loading, setLoading] = useState(true) const [showCreateForm, setShowCreateForm] = useState(false) const [formData, setFormData] = useState({ section: '', text: '', type: 'pass_fail', points: 1, allow_photos: true, max_photos: 3, requires_comment_on_fail: 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 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: 'pass_fail', points: 1, allow_photos: true, max_photos: 3, requires_comment_on_fail: 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 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() } else { alert('Error al eliminar pregunta') } } catch (error) { console.error('Error:', error) alert('Error al eliminar pregunta') } } 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 Form */} {showCreateForm && (
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, 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 />
{/* Pregunta Condicional */}

⚡ Pregunta Condicional (opcional)

Esta pregunta aparecerá solo si se responde la pregunta padre

La pregunta solo se mostrará con esta respuesta

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

🤖 Prompt de IA (opcional)