Files
ATLAS/frontend/src/pages/Configuracion.tsx
FlotillasGPS Developer 51d78bacf4 FlotillasGPS - Sistema completo de monitoreo de flotillas GPS
Sistema completo para monitoreo y gestion de flotas de vehiculos con:
- Backend FastAPI con PostgreSQL/TimescaleDB
- Frontend React con TypeScript y TailwindCSS
- App movil React Native con Expo
- Soporte para dispositivos GPS, Meshtastic y celulares
- Video streaming en vivo con MediaMTX
- Geocercas, alertas, viajes y reportes
- Autenticacion JWT y WebSockets en tiempo real

Documentacion completa y guias de usuario incluidas.
2026-01-21 08:18:00 +00:00

422 lines
16 KiB
TypeScript

import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import {
UserIcon,
BellIcon,
MapIcon,
PaintBrushIcon,
ShieldCheckIcon,
GlobeAltIcon,
} from '@heroicons/react/24/outline'
import clsx from 'clsx'
import { useAuthStore } from '@/store/authStore'
import { useConfigStore } from '@/store/configStore'
import { authApi } from '@/api'
import Card, { CardHeader } from '@/components/ui/Card'
import Button from '@/components/ui/Button'
import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
import Checkbox from '@/components/ui/Checkbox'
import { Tabs, TabPanel } from '@/components/ui/Tabs'
import { useToast } from '@/components/ui/Toast'
export default function Configuracion() {
const toast = useToast()
const user = useAuthStore((state) => state.user)
const config = useConfigStore()
const [activeTab, setActiveTab] = useState('perfil')
const [passwordForm, setPasswordForm] = useState({
current: '',
new: '',
confirm: '',
})
const cambiarPasswordMutation = useMutation({
mutationFn: (data: { currentPassword: string; newPassword: string }) =>
authApi.cambiarPassword(data),
onSuccess: () => {
toast.success('Contrasena actualizada')
setPasswordForm({ current: '', new: '', confirm: '' })
},
onError: () => {
toast.error('Error al cambiar contrasena')
},
})
const handleCambiarPassword = (e: React.FormEvent) => {
e.preventDefault()
if (passwordForm.new !== passwordForm.confirm) {
toast.error('Las contrasenas no coinciden')
return
}
if (passwordForm.new.length < 8) {
toast.error('La contrasena debe tener al menos 8 caracteres')
return
}
cambiarPasswordMutation.mutate({
currentPassword: passwordForm.current,
newPassword: passwordForm.new,
})
}
const tabs = [
{ id: 'perfil', label: 'Perfil', icon: UserIcon },
{ id: 'notificaciones', label: 'Notificaciones', icon: BellIcon },
{ id: 'mapa', label: 'Mapa', icon: MapIcon },
{ id: 'apariencia', label: 'Apariencia', icon: PaintBrushIcon },
{ id: 'seguridad', label: 'Seguridad', icon: ShieldCheckIcon },
]
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-white">Configuracion</h1>
<p className="text-slate-500 mt-1">
Personaliza tu experiencia en la plataforma
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar tabs */}
<div className="space-y-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={clsx(
'w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors text-left',
activeTab === tab.id
? 'bg-accent-500/20 text-accent-400'
: 'text-slate-400 hover:bg-slate-800/50 hover:text-white'
)}
>
<tab.icon className="w-5 h-5" />
<span>{tab.label}</span>
</button>
))}
</div>
{/* Content */}
<div className="lg:col-span-3">
{/* Perfil */}
{activeTab === 'perfil' && (
<Card padding="lg">
<CardHeader
title="Informacion del perfil"
subtitle="Actualiza tu informacion personal"
/>
<div className="space-y-4 mt-6">
<div className="flex items-center gap-4">
<div className="w-20 h-20 bg-accent-500/20 rounded-full flex items-center justify-center">
<span className="text-2xl font-bold text-accent-400">
{user?.nombre?.charAt(0) || 'U'}
</span>
</div>
<div>
<p className="text-white font-medium">{user?.nombre}</p>
<p className="text-sm text-slate-500">{user?.email}</p>
<p className="text-xs text-slate-500 capitalize">Rol: {user?.rol}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-4">
<Input
label="Nombre"
value={user?.nombre || ''}
disabled
/>
<Input
label="Email"
type="email"
value={user?.email || ''}
disabled
/>
</div>
<p className="text-xs text-slate-500">
Para cambiar tu nombre o email, contacta al administrador.
</p>
</div>
</Card>
)}
{/* Notificaciones */}
{activeTab === 'notificaciones' && (
<Card padding="lg">
<CardHeader
title="Preferencias de notificaciones"
subtitle="Configura como deseas recibir alertas"
/>
<div className="space-y-6 mt-6">
<div className="space-y-4">
<h3 className="text-white font-medium">Notificaciones en la app</h3>
<Checkbox
label="Alertas criticas"
description="Recibir notificaciones de alertas criticas"
checked={config.notificaciones.sonido}
onChange={(checked) =>
config.setNotificaciones({ sonido: checked })
}
/>
<Checkbox
label="Alertas de velocidad"
description="Excesos de velocidad y geocercas"
checked={config.notificaciones.escritorio}
onChange={(checked) =>
config.setNotificaciones({ escritorio: checked })
}
/>
<Checkbox
label="Alertas de mantenimiento"
description="Recordatorios de servicios programados"
checked={config.notificaciones.email}
onChange={(checked) =>
config.setNotificaciones({ email: checked })
}
/>
</div>
<div className="space-y-4 pt-4 border-t border-slate-700/50">
<h3 className="text-white font-medium">Sonidos</h3>
<Checkbox
label="Reproducir sonido"
description="Sonido al recibir alertas importantes"
checked={config.notificaciones.sonido}
onChange={(checked) =>
config.setNotificaciones({ sonido: checked })
}
/>
</div>
</div>
</Card>
)}
{/* Mapa */}
{activeTab === 'mapa' && (
<Card padding="lg">
<CardHeader
title="Configuracion del mapa"
subtitle="Personaliza la visualizacion del mapa"
/>
<div className="space-y-6 mt-6">
<Select
label="Estilo del mapa"
value={config.mapa.estilo}
onChange={(e) => config.setMapa({ estilo: e.target.value as any })}
options={[
{ value: 'dark', label: 'Oscuro' },
{ value: 'light', label: 'Claro' },
{ value: 'satellite', label: 'Satelite' },
{ value: 'streets', label: 'Calles' },
]}
/>
<div className="grid grid-cols-2 gap-4">
<Input
label="Latitud inicial"
type="number"
step="any"
value={config.mapa.centroInicial.lat}
onChange={(e) =>
config.setMapa({
centroInicial: {
...config.mapa.centroInicial,
lat: parseFloat(e.target.value),
},
})
}
/>
<Input
label="Longitud inicial"
type="number"
step="any"
value={config.mapa.centroInicial.lng}
onChange={(e) =>
config.setMapa({
centroInicial: {
...config.mapa.centroInicial,
lng: parseFloat(e.target.value),
},
})
}
/>
</div>
<Input
label="Zoom inicial"
type="number"
min={1}
max={18}
value={config.mapa.zoomInicial}
onChange={(e) =>
config.setMapa({ zoomInicial: parseInt(e.target.value) })
}
/>
<div className="space-y-4 pt-4 border-t border-slate-700/50">
<h3 className="text-white font-medium">Capas visibles</h3>
<Checkbox
label="Mostrar trafico"
checked={config.mapa.mostrarTrafico}
onChange={(checked) => config.setMapa({ mostrarTrafico: checked })}
/>
<Checkbox
label="Mostrar geocercas"
checked={config.mapa.mostrarGeocercas}
onChange={(checked) => config.setMapa({ mostrarGeocercas: checked })}
/>
<Checkbox
label="Mostrar POIs"
checked={config.mapa.mostrarPOIs}
onChange={(checked) => config.setMapa({ mostrarPOIs: checked })}
/>
<Checkbox
label="Agrupar vehiculos cercanos"
checked={config.mapa.clusterVehiculos}
onChange={(checked) => config.setMapa({ clusterVehiculos: checked })}
/>
</div>
</div>
</Card>
)}
{/* Apariencia */}
{activeTab === 'apariencia' && (
<Card padding="lg">
<CardHeader
title="Apariencia"
subtitle="Personaliza el aspecto visual"
/>
<div className="space-y-6 mt-6">
<Select
label="Tema"
value={config.tema}
onChange={(e) => config.setTema(e.target.value as any)}
options={[
{ value: 'dark', label: 'Oscuro' },
{ value: 'light', label: 'Claro' },
{ value: 'system', label: 'Sistema' },
]}
/>
<Select
label="Idioma"
value={config.idioma}
onChange={(e) => config.setIdioma(e.target.value)}
options={[
{ value: 'es', label: 'Espanol' },
{ value: 'en', label: 'English' },
]}
/>
<Select
label="Zona horaria"
value={config.zonaHoraria}
onChange={(e) => config.setZonaHoraria(e.target.value)}
options={[
{ value: 'America/Mexico_City', label: 'Ciudad de Mexico (GMT-6)' },
{ value: 'America/Monterrey', label: 'Monterrey (GMT-6)' },
{ value: 'America/Tijuana', label: 'Tijuana (GMT-8)' },
{ value: 'America/Cancun', label: 'Cancun (GMT-5)' },
]}
/>
<div className="space-y-4 pt-4 border-t border-slate-700/50">
<h3 className="text-white font-medium">Unidades</h3>
<Select
label="Sistema de unidades"
value={config.unidades.distancia}
onChange={(e) =>
config.setUnidades({
distancia: e.target.value as 'km' | 'mi',
velocidad: e.target.value === 'km' ? 'kmh' : 'mph',
})
}
options={[
{ value: 'km', label: 'Metrico (km, km/h)' },
{ value: 'mi', label: 'Imperial (mi, mph)' },
]}
/>
</div>
</div>
</Card>
)}
{/* Seguridad */}
{activeTab === 'seguridad' && (
<Card padding="lg">
<CardHeader
title="Seguridad"
subtitle="Gestiona la seguridad de tu cuenta"
/>
<div className="space-y-6 mt-6">
<form onSubmit={handleCambiarPassword} className="space-y-4">
<h3 className="text-white font-medium">Cambiar contrasena</h3>
<Input
label="Contrasena actual"
type="password"
value={passwordForm.current}
onChange={(e) =>
setPasswordForm({ ...passwordForm, current: e.target.value })
}
placeholder="Ingresa tu contrasena actual"
/>
<Input
label="Nueva contrasena"
type="password"
value={passwordForm.new}
onChange={(e) =>
setPasswordForm({ ...passwordForm, new: e.target.value })
}
placeholder="Minimo 8 caracteres"
/>
<Input
label="Confirmar contrasena"
type="password"
value={passwordForm.confirm}
onChange={(e) =>
setPasswordForm({ ...passwordForm, confirm: e.target.value })
}
placeholder="Repite la nueva contrasena"
/>
<Button
type="submit"
isLoading={cambiarPasswordMutation.isPending}
>
Cambiar contrasena
</Button>
</form>
<div className="pt-4 border-t border-slate-700/50 space-y-4">
<h3 className="text-white font-medium">Sesiones activas</h3>
<div className="p-4 bg-slate-800/50 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<GlobeAltIcon className="w-8 h-8 text-accent-400" />
<div>
<p className="text-white">Sesion actual</p>
<p className="text-xs text-slate-500">
Este navegador - Activo ahora
</p>
</div>
</div>
<span className="text-xs text-success-400">Activo</span>
</div>
</div>
</div>
<div className="pt-4 border-t border-slate-700/50">
<h3 className="text-white font-medium mb-4">Zona de peligro</h3>
<Button variant="danger">Cerrar todas las sesiones</Button>
</div>
</div>
</Card>
)}
</div>
</div>
</div>
)
}