feat(): Criado a visualização de pendência por cliente e criado o serviço que acesso o endpoit de warning
This commit is contained in:
parent
588f3b3d06
commit
30b5d29c37
13 changed files with 501 additions and 67 deletions
|
|
@ -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 (
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-semibold mb-6">
|
||||
{logServer?.cns ?? 'CNS não disponível'} - {logServer?.cartorio ?? 'Cartório não disponível'}
|
||||
</h1>
|
||||
|
||||
|
||||
<h1 className="text-2xl font-semibold mb-6">
|
||||
{logServer?.cns ?? 'CNS não disponível'} - {logServer?.cartorio ?? 'Cartório não disponível'}
|
||||
<span className="m-4 text-gray-500 "> :: </span>
|
||||
<Link href={`/administrativo/clientes`} className="ml-2 text-gray-500 hover:underline">
|
||||
Voltar
|
||||
</Link>
|
||||
</h1>
|
||||
|
||||
|
||||
<Tabs defaultValue="server" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsList className="grid w-full grid-cols-6">
|
||||
<TabsTrigger className='cursor-pointer' value="server">Informações do Servidor</TabsTrigger>
|
||||
<TabsTrigger className='cursor-pointer' value="database">Banco de Dados</TabsTrigger>
|
||||
<TabsTrigger className='cursor-pointer' value="ged">GED</TabsTrigger>
|
||||
<TabsTrigger className='cursor-pointer' value="disk">Unidades de Disco</TabsTrigger>
|
||||
<TabsTrigger className='cursor-pointer' value="backup">Backup</TabsTrigger>
|
||||
<TabsTrigger className='cursor-pointer' value="warning">Avisos</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* ===================================================== */}
|
||||
|
|
@ -620,7 +642,7 @@ export default function ClientePage() {
|
|||
|
||||
|
||||
{/* ===================================================== */}
|
||||
{/* Aba: Informação do servidor */}
|
||||
{/* Aba: Informação do Backup */}
|
||||
{/* ===================================================== */}
|
||||
<TabsContent value="backup">
|
||||
{logBackup?.data ? (
|
||||
|
|
@ -689,6 +711,54 @@ export default function ClientePage() {
|
|||
</TabsContent>
|
||||
|
||||
|
||||
{/* ===================================================== */}
|
||||
{/* Aba: Avisos (Warnings) */}
|
||||
{/* ===================================================== */}
|
||||
<TabsContent value="warning">
|
||||
{logWarning?.data?.warning && logWarning.data.warning.length > 0 ? (
|
||||
<div className="mt-4 space-y-6">
|
||||
{/* Cabeçalho com data e hora */}
|
||||
<div>
|
||||
<Badge variant="outline" className="ml-2 bg-yellow-100 text-yellow-800 border-yellow-400">
|
||||
Data do log: {logWarning?.data?.data ?? "Não informado"} às {logWarning?.data?.hora ?? "00:00"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Card de lista de avisos */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lista de Avisos</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[400px] w-full rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-center w-[80px]">#</TableHead>
|
||||
<TableHead>Mensagem</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{logWarning.data.warning.map((msg: string, index: number) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="text-center">{index + 1}</TableCell>
|
||||
<TableCell className="text-sm text-gray-800">{msg}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<p className="p-4 text-gray-500 italic">
|
||||
Nenhum aviso disponível para este cliente.
|
||||
</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export default function RootLayout({
|
|||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="#">
|
||||
<BreadcrumbLink href="/">
|
||||
Orius Tecnologia
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
16
src/components/ui/spinner.tsx
Normal file
16
src/components/ui/spinner.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Loader2Icon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cn("size-4 animate-spin", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Spinner }
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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<Record<number, FinancialStatus>>({});
|
||||
|
||||
// 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<number, FinancialStatus> = {}; // 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 (
|
||||
<Table>
|
||||
{/* Cabeçalho da tabela */}
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Financeiro</TableHead>
|
||||
<TableHead>CNS</TableHead>
|
||||
<TableHead>Nome</TableHead>
|
||||
<TableHead>Nome</TableHead>
|
||||
<TableHead className="text-right">Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
{/* Corpo da tabela */}
|
||||
<TableBody>
|
||||
{Array.isArray(data) && data.length > 0 ? ( data.map((client) => (
|
||||
<TableRow key={client.client_id}>
|
||||
<TableCell className="font-medium">{client.client_id}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={client.status as any} />
|
||||
</TableCell>
|
||||
<TableCell>{client.cns}</TableCell>
|
||||
<TableCell>{client.name}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer">
|
||||
<EllipsisIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
{Array.isArray(data) && data.length > 0 ? (
|
||||
data.map((client) => (
|
||||
<TableRow key={client.client_id}>
|
||||
{/* Coluna: ID do cliente */}
|
||||
<TableCell className="font-medium">{client.client_id}</TableCell>
|
||||
|
||||
<DropdownMenuContent side="left" align="start">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => onEdit(client)}
|
||||
>
|
||||
<PencilIcon className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => router.push(`/administrativo/clientes/${client.client_id}`)}
|
||||
>
|
||||
<ChartPie className="mr-2 h-4 w-4" />
|
||||
Dashboard
|
||||
</DropdownMenuItem>
|
||||
{/* <DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => onDelete(client)}
|
||||
>
|
||||
<Trash2Icon className="mr-2 h-4 w-4" />
|
||||
Remover
|
||||
</DropdownMenuItem> */}
|
||||
{/* Coluna: Status cadastral (ativo, inativo, etc) */}
|
||||
<TableCell>
|
||||
<StatusBadge status={client.status as any} />
|
||||
</TableCell>
|
||||
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
{/* Coluna: Status financeiro retornado da API */}
|
||||
<TableCell>
|
||||
{financialStatus[client.client_id] ? (
|
||||
<FinanceStatusDialog
|
||||
triggerElement={
|
||||
// Usar <button> garante que o elemento seja interativo e receba handlers clonados
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
financialStatus[client.client_id].status === 'em atraso'
|
||||
? 'text-red-600 font-semibold cursor-pointer underline underline-offset-2 bg-transparent border-0 p-0'
|
||||
: 'text-green-600 font-semibold cursor-pointer underline underline-offset-2 bg-transparent border-0 p-0'
|
||||
}
|
||||
title={financialStatus[client.client_id].message}
|
||||
>
|
||||
{financialStatus[client.client_id].status.toUpperCase()}
|
||||
{financialStatus[client.client_id].expired_count > 0 &&
|
||||
` (${financialStatus[client.client_id].expired_count} pendência${
|
||||
financialStatus[client.client_id].expired_count > 1 ? 's' : ''
|
||||
})`}
|
||||
</button>
|
||||
}
|
||||
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}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-400 italic"><Spinner /></span>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* Coluna: CNS do cliente */}
|
||||
<TableCell>{client.cns}</TableCell>
|
||||
|
||||
{/* Coluna: Nome do cliente */}
|
||||
<TableCell>{client.name}</TableCell>
|
||||
|
||||
{/* Coluna: Ações (menu suspenso) */}
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
{/* Botão principal do menu */}
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer">
|
||||
<EllipsisIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
{/* Conteúdo do menu suspenso */}
|
||||
<DropdownMenuContent side="left" align="start">
|
||||
<DropdownMenuGroup>
|
||||
{/* Ação: Editar cliente */}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => onEdit(client)}
|
||||
>
|
||||
<PencilIcon className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Ação: Ir para dashboard do cliente */}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => router.push(`/administrativo/clientes/${client.client_id}`)}
|
||||
>
|
||||
<ChartPie className="mr-2 h-4 w-4" />
|
||||
Dashboard
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
// Caso não existam registros
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||||
Nenhum cliente encontrado.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
)}
|
||||
</TableBody>
|
||||
|
||||
{/* Rodapé da tabela com totalização */}
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>
|
||||
{/* Se existir paginação, mostra o total de registros; senão, usa o tamanho da lista */}
|
||||
Total de clientes: {pagination?.total_records ?? data?.length}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
31
src/packages/administrativo/data/Log/LogWarningData.ts
Normal file
31
src/packages/administrativo/data/Log/LogWarningData.ts
Normal file
|
|
@ -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);
|
||||
23
src/packages/administrativo/hooks/Log/useLogWarningHook.ts
Normal file
23
src/packages/administrativo/hooks/Log/useLogWarningHook.ts
Normal file
|
|
@ -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<LogWarningInterface | null>(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 };
|
||||
};
|
||||
|
|
@ -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: []
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
70
src/shared/components/finance/FinanceStatusDialog.tsx
Normal file
70
src/shared/components/finance/FinanceStatusDialog.tsx
Normal file
|
|
@ -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 (
|
||||
<Dialog>
|
||||
{/* asChild permite que o Dialog clone o elemento e injete handlers */}
|
||||
<DialogTrigger asChild>
|
||||
{triggerElement ?? (
|
||||
<button
|
||||
type="button"
|
||||
className={isLate ? 'bg-red-200 text-red-800 px-3 py-1.5 rounded cursor-pointer' : 'bg-green-200 text-green-800 px-3 py-1.5 rounded cursor-pointer'}
|
||||
>
|
||||
{status?.toUpperCase()}
|
||||
{expired_count > 0 && <span className="ml-1">({expired_count})</span>}
|
||||
</button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isLate ? 'Pendências Financeiras' : 'Situação Financeira - Em Dia'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<p className="text-sm text-gray-600 mb-3">{message}</p>
|
||||
|
||||
<ScrollArea className="max-h-[300px] pr-2">
|
||||
{details.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm">Nenhum detalhe disponível.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{details.map((item, i) => (
|
||||
<div key={i} className={`p-3 rounded-lg border ${item.type === 'vencido' ? 'bg-red-50 border-red-300' : 'bg-yellow-50 border-yellow-300'}`}>
|
||||
<p className="text-sm font-medium">{item.description}</p>
|
||||
<p className="text-xs text-gray-600 mt-1">Referência: {format(new Date(item.reference), 'dd/MM/yyyy', { locale: ptBR })}</p>
|
||||
<p className="text-xs text-gray-600">{item.type === 'vencido' ? `${item.days} dia(s) de atraso` : `${Math.abs(item.days)} dia(s) restantes`}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
88
src/shared/utils/CheckLiberationHelper.ts
Normal file
88
src/shared/utils/CheckLiberationHelper.ts
Normal file
|
|
@ -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' };
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue