feat: dashboard com dados reais de projetos, servicos e contatos
This commit is contained in:
@@ -1,4 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
status: string;
|
||||
coverImage: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface Service {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface Contact {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
subject: string | null;
|
||||
message: string;
|
||||
createdAt: string;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
projects: number;
|
||||
activeProjects: number;
|
||||
services: number;
|
||||
activeServices: number;
|
||||
contacts: number;
|
||||
unreadContacts: number;
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const [stats, setStats] = useState<Stats>({
|
||||
projects: 0,
|
||||
activeProjects: 0,
|
||||
services: 0,
|
||||
activeServices: 0,
|
||||
contacts: 0,
|
||||
unreadContacts: 0,
|
||||
});
|
||||
const [recentProjects, setRecentProjects] = useState<Project[]>([]);
|
||||
const [recentContacts, setRecentContacts] = useState<Contact[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
// Buscar projetos
|
||||
const projectsRes = await fetch('/api/projects');
|
||||
const projects: Project[] = projectsRes.ok ? await projectsRes.json() : [];
|
||||
|
||||
// Buscar serviços
|
||||
const servicesRes = await fetch('/api/services');
|
||||
const services: Service[] = servicesRes.ok ? await servicesRes.json() : [];
|
||||
|
||||
// Buscar contatos
|
||||
const contactsRes = await fetch('/api/contacts');
|
||||
const contacts: Contact[] = contactsRes.ok ? await contactsRes.json() : [];
|
||||
|
||||
// Calcular estatísticas
|
||||
setStats({
|
||||
projects: projects.length,
|
||||
activeProjects: projects.filter(p => p.status === 'Concluído' || p.status === 'Em andamento').length,
|
||||
services: services.length,
|
||||
activeServices: services.filter(s => s.active).length,
|
||||
contacts: contacts.length,
|
||||
unreadContacts: contacts.filter(c => !c.read).length,
|
||||
});
|
||||
|
||||
// Projetos recentes (últimos 5)
|
||||
setRecentProjects(
|
||||
projects
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.slice(0, 5)
|
||||
);
|
||||
|
||||
// Contatos recentes (últimos 5)
|
||||
setRecentContacts(
|
||||
contacts
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.slice(0, 5)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar dados do dashboard:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const formatTimeAgo = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 60) return `Há ${diffMins} min`;
|
||||
if (diffHours < 24) return `Há ${diffHours}h`;
|
||||
if (diffDays < 7) return `Há ${diffDays}d`;
|
||||
return date.toLocaleDateString('pt-BR');
|
||||
};
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(n => n[0])
|
||||
.slice(0, 2)
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
};
|
||||
|
||||
const getStatusStyle = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Concluído':
|
||||
return 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400';
|
||||
case 'Em andamento':
|
||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<i className="ri-loader-4-line animate-spin text-4xl text-primary"></i>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
@@ -7,71 +149,124 @@ export default function AdminDashboard() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{[
|
||||
{ label: 'Projetos Ativos', value: '12', icon: 'ri-briefcase-line', color: 'text-blue-500', bg: 'bg-blue-50 dark:bg-blue-900/20' },
|
||||
{ label: 'Mensagens Novas', value: '5', icon: 'ri-message-3-line', color: 'text-green-500', bg: 'bg-green-50 dark:bg-green-900/20' },
|
||||
{ label: 'Serviços', value: '8', icon: 'ri-tools-line', color: 'text-orange-500', bg: 'bg-orange-50 dark:bg-orange-900/20' },
|
||||
{ label: 'Visitas Hoje', value: '145', icon: 'ri-eye-line', color: 'text-purple-500', bg: 'bg-purple-50 dark:bg-purple-900/20' },
|
||||
].map((stat, index) => (
|
||||
<div key={index} className="bg-white dark:bg-secondary p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm cursor-pointer hover:shadow-md transition-all">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${stat.bg} ${stat.color}`}>
|
||||
<i className={`${stat.icon} text-2xl`}></i>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-secondary dark:text-white">{stat.value}</span>
|
||||
<Link href="/admin/projetos" className="bg-white dark:bg-secondary p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm cursor-pointer hover:shadow-md transition-all">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-blue-50 dark:bg-blue-900/20 text-blue-500">
|
||||
<i className="ri-briefcase-line text-2xl"></i>
|
||||
</div>
|
||||
<h3 className="text-gray-500 dark:text-gray-400 font-medium">{stat.label}</h3>
|
||||
<span className="text-2xl font-bold text-secondary dark:text-white">{stats.projects}</span>
|
||||
</div>
|
||||
))}
|
||||
<h3 className="text-gray-500 dark:text-gray-400 font-medium">Projetos</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{stats.activeProjects} ativos</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/contatos" className="bg-white dark:bg-secondary p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm cursor-pointer hover:shadow-md transition-all">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-green-50 dark:bg-green-900/20 text-green-500">
|
||||
<i className="ri-message-3-line text-2xl"></i>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-secondary dark:text-white">{stats.contacts}</span>
|
||||
</div>
|
||||
<h3 className="text-gray-500 dark:text-gray-400 font-medium">Mensagens</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{stats.unreadContacts} não lidas</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/servicos" className="bg-white dark:bg-secondary p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm cursor-pointer hover:shadow-md transition-all">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-orange-50 dark:bg-orange-900/20 text-orange-500">
|
||||
<i className="ri-tools-line text-2xl"></i>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-secondary dark:text-white">{stats.services}</span>
|
||||
</div>
|
||||
<h3 className="text-gray-500 dark:text-gray-400 font-medium">Serviços</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{stats.activeServices} ativos</p>
|
||||
</Link>
|
||||
|
||||
<div className="bg-white dark:bg-secondary p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-purple-50 dark:bg-purple-900/20 text-purple-500">
|
||||
<i className="ri-eye-line text-2xl"></i>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-secondary dark:text-white">—</span>
|
||||
</div>
|
||||
<h3 className="text-gray-500 dark:text-gray-400 font-medium">Visitas</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">Em breve</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div className="bg-white dark:bg-secondary p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-bold text-secondary dark:text-white">Últimas Mensagens</h3>
|
||||
<button className="text-primary text-sm font-bold hover:underline cursor-pointer">Ver todas</button>
|
||||
<Link href="/admin/contatos" className="text-primary text-sm font-bold hover:underline">Ver todas</Link>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-start gap-4 p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-white/5 transition-colors border border-transparent hover:border-gray-100 dark:hover:border-white/5 cursor-pointer">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-white/10 flex items-center justify-center shrink-0">
|
||||
<span className="font-bold text-gray-500 dark:text-gray-400">JD</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h4 className="font-bold text-secondary dark:text-white text-sm">João da Silva</h4>
|
||||
<span className="text-xs text-gray-400">Há 2 horas</span>
|
||||
{recentContacts.length === 0 ? (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-center py-8">Nenhuma mensagem recebida.</p>
|
||||
) : (
|
||||
recentContacts.map((contact) => (
|
||||
<Link
|
||||
key={contact.id}
|
||||
href="/admin/contatos"
|
||||
className="flex items-start gap-4 p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-white/5 transition-colors border border-transparent hover:border-gray-100 dark:hover:border-white/5 cursor-pointer"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<span className="font-bold text-primary text-sm">{getInitials(contact.name)}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
|
||||
Gostaria de solicitar um orçamento para adequação de frota conforme NR-12...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h4 className="font-bold text-secondary dark:text-white text-sm truncate">
|
||||
{contact.name}
|
||||
{!contact.read && (
|
||||
<span className="ml-2 w-2 h-2 bg-primary rounded-full inline-block"></span>
|
||||
)}
|
||||
</h4>
|
||||
<span className="text-xs text-gray-400 shrink-0 ml-2">{formatTimeAgo(contact.createdAt)}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
|
||||
{contact.message}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-secondary p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-bold text-secondary dark:text-white">Projetos Recentes</h3>
|
||||
<button className="text-primary text-sm font-bold hover:underline cursor-pointer">Ver todos</button>
|
||||
<Link href="/admin/projetos" className="text-primary text-sm font-bold hover:underline">Ver todos</Link>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center gap-4 p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-white/5 transition-colors border border-transparent hover:border-gray-100 dark:hover:border-white/5 cursor-pointer">
|
||||
<div className="w-16 h-12 rounded-lg bg-gray-200 dark:bg-white/10 overflow-hidden">
|
||||
{/* Placeholder image */}
|
||||
<div className="w-full h-full bg-gray-300 dark:bg-white/20"></div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-bold text-secondary dark:text-white text-sm">Adequação Coca-Cola</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Engenharia Veicular</p>
|
||||
</div>
|
||||
<span className="px-3 py-1 rounded-full text-xs font-bold bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
||||
Concluído
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{recentProjects.length === 0 ? (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-center py-8">Nenhum projeto cadastrado.</p>
|
||||
) : (
|
||||
recentProjects.map((project) => (
|
||||
<Link
|
||||
key={project.id}
|
||||
href={`/admin/projetos/${project.id}/editar`}
|
||||
className="flex items-center gap-4 p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-white/5 transition-colors border border-transparent hover:border-gray-100 dark:hover:border-white/5 cursor-pointer"
|
||||
>
|
||||
<div className="w-16 h-12 rounded-lg bg-gray-200 dark:bg-white/10 overflow-hidden shrink-0">
|
||||
{project.coverImage ? (
|
||||
<img src={project.coverImage} alt={project.title} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-300 dark:bg-white/20 flex items-center justify-center">
|
||||
<i className="ri-image-line text-gray-400"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-bold text-secondary dark:text-white text-sm truncate">{project.title}</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{project.category}</p>
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-bold shrink-0 ${getStatusStyle(project.status)}`}>
|
||||
{project.status}
|
||||
</span>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user