diff --git a/src/app/(protected)/administrativo/(client)/clientes/[id]/page.tsx b/src/app/(protected)/administrativo/(client)/clientes/[id]/page.tsx index 818ea63..8f9fe33 100644 --- a/src/app/(protected)/administrativo/(client)/clientes/[id]/page.tsx +++ b/src/app/(protected)/administrativo/(client)/clientes/[id]/page.tsx @@ -3,6 +3,7 @@ import React from "react"; import { useParams } from 'next/navigation'; import { useEffect } from 'react'; +import Link from 'next/link'; // Helpers e hooks customizados import { formatLogDateTime } from "@/shared/utils/formatLogDateTime"; @@ -15,6 +16,7 @@ import { useLogDatabaseHook } from '@/packages/administrativo/hooks/Log/useLogDa import { useLogGedHook } from '@/packages/administrativo/hooks/Log/useLogGedHook'; import { useLogDiskHook } from '@/packages/administrativo/hooks/Log/useLogDiskHook'; import { useLogBackupHook } from '@/packages/administrativo/hooks/Log/useLogBackupHook'; +import { useLogWarningHook } from '@/packages/administrativo/hooks/Log/useLogWarningHook'; // Componentes do Shadcn/UI import { @@ -43,6 +45,7 @@ export default function ClientePage() { const { logGed, fetchLogGed } = useLogGedHook(); const { logDisk, fetchLogDisk } = useLogDiskHook(); const { logBackup, fetchLogBackup } = useLogBackupHook(); + const { logWarning, fetchLogWarning } = useLogWarningHook(); // Efeito responsável por buscar logs de forma sequencial useEffect(() => { @@ -102,6 +105,17 @@ export default function ClientePage() { } catch (error) { console.error("Erro ao buscar log do Backup:", error); + } finally { + // E SOMENTE após a conclusão da busca do Backup + // (mesmo que dê erro ou traga 0 registros), + // executa a busca no Warning + try { + + await fetchLogWarning(Number(id)); + } catch (error) { + console.error("Erro ao buscar log do Warning:", error); + + } } } } @@ -123,17 +137,25 @@ export default function ClientePage() { // ============================================================ return (
-

- {logServer?.cns ?? 'CNS não disponível'} - {logServer?.cartorio ?? 'Cartório não disponível'} -

+ + +

+ {logServer?.cns ?? 'CNS não disponível'} - {logServer?.cartorio ?? 'Cartório não disponível'} + :: + + Voltar + +

+ - + Informações do Servidor Banco de Dados GED Unidades de Disco Backup + Avisos {/* ===================================================== */} @@ -620,7 +642,7 @@ export default function ClientePage() { {/* ===================================================== */} - {/* Aba: Informação do servidor */} + {/* Aba: Informação do Backup */} {/* ===================================================== */} {logBackup?.data ? ( @@ -689,6 +711,54 @@ export default function ClientePage() { + {/* ===================================================== */} + {/* Aba: Avisos (Warnings) */} + {/* ===================================================== */} + + {logWarning?.data?.warning && logWarning.data.warning.length > 0 ? ( +
+ {/* Cabeçalho com data e hora */} +
+ + Data do log: {logWarning?.data?.data ?? "Não informado"} às {logWarning?.data?.hora ?? "00:00"} + +
+ + {/* Card de lista de avisos */} + + + Lista de Avisos + + + + + + + # + Mensagem + + + + {logWarning.data.warning.map((msg: string, index: number) => ( + + {index + 1} + {msg} + + ))} + +
+
+
+
+
+ ) : ( +

+ Nenhum aviso disponível para este cliente. +

+ )} +
+ +
); diff --git a/src/app/(protected)/layout.tsx b/src/app/(protected)/layout.tsx index 49d55c9..a14024c 100644 --- a/src/app/(protected)/layout.tsx +++ b/src/app/(protected)/layout.tsx @@ -56,7 +56,7 @@ export default function RootLayout({ - + Orius Tecnologia diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index a0361a8..a57a24a 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -8,7 +8,6 @@ import { } from 'lucide-react'; import { NavMain } from '@/components/nav-main'; -import { NavProjects } from '@/components/nav-projects'; import { NavUser } from '@/components/nav-user'; import { Sidebar, diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx new file mode 100644 index 0000000..a70e713 --- /dev/null +++ b/src/components/ui/spinner.tsx @@ -0,0 +1,16 @@ +import { Loader2Icon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Spinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + ) +} + +export { Spinner } diff --git a/src/config/app.json b/src/config/app.json index 6a28307..aa128cf 100644 --- a/src/config/app.json +++ b/src/config/app.json @@ -4,5 +4,6 @@ "url": "https://monitoring-api.oriustecnologia.com/", "prefix": "api/v1/", "content_type": "application/json" - } + }, + "api_debit": "https://admin.oriustecnologia.com/router.php" } \ No newline at end of file diff --git a/src/packages/administrativo/components/Client/ClientTable.tsx b/src/packages/administrativo/components/Client/ClientTable.tsx index 54325f8..c233636 100644 --- a/src/packages/administrativo/components/Client/ClientTable.tsx +++ b/src/packages/administrativo/components/Client/ClientTable.tsx @@ -1,6 +1,9 @@ -'use client'; +'use client'; // Indica que este componente é executado no lado do cliente (Next.js) +import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; + +// Componentes de UI import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -20,92 +23,190 @@ import { TableRow, } from '@/components/ui/table'; -import { ChartPie, EllipsisIcon, PencilIcon, Trash2Icon } from 'lucide-react'; +// Ícones da biblioteca Lucide +import { ChartPie, EllipsisIcon, PencilIcon } from 'lucide-react'; +import { Spinner } from "@/components/ui/spinner" + +// Interfaces e componentes auxiliares import { ClientTableInterface } from '../../interfaces/Client/ClienteTableInterface'; import { StatusBadge } from '@/components/StatusBadge'; +import { CheckLiberationHelper } from '@/shared/utils/CheckLiberationHelper'; +import FinanceStatusDialog from '@/shared/components/finance/FinanceStatusDialog'; + +// Tipagem do retorno financeiro do helper +interface FinancialStatus { + status: string; + message: string; + expired_count: number; + next_due_count?: number; + details: { + type: string; + description: string; + reference: string; + days: number; + }[]; +} export default function ClientTable({ data, pagination, onEdit, onDelete }: ClientTableInterface) { - - // Inicializa o roteador - const router = useRouter(); + const router = useRouter(); // Hook do Next.js para navegação + + // Estado que armazena o status financeiro de cada cliente, indexado pelo ID + const [financialStatus, setFinancialStatus] = useState>({}); + + // Efeito que busca o status financeiro de todos os clientes ao carregar ou atualizar a tabela + useEffect(() => { + // Se não houver dados válidos, encerra a execução + if (!Array.isArray(data)) return; + + // Função assíncrona responsável por consultar os status financeiros + const fetchStatus = async () => { + const newStatuses: Record = {}; // Armazena os resultados temporariamente + + // Faz as requisições em paralelo para cada cliente + await Promise.all( + data.map(async (client) => { + const res = await CheckLiberationHelper(client.cns); // Chama o helper externo + + // Normaliza o retorno e armazena no objeto temporário + newStatuses[client.client_id] = { + status: res.status || 'indefinido', + message: res.message || '', + expired_count: res.expired_count || 0, + next_due_count: res.next_due_count || 0, + details: res.details || [], + }; + }) + ); + + // Atualiza o estado global com todos os resultados de uma vez + setFinancialStatus(newStatuses); + }; + + // Executa a função + fetchStatus(); + }, [data]); // Dependência: roda novamente se o array de clientes mudar return ( + {/* Cabeçalho da tabela */} # Status + Financeiro CNS - Nome + Nome Ações + {/* Corpo da tabela */} - {Array.isArray(data) && data.length > 0 ? ( data.map((client) => ( - - {client.client_id} - - - - {client.cns} - {client.name} - - - - - + {Array.isArray(data) && data.length > 0 ? ( + data.map((client) => ( + + {/* Coluna: ID do cliente */} + {client.client_id} - - - onEdit(client)} - > - - Editar - - - router.push(`/administrativo/clientes/${client.client_id}`)} - > - - Dashboard - - {/* - onDelete(client)} - > - - Remover - */} + {/* Coluna: Status cadastral (ativo, inativo, etc) */} + + + - - - - - - )) + {/* Coluna: Status financeiro retornado da API */} + + {financialStatus[client.client_id] ? ( + garante que o elemento seja interativo e receba handlers clonados + + } + status={financialStatus[client.client_id].status} + message={financialStatus[client.client_id].message} + details={financialStatus[client.client_id].details} + expired_count={financialStatus[client.client_id].expired_count} + /> + ) : ( + + )} + + + {/* Coluna: CNS do cliente */} + {client.cns} + + {/* Coluna: Nome do cliente */} + {client.name} + + {/* Coluna: Ações (menu suspenso) */} + + + {/* Botão principal do menu */} + + + + + {/* Conteúdo do menu suspenso */} + + + {/* Ação: Editar cliente */} + onEdit(client)} + > + + Editar + + + + + {/* Ação: Ir para dashboard do cliente */} + router.push(`/administrativo/clientes/${client.client_id}`)} + > + + Dashboard + + + + + + + )) ) : ( + // Caso não existam registros - + Nenhum cliente encontrado. - )} + )} + + {/* Rodapé da tabela com totalização */} - + - {/* Se existir paginação, mostra o total de registros; senão, usa o tamanho da lista */} Total de clientes: {pagination?.total_records ?? data?.length}
); -} \ No newline at end of file +} diff --git a/src/packages/administrativo/data/Client/ClientDeleteData.ts b/src/packages/administrativo/data/Client/ClientDeleteData.ts index b4b599e..8a34770 100644 --- a/src/packages/administrativo/data/Client/ClientDeleteData.ts +++ b/src/packages/administrativo/data/Client/ClientDeleteData.ts @@ -11,7 +11,7 @@ import { withClientErrorHandler } from '@/withClientErrorHandler/withClientError // Importa o decorador que adiciona tratamento global de erros às funções assíncronas // Função principal responsável por enviar a requisição de exclusão de um cliente -async function executeClientDeleteData(usuarioId: number) { +async function executeClientDeleteData(client_id: number) { // Cria uma nova instância da classe API, que gerencia headers, baseURL e envio das requisições const api = new API(); @@ -19,7 +19,7 @@ async function executeClientDeleteData(usuarioId: number) { // Envia a requisição DELETE para o endpoint correspondente ao cliente informado const response = await api.send({ 'method': Methods.DELETE, // Define o método HTTP como DELETE - 'endpoint': `administrativo/client/${usuarioId}` // Define o endpoint incluindo o ID do cliente + 'endpoint': `administrativo/client/${client_id}` // Define o endpoint incluindo o ID do cliente }); // Retorna a resposta da API, contendo status, mensagem e possíveis dados adicionais diff --git a/src/packages/administrativo/data/Log/LogWarningData.ts b/src/packages/administrativo/data/Log/LogWarningData.ts new file mode 100644 index 0000000..2e9c8dc --- /dev/null +++ b/src/packages/administrativo/data/Log/LogWarningData.ts @@ -0,0 +1,31 @@ +'use server' +// Indica que este módulo será executado no lado do servidor (Warning Action do Next.js) + +import { Methods } from '@/shared/services/api/enums/ApiMethodEnum'; +// Importa o enumerador que contém os métodos HTTP padronizados (GET, POST, PUT, DELETE) + +import API from '@/shared/services/api/Api'; +// Importa a classe responsável por realizar requisições HTTP à API backend + +import { withClientErrorHandler } from '@/withClientErrorHandler/withClientErrorHandler'; +// Importa o wrapper que padroniza o tratamento de erros e respostas para o cliente + +// Função principal responsável por buscar um usuário específico pelo seu ID +async function executeLogWarningData(client_id: number) { + + // Cria uma nova instância da classe de comunicação com a API + const api = new API(); + + // Envia uma requisição GET ao endpoint que retorna os dados de um usuário específico + const response = await api.send({ + 'method': Methods.GET, // Define o método HTTP da requisição + 'endpoint': `administrativo/log/warning/${client_id}` // Monta dinamicamente o endpoint com o ID do usuário + }); + + // Retorna a resposta recebida da API (dados do usuário ou erro) + return response; +} + +// Exporta a função encapsulada com o handler de erro +// Isso garante que exceções sejam tratadas de forma padronizada na camada superior +export const LogWarningData = withClientErrorHandler(executeLogWarningData); diff --git a/src/packages/administrativo/hooks/Log/useLogWarningHook.ts b/src/packages/administrativo/hooks/Log/useLogWarningHook.ts new file mode 100644 index 0000000..bc3e502 --- /dev/null +++ b/src/packages/administrativo/hooks/Log/useLogWarningHook.ts @@ -0,0 +1,23 @@ +'use client'; + +import { useState } from 'react'; +import { LogWarningInterface } from '../../interfaces/Log/LogWarningInterface'; +import { LogWarningService } from '../../services/Log/LogWarningService'; +import { useResponse } from '@/shared/components/response/ResponseContext'; + +export const useLogWarningHook = () => { + const { setResponse } = useResponse(); + const [logWarning, setLog] = useState(null); + + const fetchLogWarning = async (client_id: number) => { + try { + const response = await LogWarningService(client_id); + setLog(response as LogWarningInterface); + setResponse(response); + } catch (error) { + console.error("Erro ao buscar informação do banco de dados:", error); + } + }; + + return { logWarning, fetchLogWarning }; +}; diff --git a/src/packages/administrativo/interfaces/Log/LogWarningInterface.ts b/src/packages/administrativo/interfaces/Log/LogWarningInterface.ts new file mode 100644 index 0000000..f6861c1 --- /dev/null +++ b/src/packages/administrativo/interfaces/Log/LogWarningInterface.ts @@ -0,0 +1,13 @@ +/** + * Interface que representa o log de banco de dados retornado pelo endpoint /log/database. + */ +export interface LogDiskInterface { + message?: string; + data: { + cns: string; + cartorio: string; + data: string; + hora: string; + warning: [] + }; +} diff --git a/src/packages/administrativo/services/Log/LogWarningService.ts b/src/packages/administrativo/services/Log/LogWarningService.ts new file mode 100644 index 0000000..30c335f --- /dev/null +++ b/src/packages/administrativo/services/Log/LogWarningService.ts @@ -0,0 +1,22 @@ +'use server' +// Indica que este arquivo é um "Warning Action", executado no lado do servidor pelo Next.js + +import { withClientErrorHandler } from "@/withClientErrorHandler/withClientErrorHandler"; +// Importa o wrapper responsável por padronizar o tratamento de erros nas requisições do Loge + +import { LogWarningData } from "../../data/Log/LogWarningData"; +// Importa a função que acessa a camada de dados e retorna as informações do usuário a partir do ID + +// Função assíncrona principal responsável por buscar um usuário pelo seu ID +async function executeLogWarningService(client_id: number) { + + // Executa a função de busca de usuário, passando o ID recebido como parâmetro + const response = await LogWarningData(client_id); + + // Retorna a resposta vinda da camada de dados (usuário encontrado ou erro) + return response; +} + +// Exporta o serviço com o tratamento de erros encapsulado +// O wrapper "withClientErrorHandler" assegura respostas consistentes em caso de falhas +export const LogWarningService = withClientErrorHandler(executeLogWarningService); diff --git a/src/shared/components/finance/FinanceStatusDialog.tsx b/src/shared/components/finance/FinanceStatusDialog.tsx new file mode 100644 index 0000000..1f5899a --- /dev/null +++ b/src/shared/components/finance/FinanceStatusDialog.tsx @@ -0,0 +1,70 @@ +// src/shared/components/finance/FinanceStatusDialog.tsx +'use client'; + +import React from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { format } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; + +interface FinanceDetail { type: string; description: string; reference: string; days: number; } + +interface Props { + status: string; + message?: string; + details?: FinanceDetail[]; + expired_count?: number; + // Aceitar apenas ReactElement garante que as props clonadas pelo DialogTrigger funcionem + triggerElement?: React.ReactElement | null; +} + +export default function FinanceStatusDialog({ + status, + message, + details = [], + expired_count = 0, + triggerElement = null, +}: Props) { + const isLate = status?.toLowerCase() === 'em atraso'; + + return ( + + {/* asChild permite que o Dialog clone o elemento e injete handlers */} + + {triggerElement ?? ( + + )} + + + + + {isLate ? 'Pendências Financeiras' : 'Situação Financeira - Em Dia'} + + +

{message}

+ + + {details.length === 0 ? ( +

Nenhum detalhe disponível.

+ ) : ( +
+ {details.map((item, i) => ( +
+

{item.description}

+

Referência: {format(new Date(item.reference), 'dd/MM/yyyy', { locale: ptBR })}

+

{item.type === 'vencido' ? `${item.days} dia(s) de atraso` : `${Math.abs(item.days)} dia(s) restantes`}

+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/shared/utils/CheckLiberationHelper.ts b/src/shared/utils/CheckLiberationHelper.ts new file mode 100644 index 0000000..e517cc7 --- /dev/null +++ b/src/shared/utils/CheckLiberationHelper.ts @@ -0,0 +1,88 @@ +// src/shared/helpers/CheckLiberationHelper.ts +'use server'; + +import appConfig from '@/config/app.json'; + +/** + * Consulta o status financeiro de um cliente a partir do CNS. + * Faz POST x-www-form-urlencoded para a URL definida em app.json (api_debit). + * Interpreta dinamicamente os campos retornados, como expired, next_due_date etc. + */ +export async function CheckLiberationHelper(cns: string) { + try { + const apiUrl = appConfig.api_debit; + + // Monta o corpo no formato x-www-form-urlencoded + const body = new URLSearchParams({ + TABLE: 'liberation', + ACTION: 'liberation', + FOLDER: 'action', + hash: 'DOa62FKFI9yjcRi/k/n3fSzz67CjrWRW6qCl8IbC3RjQEQHgAPc6Wz0fjXSUlJKdPnRVw/Ejwj3DKTOFNuPxXX4oFzfCUo1Eqqc8tl7sJ1slbRjwL6XSahaXxxboWHiiltirhr/nu5FDeOH1oLxH2w==', + cns: cns, + }); + + // Requisição POST + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: body.toString(), + cache: 'no-store', + }); + + if (!response.ok) { + return { status: 'erro', message: 'Falha ao consultar status financeiro' }; + } + + const json = await response.json(); + + // Se não existir campo "data", retorna mensagem padrão + if (!json?.data) { + return { status: 'indefinido', message: 'Sem informações financeiras' }; + } + + // Extrai dinamicamente as listas retornadas + const data = json.data; + const expired = Array.isArray(data.expired) ? data.expired : []; + const next = Array.isArray(data.next_due_date) ? data.next_due_date : []; + + // Determina status geral com base nas listas + let status = 'em dia'; + if (expired.length > 0) { + status = 'em atraso'; + } else if (next.length > 0) { + const nextDays = next[0]?.days ?? 0; + status = nextDays === 0 ? 'vence hoje' : 'em dia'; + } + + // Cria um resumo com detalhes de cada item (útil para exibir tooltip, modal, etc.) + const details = [ + ...expired.map((item) => ({ + type: 'vencido', + description: item.description, + reference: item.reference, + days: item.days, + })), + ...next.map((item) => ({ + type: 'a_vencer', + description: item.description, + reference: item.reference, + days: item.days, + })), + ]; + + // Retorna objeto completo + return { + code: json.code ?? 200, + status, // "em atraso", "vence hoje", "em dia", etc. + message: json.message ?? 'Sem mensagem', + expired_count: expired.length, + next_due_count: next.length, + details, + }; + } catch (error) { + console.error('Erro ao consultar API externa:', error); + return { status: 'erro', message: 'Erro de comunicação com servidor externo' }; + } +}