trello_fake/components/user-profile/user-profile-dialog.tsx

312 lines
10 KiB
TypeScript
Raw Normal View History

"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}>
<DialogContent className="sm:max-w-lg max-h-[85vh] overflow-y-auto">
<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>
);
}