first commit

This commit is contained in:
rodrigo committed 2025-04-10 23:07:21 -03:00
commit c9fb67e751
423 files changed
+53287

No files matched your search

@@ -0,0 +1,50 @@
import React from 'react';
import { useQuery } from 'react-query';
import { format, differenceInDays } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
import api from '../../services/api';
export const AlertasPrazos: React.FC = () => {
const { data: prazos = [] } = useQuery('prazosFuturos', async () => {
const response = await api.get('/dashboard/prazos/proximos');
return response.data;
});
const getPrioridadeStyle = (dias: number) => {
if (dias <= 3) return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
if (dias <= 7) return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
};
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Prazos Próximos
</h2>
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
</div>
<div className="space-y-4">
{prazos.map((prazo: any) => {
const dias = differenceInDays(new Date(prazo.dataLimite), new Date());
return (
<div key={prazo.id} className="flex items-center justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-gray-900 dark:text-white">
{prazo.descricao}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Processo: {prazo.processo.numero}
</p>
</div>
<span className={`px-2 py-1 rounded-full text-xs ${getPrioridadeStyle(dias)}`}>
{format(new Date(prazo.dataLimite), 'dd/MM/yyyy', { locale: ptBR })}
</span>
</div>
);
})}
</div>
</div>
);
};
@@ -0,0 +1,45 @@
import React from 'react';
import { useQuery } from 'react-query';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import api from '../../services/api';
export const AtividadesRecentes: React.FC = () => {
const { data: atividades = [] } = useQuery('atividades', async () => {
const response = await api.get('/dashboard/atividades');
return response.data;
});
const formatData = (data: string | Date | null) => {
if (!data) return '';
try {
return format(new Date(data), "dd 'de' MMMM 'às' HH:mm", { locale: ptBR });
} catch (error) {
console.error('Erro ao formatar data:', error);
return 'Data inválida';
}
};
return (
<div className="p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Atividades Recentes
</h2>
<div className="space-y-4">
{atividades.map((atividade: any) => (
<div key={atividade.id} className="flex items-start space-x-3">
<span className="text-2xl">{atividade.tipo === 'PROCESSO' ? '⚖️' : '📋'}</span>
<div className="flex-1">
<p className="text-sm text-gray-900 dark:text-white">
{atividade.descricao}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{formatData(atividade.data)}
</p>
</div>
</div>
))}
</div>
</div>
);
};
+92
View File
@@ -0,0 +1,92 @@
import React from 'react';
import {
ArrowUpIcon,
ArrowDownIcon,
} from '@heroicons/react/24/outline';
interface CardProps {
title: string;
value: string | number;
trend?: number;
trendLabel?: string;
icon?: string;
color?: 'blue' | 'green' | 'red' | 'yellow' | 'purple';
items?: Array<{ label: string; value: string | number }>;
type?: 'default' | 'list';
}
export const Card: React.FC<CardProps> = ({
title,
value,
trend,
trendLabel,
icon,
color = 'blue',
items,
type = 'default'
}) => {
const colorClasses = {
blue: 'text-blue-600',
green: 'text-green-600',
red: 'text-red-600',
yellow: 'text-yellow-600',
purple: 'text-purple-600'
};
const bgColorClasses = {
blue: 'bg-blue-50',
green: 'bg-green-50',
red: 'bg-red-50',
yellow: 'bg-yellow-50',
purple: 'bg-purple-50'
};
if (type === 'list' && items) {
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-sm font-medium text-gray-900 mb-4">{title}</h3>
<div className="space-y-2">
{items.map((item, index) => (
<div key={index} className="flex justify-between items-center">
<span className="text-sm text-gray-500">{item.label}</span>
<span className="text-sm font-medium text-gray-900">{item.value}</span>
</div>
))}
</div>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow p-4">
<div className="flex justify-between items-start">
<h3 className="text-sm font-medium text-gray-900">{title}</h3>
{icon && (
<div className={`p-2 rounded-lg ${bgColorClasses[color]}`}>
{/* Renderizar ícone dinamicamente */}
</div>
)}
</div>
<div className="mt-2 flex items-baseline">
<p className={`text-2xl font-semibold ${colorClasses[color]}`}>
{value}
</p>
{trend !== undefined && (
<div className={`ml-2 flex items-center text-sm ${trend >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{trend >= 0 ? (
<ArrowUpIcon className="h-4 w-4" />
) : (
<ArrowDownIcon className="h-4 w-4" />
)}
<span className="ml-1">{Math.abs(trend)}%</span>
{trendLabel && (
<span className="ml-1 text-gray-500">
{trendLabel}
</span>
)}
</div>
)}
</div>
</div>
);
};
@@ -0,0 +1,53 @@
import React from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { useDashboardStats } from '../../hooks/useDashboardStats';
import { ProcessosStats } from './stats/ProcessosStats';
import { TarefasStats } from './stats/TarefasStats';
import { AtendimentosStats } from './stats/AtendimentosStats';
import { FinanceiroStats } from './stats/FinanceiroStats';
import { AlertasPrazos } from './AlertasPrazos';
import { AtividadesRecentes } from './AtividadesRecentes';
import { TaxaSucessoCard } from './TaxaSucessoCard';
import { KPIsFinanceiros } from './stats/KPIsFinanceiros';
export const DashboardLayout: React.FC = () => {
const { user } = useAuth();
const {
processosStats,
tarefasStats,
atendimentosStats,
financeiroStats,
proximosPrazos,
atividades
} = useDashboardStats();
const isAdminOrAdvogado = ['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user?.perfil || '');
return (
<div className="space-y-6 p-6">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{processosStats.data && <ProcessosStats data={processosStats.data} />}
{tarefasStats.data && <TarefasStats data={tarefasStats.data} />}
{atendimentosStats.data && <AtendimentosStats data={atendimentosStats.data} />}
{isAdminOrAdvogado && financeiroStats.data && (
<FinanceiroStats data={financeiroStats.data} />
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Coluna da esquerda */}
<div className="lg:col-span-2 space-y-6">
<TaxaSucessoCard />
{isAdminOrAdvogado && <KPIsFinanceiros />}
</div>
{/* Coluna da direita */}
<div className="space-y-6">
<AlertasPrazos />
<AtividadesRecentes />
</div>
</div>
</div>
);
};
@@ -0,0 +1,66 @@
import React from 'react';
import { useQuery } from 'react-query';
import api from '../../services/api';
export const KPIsAdvogados: React.FC = () => {
const { data: kpis } = useQuery('advogadosKpis', async () => {
const response = await api.get('/dashboard/advogados/kpis');
return response.data;
});
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Produtividade da Equipe
</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-900">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Advogado
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Tarefas Concluídas
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Processos Concluídos
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Produtividade
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{kpis?.map((kpi: any) => (
<tr key={kpi.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{kpi.nome}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{kpi.tarefasConcluidas} / {kpi.tarefasCriadas}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{kpi.processosConcluidos}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${kpi.produtividade}%` }}
/>
</div>
<span className="text-xs text-gray-500 dark:text-gray-400">
{kpi.produtividade}%
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
@@ -0,0 +1,39 @@
import React from 'react';
import { useQuery } from 'react-query';
import api from '../../services/api';
export const TaxaSucessoCard: React.FC = () => {
const { data: stats } = useQuery('taxa-sucesso', async () => {
const response = await api.get('/dashboard/processos/taxa-sucesso');
return response.data;
});
if (!stats) return null;
const estatisticas = stats || [];
const total = estatisticas.reduce((acc: number, curr: any) => acc + (curr._count || 0), 0);
return (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Taxa de Sucesso</h3>
<div className="flex items-center justify-between">
<div className="text-3xl font-bold text-green-600">
{estatisticas.find((s: any) => s.resultado === 'PROCEDENTE')?._count
? Math.round((estatisticas.find((s: any) => s.resultado === 'PROCEDENTE')._count / total) * 100)
: 0}%
</div>
<div className="text-sm text-gray-500">
Total julgados: {total}
</div>
</div>
<div className="mt-4 space-y-2">
{estatisticas.map((item: any) => (
<div key={item.resultado} className="flex justify-between">
<span className="text-sm text-gray-600">{item.resultado}</span>
<span className="text-sm font-medium">{Math.round((item._count / total) * 100)}%</span>
</div>
))}
</div>
</div>
);
};
@@ -0,0 +1,36 @@
import React from 'react';
import { useQuery } from 'react-query';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import api from '../../../services/api';
export const MapaCalor: React.FC = () => {
const { data: atividadesDiarias } = useQuery('atividadesDiarias', async () => {
const response = await api.get('/dashboard/atividades/diarias');
return response.data;
});
const intensidadeCor = (quantidade: number) => {
if (quantidade === 0) return 'bg-gray-100 dark:bg-gray-800';
if (quantidade <= 3) return 'bg-blue-100 dark:bg-blue-900';
if (quantidade <= 6) return 'bg-blue-300 dark:bg-blue-700';
return 'bg-blue-500 dark:bg-blue-500';
};
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Mapa de Atividades
</h2>
<div className="grid grid-cols-7 gap-2">
{atividadesDiarias?.map((dia: any) => (
<div
key={dia.data}
className={`aspect-square rounded-sm ${intensidadeCor(dia.quantidade)}`}
title={`${format(new Date(dia.data), "dd 'de' MMMM", { locale: ptBR })}: ${dia.quantidade} atividades`}
/>
))}
</div>
</div>
);
};
@@ -0,0 +1,39 @@
import React from 'react';
import { useQuery } from 'react-query';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import api from '../../../services/api';
import { useAuth } from '../../../contexts/AuthContext';
import { formatCurrency } from '../../../utils/formatters';
export const PrevisaoReceitasChart: React.FC = () => {
const { user } = useAuth();
if (!['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user?.perfil || '')) {
return null;
}
const { data: previsao } = useQuery('previsaoReceitas', async () => {
const response = await api.get('/dashboard/financeiro/previsao');
return response.data;
});
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Previsão de Receitas
</h2>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={previsao}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mes" />
<YAxis tickFormatter={(value) => formatCurrency(value)} />
<Tooltip formatter={(value) => formatCurrency(value)} />
<Bar dataKey="honorarios" name="Honorários" fill="#10B981" />
<Bar dataKey="custas" name="Custas" fill="#6366F1" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};
@@ -0,0 +1,18 @@
import React from 'react';
import { useAuth } from '../../../contexts/AuthContext';
import { ProcessosEvolucao } from './ProcessosEvolucao';
import { TaxaSucessoChart } from './TaxaSucessoChart';
import { PrevisaoReceitasChart } from './PrevisaoReceitasChart';
export const ProcessosCharts: React.FC = () => {
const { user } = useAuth();
const isAdminOrAdvogado = user?.perfil === 'ADVOGADO_ADMIN' || user?.perfil === 'ADVOGADO';
return (
<>
<ProcessosEvolucao />
<TaxaSucessoChart />
{isAdminOrAdvogado && <PrevisaoReceitasChart />}
</>
);
};
@@ -0,0 +1,31 @@
import React from 'react';
import { useQuery } from 'react-query';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import api from '../../../services/api';
export const ProcessosEvolucao: React.FC = () => {
const { data: evolucao } = useQuery('processosEvolucao', async () => {
const response = await api.get('/dashboard/processos/evolucao');
return response.data;
});
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Evolução de Processos
</h2>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={evolucao}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mes" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="novos" stroke="#3B82F6" name="Novos" />
<Line type="monotone" dataKey="concluidos" stroke="#10B981" name="Concluídos" />
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
};
@@ -0,0 +1,42 @@
import React from 'react';
import { useQuery } from 'react-query';
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
import api from '../../../services/api';
export const TaxaSucessoChart: React.FC = () => {
const { data: stats } = useQuery('taxaSucesso', async () => {
const response = await api.get('/dashboard/processos/taxa-sucesso');
return response.data;
});
const COLORS = ['#10B981', '#EF4444', '#F59E0B'];
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Taxa de Sucesso em Processos
</h2>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={stats}
dataKey="valor"
nameKey="categoria"
cx="50%"
cy="50%"
outerRadius={80}
label
>
{stats?.map((entry: any, index: number) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
</div>
);
};
@@ -0,0 +1,35 @@
import React from 'react';
import { useQuery } from 'react-query';
import { Card } from '../Card';
import api from '../../../services/api';
export const AtendimentosStats: React.FC = () => {
const { data: stats } = useQuery('atendimentosStats', async () => {
const response = await api.get('/dashboard/atendimentos/stats');
return response.data;
});
return (
<div className="grid gap-4">
<Card
title="Atendimentos Hoje"
value={stats?.hoje || 0}
icon="Calendar"
color="blue"
/>
<Card
title="Próximos 7 dias"
value={stats?.proximaSemana || 0}
icon="CalendarDays"
color="purple"
/>
<Card
title="Total no Mês"
value={stats?.totalMes || 0}
trend={stats?.crescimentoMes}
icon="ChartBar"
color="green"
/>
</div>
);
};
@@ -0,0 +1,37 @@
import React from 'react';
import { useQuery } from 'react-query';
import { Card } from '../Card';
import api from '../../../services/api';
import { formatCurrency } from '../../../utils/formatters';
export const FinanceiroStats: React.FC = () => {
const { data: stats } = useQuery('financeiroStats', async () => {
const response = await api.get('/dashboard/financeiro/stats');
return response.data;
});
return (
<div className="grid gap-4">
<Card
title="Honorários a Receber"
value={formatCurrency(stats?.honorariosReceber || 0)}
icon="BanknotesIcon"
color="green"
/>
<Card
title="Custas do Mês"
value={formatCurrency(stats?.custasMes || 0)}
trend={stats?.variacaoCustas}
icon="ReceiptPercentIcon"
color="red"
/>
<Card
title="Receitas (Mês)"
value={formatCurrency(stats?.receitasMes || 0)}
trend={stats?.crescimentoReceitas}
icon="CurrencyDollarIcon"
color="blue"
/>
</div>
);
};
@@ -0,0 +1,44 @@
import React from 'react';
import { useQuery } from 'react-query';
import { useAuth } from '../../../contexts/AuthContext';
import { Card } from '../../dashboard/Card';
import api from '../../../services/api';
import { formatCurrency } from '../../../utils/formatters';
export const KPIsFinanceiros: React.FC = () => {
const { user } = useAuth();
if (!['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user?.perfil || '')) {
return null;
}
const { data: stats } = useQuery('financeiroKPIs', async () => {
const response = await api.get('/dashboard/financeiro/stats');
return response.data;
});
return (
<div className="grid gap-4">
<Card
title="Faturamento Previsto"
value={formatCurrency(stats?.previsaoFaturamento || 0)}
trend={stats?.crescimentoFaturamento}
icon="BanknotesIcon"
color="green"
/>
<Card
title="Taxa de Sucesso"
value={`${stats?.taxaSucesso || 0}%`}
trend={stats?.crescimentoTaxaSucesso}
icon="ChartBarIcon"
color="blue"
/>
<Card
title="Custas em Aberto"
value={formatCurrency(stats?.custasEmAberto || 0)}
icon="ExclamationTriangleIcon"
color="yellow"
/>
</div>
);
};
@@ -0,0 +1,32 @@
import React from 'react';
import { useQuery } from 'react-query';
import api from '../../../services/api';
import { Card } from '../Card';
export const ProcessosStats: React.FC = () => {
const { data: stats } = useQuery('processosStats', async () => {
const response = await api.get('/dashboard/processos/stats');
return response.data;
});
return (
<div className="grid gap-4">
<Card
title="Total de Processos"
value={stats?.total || 0}
trend={stats?.crescimento || 0}
icon="DocumentText"
/>
<Card
title="Por Tribunal"
items={stats?.porTribunal || []}
type="list"
/>
<Card
title="Por Status"
items={stats?.porStatus || []}
type="list"
/>
</div>
);
};
@@ -0,0 +1,61 @@
import React from 'react';
import { useQuery } from 'react-query';
import { Card } from '../Card';
import api from '../../../services/api';
export const TarefasStats: React.FC = () => {
const { data: stats } = useQuery('tarefasStats', async () => {
try {
const response = await api.get('/dashboard/tarefas/stats');
return response.data;
} catch (error) {
console.error('Erro ao buscar estatísticas:', error);
return {
pendentes: 0,
atrasadas: 0,
concluidas: 0,
total: 0,
taxaConclusao: 0,
porStatus: { PENDENTE: 0 }
};
}
});
const defaultStats = {
pendentes: 0,
atrasadas: 0,
concluidas: 0,
total: 0,
taxaConclusao: 0,
porStatus: { PENDENTE: 0 }
};
const safeStats = stats || defaultStats;
return (
<div className="grid gap-4">
<Card
title="Tarefas Pendentes"
value={safeStats.porStatus?.PENDENTE || 0}
trend={safeStats.atrasadas}
trendLabel="atrasadas"
icon="ClipboardDocumentList"
color="yellow"
/>
<Card
title="Para Hoje"
value={safeStats.pendentes || 0}
icon="Clock"
color="blue"
/>
<Card
title="Concluídas (Mês)"
value={safeStats.concluidas || 0}
trend={safeStats.taxaConclusao}
trendLabel="taxa de conclusão"
icon="CheckCircle"
color="green"
/>
</div>
);
};