feat: dashboard com dados reais de projetos, servicos e contatos
This commit is contained in:
15
frontend/package-lock.json
generated
15
frontend/package-lock.json
generated
@@ -8297,6 +8297,21 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
|
"version": "15.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.0.tgz",
|
||||||
|
"integrity": "sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
@@ -7,71 +149,124 @@ export default function AdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
{[
|
<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">
|
||||||
{ label: 'Projetos Ativos', value: '12', icon: 'ri-briefcase-line', color: 'text-blue-500', bg: 'bg-blue-50 dark:bg-blue-900/20' },
|
<div className="flex items-center justify-between mb-4">
|
||||||
{ label: 'Mensagens Novas', value: '5', icon: 'ri-message-3-line', color: 'text-green-500', bg: 'bg-green-50 dark:bg-green-900/20' },
|
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-blue-50 dark:bg-blue-900/20 text-blue-500">
|
||||||
{ label: 'Serviços', value: '8', icon: 'ri-tools-line', color: 'text-orange-500', bg: 'bg-orange-50 dark:bg-orange-900/20' },
|
<i className="ri-briefcase-line text-2xl"></i>
|
||||||
{ 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>
|
|
||||||
</div>
|
</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>
|
</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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<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="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">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h3 className="text-lg font-bold text-secondary dark:text-white">Últimas Mensagens</h3>
|
<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>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[1, 2, 3].map((i) => (
|
{recentContacts.length === 0 ? (
|
||||||
<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">
|
<p className="text-gray-500 dark:text-gray-400 text-center py-8">Nenhuma mensagem recebida.</p>
|
||||||
<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>
|
recentContacts.map((contact) => (
|
||||||
</div>
|
<Link
|
||||||
<div>
|
key={contact.id}
|
||||||
<div className="flex items-center justify-between mb-1">
|
href="/admin/contatos"
|
||||||
<h4 className="font-bold text-secondary dark:text-white text-sm">João da Silva</h4>
|
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"
|
||||||
<span className="text-xs text-gray-400">Há 2 horas</span>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
|
<div className="flex-1 min-w-0">
|
||||||
Gostaria de solicitar um orçamento para adequação de frota conforme NR-12...
|
<div className="flex items-center justify-between mb-1">
|
||||||
</p>
|
<h4 className="font-bold text-secondary dark:text-white text-sm truncate">
|
||||||
</div>
|
{contact.name}
|
||||||
</div>
|
{!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>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-secondary p-6 rounded-xl border border-gray-200 dark:border-white/10 shadow-sm">
|
<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">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h3 className="text-lg font-bold text-secondary dark:text-white">Projetos Recentes</h3>
|
<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>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[1, 2, 3].map((i) => (
|
{recentProjects.length === 0 ? (
|
||||||
<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">
|
<p className="text-gray-500 dark:text-gray-400 text-center py-8">Nenhum projeto cadastrado.</p>
|
||||||
<div className="w-16 h-12 rounded-lg bg-gray-200 dark:bg-white/10 overflow-hidden">
|
) : (
|
||||||
{/* Placeholder image */}
|
recentProjects.map((project) => (
|
||||||
<div className="w-full h-full bg-gray-300 dark:bg-white/20"></div>
|
<Link
|
||||||
</div>
|
key={project.id}
|
||||||
<div className="flex-1">
|
href={`/admin/projetos/${project.id}/editar`}
|
||||||
<h4 className="font-bold text-secondary dark:text-white text-sm">Adequação Coca-Cola</h4>
|
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"
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Engenharia Veicular</p>
|
>
|
||||||
</div>
|
<div className="w-16 h-12 rounded-lg bg-gray-200 dark:bg-white/10 overflow-hidden shrink-0">
|
||||||
<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">
|
{project.coverImage ? (
|
||||||
Concluído
|
<img src={project.coverImage} alt={project.title} className="w-full h-full object-cover" />
|
||||||
</span>
|
) : (
|
||||||
</div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -11,7 +15,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -19,7 +23,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
@@ -30,5 +36,7 @@
|
|||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user