312 lines
11 KiB
TypeScript
312 lines
11 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useEffect, useState } from "react";
|
||
|
|
import { User, Mail, Building, Calendar, Shield, Briefcase, Globe, Phone } from "lucide-react";
|
||
|
|
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { Separator } from "@/components/ui/separator";
|
||
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
||
|
|
import { getUser } from "@/lib/usersService";
|
||
|
|
import { getProjects } from "@/lib/projectsService";
|
||
|
|
import { Project } from "@/types/project";
|
||
|
|
|
||
|
|
interface DolibarrUser {
|
||
|
|
id: string;
|
||
|
|
login: string;
|
||
|
|
firstname: string;
|
||
|
|
lastname: string;
|
||
|
|
email: string;
|
||
|
|
admin: string;
|
||
|
|
statut: string;
|
||
|
|
employee: string;
|
||
|
|
job: string;
|
||
|
|
address: string;
|
||
|
|
zip: string;
|
||
|
|
town: string;
|
||
|
|
state_id: string;
|
||
|
|
office_phone: string;
|
||
|
|
user_mobile: string;
|
||
|
|
fk_member: string;
|
||
|
|
datelastlogin: number;
|
||
|
|
datepreviouslogin: number;
|
||
|
|
datec: string;
|
||
|
|
datem: string;
|
||
|
|
fk_soc: string;
|
||
|
|
entity: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function PerfilPage() {
|
||
|
|
const [user, setUser] = useState<DolibarrUser | null>(null);
|
||
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
||
|
|
const [isLoading, setIsLoading] = useState(true);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
async function loadData() {
|
||
|
|
try {
|
||
|
|
const [userData, projectsData] = await Promise.all([
|
||
|
|
getUser(),
|
||
|
|
getProjects(),
|
||
|
|
]);
|
||
|
|
setUser(userData);
|
||
|
|
setProjects(projectsData);
|
||
|
|
} catch (err) {
|
||
|
|
console.error("Error loading profile data:", err);
|
||
|
|
setError("No se pudo cargar la información del perfil");
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
loadData();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
if (isLoading) {
|
||
|
|
return <ProfileSkeleton />;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (error || !user) {
|
||
|
|
return (
|
||
|
|
<div className="p-6">
|
||
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||
|
|
<User className="h-12 w-12 text-muted-foreground mb-4" />
|
||
|
|
<h2 className="text-lg font-semibold mb-2">Error al cargar el perfil</h2>
|
||
|
|
<p className="text-sm text-muted-foreground">{error}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const fullName = `${user.firstname} ${user.lastname}`;
|
||
|
|
const initials = `${user.firstname?.charAt(0) || ""}${user.lastname?.charAt(0) || ""}`;
|
||
|
|
const isAdmin = user.admin === "1";
|
||
|
|
const isActive = user.statut === "1";
|
||
|
|
const isEmployee = user.employee === "1";
|
||
|
|
|
||
|
|
// Stats from projects
|
||
|
|
const activeProjects = projects.filter((p) => p.status === "1").length;
|
||
|
|
const completedProjects = projects.filter((p) => p.status === "2").length;
|
||
|
|
const totalBudget = projects.reduce((acc, p) => acc + (p.budget || 0), 0);
|
||
|
|
|
||
|
|
const formatDate = (timestamp: number | string) => {
|
||
|
|
if (!timestamp) return "N/A";
|
||
|
|
const ts = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;
|
||
|
|
const date = new Date(ts * 1000);
|
||
|
|
return date.toLocaleDateString("es-ES", {
|
||
|
|
day: "2-digit",
|
||
|
|
month: "long",
|
||
|
|
year: "numeric",
|
||
|
|
hour: "2-digit",
|
||
|
|
minute: "2-digit",
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="p-6 space-y-6 max-w-4xl mx-auto">
|
||
|
|
{/* Header */}
|
||
|
|
<div>
|
||
|
|
<h1 className="text-2xl font-bold tracking-tight">Mi Perfil</h1>
|
||
|
|
<p className="text-muted-foreground">Información de tu cuenta de Dolibarr</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Profile card */}
|
||
|
|
<Card>
|
||
|
|
<CardContent className="p-6">
|
||
|
|
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-6">
|
||
|
|
{/* Avatar */}
|
||
|
|
<Avatar className="h-24 w-24 text-2xl">
|
||
|
|
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-2xl">
|
||
|
|
{initials}
|
||
|
|
</AvatarFallback>
|
||
|
|
</Avatar>
|
||
|
|
|
||
|
|
{/* Info */}
|
||
|
|
<div className="flex-1 text-center sm:text-left space-y-3">
|
||
|
|
<div>
|
||
|
|
<h2 className="text-2xl font-bold">{fullName}</h2>
|
||
|
|
<p className="text-muted-foreground">@{user.login}</p>
|
||
|
|
</div>
|
||
|
|
<div className="flex flex-wrap items-center justify-center sm:justify-start gap-2">
|
||
|
|
<Badge variant={isActive ? "default" : "secondary"} className={isActive ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" : ""}>
|
||
|
|
{isActive ? "Activo" : "Inactivo"}
|
||
|
|
</Badge>
|
||
|
|
{isAdmin && (
|
||
|
|
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-800">
|
||
|
|
<Shield className="h-3 w-3 mr-1" />
|
||
|
|
Administrador
|
||
|
|
</Badge>
|
||
|
|
)}
|
||
|
|
{isEmployee && (
|
||
|
|
<Badge variant="outline">
|
||
|
|
<Briefcase className="h-3 w-3 mr-1" />
|
||
|
|
Empleado
|
||
|
|
</Badge>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Stats */}
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||
|
|
<Card>
|
||
|
|
<CardContent className="p-4 text-center">
|
||
|
|
<p className="text-3xl font-bold text-blue-600">{activeProjects}</p>
|
||
|
|
<p className="text-sm text-muted-foreground">Proyectos activos</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<Card>
|
||
|
|
<CardContent className="p-4 text-center">
|
||
|
|
<p className="text-3xl font-bold text-green-600">{completedProjects}</p>
|
||
|
|
<p className="text-sm text-muted-foreground">Proyectos completados</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<Card>
|
||
|
|
<CardContent className="p-4 text-center">
|
||
|
|
<p className="text-3xl font-bold bg-gradient-to-r from-blue-500 to-purple-600 bg-clip-text text-transparent">
|
||
|
|
{totalBudget.toLocaleString("es-ES")} €
|
||
|
|
</p>
|
||
|
|
<p className="text-sm text-muted-foreground">Presupuesto total gestionado</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Details */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
|
{/* Contact info */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">Información de Contacto</CardTitle>
|
||
|
|
<CardDescription>Datos de contacto registrados en Dolibarr</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<InfoRow icon={Mail} label="Email" value={user.email || "No configurado"} />
|
||
|
|
<Separator />
|
||
|
|
<InfoRow icon={Phone} label="Teléfono oficina" value={user.office_phone || "No configurado"} />
|
||
|
|
<Separator />
|
||
|
|
<InfoRow icon={Phone} label="Móvil" value={user.user_mobile || "No configurado"} />
|
||
|
|
<Separator />
|
||
|
|
<InfoRow icon={Briefcase} label="Puesto" value={user.job || "No configurado"} />
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Account info */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">Información de la Cuenta</CardTitle>
|
||
|
|
<CardDescription>Detalles de tu cuenta en el sistema</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<InfoRow icon={User} label="ID de usuario" value={`#${user.id}`} />
|
||
|
|
<Separator />
|
||
|
|
<InfoRow icon={Globe} label="Entidad" value={user.entity || "1"} />
|
||
|
|
<Separator />
|
||
|
|
<InfoRow
|
||
|
|
icon={Calendar}
|
||
|
|
label="Último acceso"
|
||
|
|
value={user.datelastlogin ? formatDate(user.datelastlogin) : "N/A"}
|
||
|
|
/>
|
||
|
|
<Separator />
|
||
|
|
<InfoRow
|
||
|
|
icon={Calendar}
|
||
|
|
label="Acceso anterior"
|
||
|
|
value={user.datepreviouslogin ? formatDate(user.datepreviouslogin) : "N/A"}
|
||
|
|
/>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Address */}
|
||
|
|
{(user.address || user.town || user.zip) && (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-lg">Dirección</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="flex items-start gap-3">
|
||
|
|
<Building className="h-5 w-5 text-muted-foreground mt-0.5" />
|
||
|
|
<div className="text-sm">
|
||
|
|
{user.address && <p>{user.address}</p>}
|
||
|
|
{(user.zip || user.town) && (
|
||
|
|
<p className="text-muted-foreground">
|
||
|
|
{[user.zip, user.town].filter(Boolean).join(", ")}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reusable info row component
|
||
|
|
function InfoRow({ icon: Icon, label, value }: { icon: React.ComponentType<{ className?: string }>; label: string; value: string }) {
|
||
|
|
return (
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||
|
|
<p className="text-sm font-medium truncate">{value}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Skeleton
|
||
|
|
function ProfileSkeleton() {
|
||
|
|
return (
|
||
|
|
<div className="p-6 space-y-6 max-w-4xl mx-auto">
|
||
|
|
<div>
|
||
|
|
<Skeleton className="h-8 w-48 mb-2" />
|
||
|
|
<Skeleton className="h-4 w-72" />
|
||
|
|
</div>
|
||
|
|
<Card>
|
||
|
|
<CardContent className="p-6">
|
||
|
|
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-6">
|
||
|
|
<Skeleton className="h-24 w-24 rounded-full" />
|
||
|
|
<div className="space-y-3 flex-1">
|
||
|
|
<Skeleton className="h-8 w-48" />
|
||
|
|
<Skeleton className="h-4 w-32" />
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Skeleton className="h-6 w-16" />
|
||
|
|
<Skeleton className="h-6 w-28" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||
|
|
{[1, 2, 3].map((i) => (
|
||
|
|
<Card key={i}>
|
||
|
|
<CardContent className="p-4 text-center space-y-2">
|
||
|
|
<Skeleton className="h-8 w-16 mx-auto" />
|
||
|
|
<Skeleton className="h-4 w-32 mx-auto" />
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
|
{[1, 2].map((i) => (
|
||
|
|
<Card key={i}>
|
||
|
|
<CardHeader>
|
||
|
|
<Skeleton className="h-6 w-48" />
|
||
|
|
<Skeleton className="h-4 w-64" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
{[1, 2, 3, 4].map((j) => (
|
||
|
|
<div key={j}>
|
||
|
|
<Skeleton className="h-10 w-full" />
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|