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.
422 lines
16 KiB
TypeScript
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>
|
|
)
|
|
}
|