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.
This commit is contained in:
421
frontend/src/pages/Configuracion.tsx
Normal file
421
frontend/src/pages/Configuracion.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user