2026-02-06 18:24:39 +00:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState, useCallback } from "react";
|
|
|
|
|
import {
|
|
|
|
|
Mail,
|
|
|
|
|
Phone,
|
|
|
|
|
Smartphone,
|
|
|
|
|
MapPin,
|
|
|
|
|
Briefcase,
|
|
|
|
|
Shield,
|
|
|
|
|
ShieldCheck,
|
|
|
|
|
Clock,
|
|
|
|
|
Globe,
|
|
|
|
|
Calendar,
|
|
|
|
|
User,
|
|
|
|
|
Building,
|
|
|
|
|
RefreshCw,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
|
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
|
|
|
|
|
|
|
import { getCurrentUserProfile } from "@/lib/usersService";
|
|
|
|
|
import { UserProfile, formatLastLogin } from "@/types/user";
|
|
|
|
|
|
|
|
|
|
interface UserProfileDialogProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Componente para mostrar un campo de información
|
|
|
|
|
function InfoField({
|
|
|
|
|
icon: Icon,
|
|
|
|
|
label,
|
|
|
|
|
value,
|
|
|
|
|
className = "",
|
|
|
|
|
}: {
|
|
|
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
|
|
|
label: string;
|
|
|
|
|
value: string | null | undefined;
|
|
|
|
|
className?: string;
|
|
|
|
|
}) {
|
|
|
|
|
if (!value) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={`flex items-start gap-3 ${className}`}>
|
|
|
|
|
<div className="flex-shrink-0 p-2 bg-muted rounded-md">
|
|
|
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<p className="text-xs text-muted-foreground">{label}</p>
|
|
|
|
|
<p className="text-sm font-medium truncate">{value}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skeleton para estado de carga
|
|
|
|
|
function ProfileSkeleton() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Header skeleton */}
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<Skeleton className="h-20 w-20 rounded-full" />
|
|
|
|
|
<div className="space-y-2 flex-1">
|
|
|
|
|
<Skeleton className="h-6 w-40" />
|
|
|
|
|
<Skeleton className="h-4 w-32" />
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Skeleton className="h-5 w-16" />
|
|
|
|
|
<Skeleton className="h-5 w-20" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* Content skeleton */}
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
|
|
|
<div key={i} className="flex items-start gap-3">
|
|
|
|
|
<Skeleton className="h-8 w-8 rounded-md" />
|
|
|
|
|
<div className="space-y-1 flex-1">
|
|
|
|
|
<Skeleton className="h-3 w-16" />
|
|
|
|
|
<Skeleton className="h-4 w-32" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Estado de error
|
|
|
|
|
function ErrorState({ onRetry }: { onRetry: () => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
|
|
|
<div className="p-3 rounded-full bg-destructive/10 mb-4">
|
|
|
|
|
<User className="h-8 w-8 text-destructive" />
|
|
|
|
|
</div>
|
|
|
|
|
<h3 className="text-lg font-semibold mb-1">Error al cargar el perfil</h3>
|
|
|
|
|
<p className="text-sm text-muted-foreground mb-4 max-w-sm">
|
|
|
|
|
No se pudo cargar la información del usuario. Por favor, intenta de nuevo.
|
|
|
|
|
</p>
|
|
|
|
|
<Button onClick={onRetry} variant="outline" size="sm">
|
|
|
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
|
|
|
Reintentar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function UserProfileDialog({ open, onOpenChange }: UserProfileDialogProps) {
|
|
|
|
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const fetchProfile = useCallback(async () => {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
try {
|
|
|
|
|
const data = await getCurrentUserProfile();
|
|
|
|
|
setProfile(data);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Error fetching profile:", err);
|
|
|
|
|
setError("No se pudo cargar el perfil");
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Cargar perfil cuando se abre el dialog
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open && !profile) {
|
|
|
|
|
fetchProfile();
|
|
|
|
|
}
|
|
|
|
|
}, [open, profile, fetchProfile]);
|
|
|
|
|
|
|
|
|
|
// Formatear dirección completa
|
|
|
|
|
const formatAddress = (p: UserProfile): string | null => {
|
|
|
|
|
const parts = [p.address, p.postalCode, p.city, p.country].filter(Boolean);
|
|
|
|
|
return parts.length > 0 ? parts.join(", ") : null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
2026-02-06 19:51:04 +00:00
|
|
|
<DialogContent className="sm:max-w-2xl max-h-[85vh] overflow-y-auto">
|
2026-02-06 18:24:39 +00:00
|
|
|
<DialogHeader className="pb-2">
|
|
|
|
|
<DialogTitle className="text-xl">Mi Perfil</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
{isLoading && <ProfileSkeleton />}
|
|
|
|
|
|
|
|
|
|
{error && !isLoading && <ErrorState onRetry={fetchProfile} />}
|
|
|
|
|
|
|
|
|
|
{profile && !isLoading && !error && (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Header con avatar y nombre */}
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<Avatar className="h-20 w-20 border-2 border-border">
|
|
|
|
|
{profile.photo ? (
|
|
|
|
|
<AvatarImage src={profile.photo} alt={profile.fullName} />
|
|
|
|
|
) : null}
|
|
|
|
|
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-2xl font-semibold">
|
|
|
|
|
{profile.initials}
|
|
|
|
|
</AvatarFallback>
|
|
|
|
|
</Avatar>
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<h3 className="text-xl font-semibold truncate">{profile.fullName}</h3>
|
|
|
|
|
<p className="text-sm text-muted-foreground">@{profile.login}</p>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2 mt-2">
|
|
|
|
|
{profile.isAdmin && (
|
|
|
|
|
<Badge variant="default" className="bg-gradient-to-r from-amber-500 to-orange-500">
|
|
|
|
|
<ShieldCheck className="h-3 w-3 mr-1" />
|
|
|
|
|
Administrador
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
<Badge variant={profile.isActive ? "default" : "secondary"}>
|
|
|
|
|
{profile.isActive ? "Activo" : "Inactivo"}
|
|
|
|
|
</Badge>
|
|
|
|
|
{profile.isEmployee && (
|
|
|
|
|
<Badge variant="outline">Empleado</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* Información de contacto */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="pt-4 pb-4">
|
|
|
|
|
<h4 className="text-sm font-semibold text-muted-foreground mb-4 uppercase tracking-wide">
|
|
|
|
|
Contacto
|
|
|
|
|
</h4>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Mail}
|
|
|
|
|
label="Email"
|
|
|
|
|
value={profile.email}
|
|
|
|
|
/>
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Mail}
|
|
|
|
|
label="Email personal"
|
|
|
|
|
value={profile.personalEmail}
|
|
|
|
|
/>
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Phone}
|
|
|
|
|
label="Teléfono"
|
|
|
|
|
value={profile.phone}
|
|
|
|
|
/>
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Smartphone}
|
|
|
|
|
label="Móvil"
|
|
|
|
|
value={profile.mobile}
|
|
|
|
|
/>
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={MapPin}
|
|
|
|
|
label="Dirección"
|
|
|
|
|
value={formatAddress(profile)}
|
|
|
|
|
className="sm:col-span-2"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Información laboral */}
|
|
|
|
|
{(profile.job || profile.establishment || profile.employmentStartDate) && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="pt-4 pb-4">
|
|
|
|
|
<h4 className="text-sm font-semibold text-muted-foreground mb-4 uppercase tracking-wide">
|
|
|
|
|
Información Laboral
|
|
|
|
|
</h4>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Briefcase}
|
|
|
|
|
label="Puesto"
|
|
|
|
|
value={profile.job}
|
|
|
|
|
/>
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Building}
|
|
|
|
|
label="Establecimiento"
|
|
|
|
|
value={profile.establishment}
|
|
|
|
|
/>
|
|
|
|
|
{profile.employmentStartDate && (
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Calendar}
|
|
|
|
|
label="Fecha de alta"
|
|
|
|
|
value={profile.employmentStartDate.toLocaleDateString('es-ES', {
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
month: 'long',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
})}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Actividad y sesión */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="pt-4 pb-4">
|
|
|
|
|
<h4 className="text-sm font-semibold text-muted-foreground mb-4 uppercase tracking-wide">
|
|
|
|
|
Actividad
|
|
|
|
|
</h4>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Clock}
|
|
|
|
|
label="Último acceso"
|
|
|
|
|
value={formatLastLogin(profile.lastLogin)}
|
|
|
|
|
/>
|
|
|
|
|
{profile.lastLoginIp && (
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Globe}
|
|
|
|
|
label="IP último acceso"
|
|
|
|
|
value={profile.lastLoginIp}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{profile.language && (
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Globe}
|
|
|
|
|
label="Idioma"
|
|
|
|
|
value={profile.language}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<InfoField
|
|
|
|
|
icon={Shield}
|
|
|
|
|
label="ID de usuario"
|
|
|
|
|
value={`#${profile.id}`}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|