first commit
This commit is contained in:
commit
c9fb67e751
423 files changed
+53287
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import provasRoutes from './routes/provas';
|
||||
import calculosJudiciaisRoutes from './routes/calculosJudiciais';
|
||||
import indicesMonetariosRoutes from './routes/indicesMonetarios';
|
||||
import pensaoAlimenticiaRoutes from './routes/pensaoAlimenticia';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
app.use('/api/provas', provasRoutes);
|
||||
app.use('/api/calculos', calculosJudiciaisRoutes);
|
||||
app.use('/api/indices', indicesMonetariosRoutes);
|
||||
app.use('/api/pensao-alimenticia', pensaoAlimenticiaRoutes);
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
|
||||
export { app, prisma };
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: {
|
||||
id: string;
|
||||
email: string;
|
||||
perfil: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const extractUser = (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: "Token não fornecido" });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'default-secret') as jwt.JwtPayload;
|
||||
req.user = {
|
||||
id: decoded.id,
|
||||
email: decoded.email,
|
||||
perfil: decoded.perfil
|
||||
};
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: "Token inválido" });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient({
|
||||
log: process.env.PRISMA_LOG_LEVEL === 'debug'
|
||||
? ['query', 'info', 'warn', 'error']
|
||||
: ['error'],
|
||||
});
|
||||
|
||||
// Tratar eventos de desconexão
|
||||
prisma.$on('beforeExit', async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
export default prisma;
|
||||
+1403
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,275 @@
|
||||
// ...existing code...
|
||||
|
||||
// Modelos para o sistema de cálculo judicial
|
||||
model CalculoJudicial {
|
||||
id String @id @default(uuid())
|
||||
titulo String
|
||||
descricao String?
|
||||
dataBase DateTime
|
||||
dataCalculo DateTime @default(now())
|
||||
valorBase Decimal @db.Decimal(15, 2)
|
||||
valorCorrigido Decimal @db.Decimal(15, 2)
|
||||
valorFinal Decimal @db.Decimal(15, 2)
|
||||
status String @default("RASCUNHO") // RASCUNHO, FINALIZADO, ARQUIVADO
|
||||
usuarioId String?
|
||||
processoId String?
|
||||
clienteId String?
|
||||
|
||||
// Relações
|
||||
usuario Usuario? @relation(fields: [usuarioId], references: [id])
|
||||
processo Processo? @relation(fields: [processoId], references: [id])
|
||||
cliente Cliente? @relation(fields: [clienteId], references: [id])
|
||||
|
||||
// Componentes do cálculo
|
||||
correcoes CorrecaoMonetaria[]
|
||||
juros JuroCalculo[]
|
||||
multas MultaCalculo[]
|
||||
honorarios HonorarioCalculo[]
|
||||
parcelas ParcelaCalculo[]
|
||||
descontos DescontoCalculo[]
|
||||
pensaoAlimenticia PensaoAlimenticia?
|
||||
|
||||
// Metadados
|
||||
configuracoes Json?
|
||||
historico HistoricoCalculo[]
|
||||
exportacoes ExportacaoCalculo[]
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("calculosjudiciais")
|
||||
}
|
||||
|
||||
model CorrecaoMonetaria {
|
||||
id String @id @default(uuid())
|
||||
calculoId String
|
||||
indice String // INPC, IPCA-E, TR, SELIC etc.
|
||||
dataInicial DateTime
|
||||
dataFinal DateTime?
|
||||
valorOriginal Decimal @db.Decimal(15, 2)
|
||||
valorCorrigido Decimal @db.Decimal(15, 2)
|
||||
fatorCorrecao Decimal @db.Decimal(15, 6)
|
||||
|
||||
calculo CalculoJudicial @relation(fields: [calculoId], references: [id], onDelete: Cascade)
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("correcoes_monetarias")
|
||||
}
|
||||
|
||||
model JuroCalculo {
|
||||
id String @id @default(uuid())
|
||||
calculoId String
|
||||
tipo String // SIMPLES, COMPOSTO
|
||||
taxa Decimal @db.Decimal(8, 6) // Taxa percentual (ex: 0.01 para 1%)
|
||||
periodicidade String // MENSAL, ANUAL, DIARIO
|
||||
natureza String // MORATORIO, COMPENSATORIO, CONVENCIONAL, LEGAL
|
||||
dataInicial DateTime
|
||||
dataFinal DateTime?
|
||||
valorBase Decimal @db.Decimal(15, 2)
|
||||
valorJuros Decimal @db.Decimal(15, 2)
|
||||
|
||||
calculo CalculoJudicial @relation(fields: [calculoId], references: [id], onDelete: Cascade)
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("juros_calculos")
|
||||
}
|
||||
|
||||
model MultaCalculo {
|
||||
id String @id @default(uuid())
|
||||
calculoId String
|
||||
tipo String // CONTRATUAL, LEGAL
|
||||
percentual Decimal @db.Decimal(8, 6)
|
||||
valorBase Decimal @db.Decimal(15, 2)
|
||||
valorMulta Decimal @db.Decimal(15, 2)
|
||||
fundamento String? // Artigo de lei ou cláusula contratual
|
||||
|
||||
calculo CalculoJudicial @relation(fields: [calculoId], references: [id], onDelete: Cascade)
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("multas_calculos")
|
||||
}
|
||||
|
||||
model HonorarioCalculo {
|
||||
id String @id @default(uuid())
|
||||
calculoId String
|
||||
tipo String // CONTRATUAL, SUCUMBENCIAL, EXECUCAO
|
||||
percentual Decimal @db.Decimal(8, 6)
|
||||
valorBase Decimal @db.Decimal(15, 2)
|
||||
valorHonorario Decimal @db.Decimal(15, 2)
|
||||
fundamento String? // Fundamentação legal ou contratual
|
||||
|
||||
calculo CalculoJudicial @relation(fields: [calculoId], references: [id], onDelete: Cascade)
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("honorarios_calculos")
|
||||
}
|
||||
|
||||
model ParcelaCalculo {
|
||||
id String @id @default(uuid())
|
||||
calculoId String
|
||||
numero Int
|
||||
dataVencimento DateTime
|
||||
valorOriginal Decimal @db.Decimal(15, 2)
|
||||
valorCorrigido Decimal @db.Decimal(15, 2)
|
||||
valorJuros Decimal @db.Decimal(15, 2)
|
||||
valorMulta Decimal @db.Decimal(15, 2)
|
||||
valorTotal Decimal @db.Decimal(15, 2)
|
||||
pago Boolean @default(false)
|
||||
dataPagamento DateTime?
|
||||
|
||||
calculo CalculoJudicial @relation(fields: [calculoId], references: [id], onDelete: Cascade)
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("parcelas_calculos")
|
||||
}
|
||||
|
||||
model DescontoCalculo {
|
||||
id String @id @default(uuid())
|
||||
calculoId String
|
||||
tipo String // INSS, IRRF, CUSTAS, OUTRO
|
||||
descricao String?
|
||||
percentual Decimal? @db.Decimal(8, 6)
|
||||
valorFixo Decimal? @db.Decimal(15, 2)
|
||||
valorBase Decimal @db.Decimal(15, 2)
|
||||
valorDesconto Decimal @db.Decimal(15, 2)
|
||||
|
||||
calculo CalculoJudicial @relation(fields: [calculoId], references: [id], onDelete: Cascade)
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("descontos_calculos")
|
||||
}
|
||||
|
||||
model HistoricoCalculo {
|
||||
id String @id @default(uuid())
|
||||
calculoId String
|
||||
usuarioId String?
|
||||
acao String // CRIACAO, ATUALIZACAO, RECALCULO, EXCLUSAO
|
||||
descricao String
|
||||
valorAnterior Decimal? @db.Decimal(15, 2)
|
||||
valorNovo Decimal? @db.Decimal(15, 2)
|
||||
metadados Json?
|
||||
|
||||
calculo CalculoJudicial @relation(fields: [calculoId], references: [id], onDelete: Cascade)
|
||||
usuario Usuario? @relation(fields: [usuarioId], references: [id])
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
|
||||
@@map("historicos_calculos")
|
||||
}
|
||||
|
||||
model ExportacaoCalculo {
|
||||
id String @id @default(uuid())
|
||||
calculoId String
|
||||
usuarioId String?
|
||||
tipo String // PDF, EXCEL, WORD
|
||||
arquivo String // Caminho do arquivo gerado
|
||||
|
||||
calculo CalculoJudicial @relation(fields: [calculoId], references: [id], onDelete: Cascade)
|
||||
usuario Usuario? @relation(fields: [usuarioId], references: [id])
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
|
||||
@@map("exportacoes_calculos")
|
||||
}
|
||||
|
||||
model IndiceMonetario {
|
||||
id String @id @default(uuid())
|
||||
nome String // INPC, IPCA-E, TR, SELIC etc.
|
||||
mesReferencia Int // 1-12 (representando os meses do ano)
|
||||
anoReferencia Int
|
||||
valor Decimal @db.Decimal(15, 6)
|
||||
dataPublicacao DateTime?
|
||||
fonte String? // IBGE, BACEN, etc.
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@unique([nome, mesReferencia, anoReferencia])
|
||||
@@map("indices_monetarios")
|
||||
}
|
||||
|
||||
model PensaoAlimenticia {
|
||||
id String @id @default(uuid())
|
||||
calculoId String @unique
|
||||
percentualSalarioMinimo Decimal @db.Decimal(8, 6)
|
||||
multaPactuada Decimal? @db.Decimal(8, 6)
|
||||
tipoAtualizacao String @default("INPC") // Tipo de índice para atualização monetária
|
||||
dataInicial DateTime
|
||||
dataFinal DateTime?
|
||||
observacoes String?
|
||||
|
||||
calculo CalculoJudicial @relation(fields: [calculoId], references: [id], onDelete: Cascade)
|
||||
historicoSalarios HistoricoSalarioMinimo[]
|
||||
parcelasPensao ParcelaPensao[]
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("pensoes_alimenticias")
|
||||
}
|
||||
|
||||
model HistoricoSalarioMinimo {
|
||||
id String @id @default(uuid())
|
||||
pensaoId String
|
||||
dataVigencia DateTime
|
||||
valor Decimal @db.Decimal(15, 2)
|
||||
|
||||
pensao PensaoAlimenticia @relation(fields: [pensaoId], references: [id], onDelete: Cascade)
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("historico_salario_minimo")
|
||||
}
|
||||
|
||||
model ParcelaPensao {
|
||||
id String @id @default(uuid())
|
||||
pensaoId String
|
||||
competencia DateTime // Mês/ano a que se refere a parcela
|
||||
valorSalarioMinimo Decimal @db.Decimal(15, 2)
|
||||
percentual Decimal @db.Decimal(8, 6)
|
||||
valorOriginal Decimal @db.Decimal(15, 2)
|
||||
valorCorrigido Decimal @db.Decimal(15, 2)
|
||||
valorJuros Decimal @db.Decimal(15, 2)
|
||||
valorMulta Decimal @db.Decimal(15, 2)
|
||||
valorTotal Decimal @db.Decimal(15, 2)
|
||||
pago Boolean @default(false)
|
||||
dataPagamento DateTime?
|
||||
|
||||
pensao PensaoAlimenticia @relation(fields: [pensaoId], references: [id], onDelete: Cascade)
|
||||
|
||||
criadoEm DateTime @default(now())
|
||||
atualizadoEm DateTime @updatedAt
|
||||
|
||||
@@map("parcelas_pensao")
|
||||
}
|
||||
|
||||
// Atualizar relações existentes
|
||||
model Usuario {
|
||||
// ...existing code...
|
||||
calculos CalculoJudicial[]
|
||||
historicoCalculos HistoricoCalculo[]
|
||||
exportacoes ExportacaoCalculo[]
|
||||
}
|
||||
|
||||
model Processo {
|
||||
// ...existing code...
|
||||
calculos CalculoJudicial[]
|
||||
}
|
||||
|
||||
model Cliente {
|
||||
// ...existing code...
|
||||
calculos CalculoJudicial[]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
log: process.env.PRISMA_LOG_LEVEL === 'debug'
|
||||
? ['query', 'info', 'warn', 'error']
|
||||
: ['error'],
|
||||
});
|
||||
|
||||
export { prisma };
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { prisma } from '../prismaClient';
|
||||
import { authMiddleware } from '../../middleware/auth';
|
||||
const { NextCloudService } = require('../../services/nextcloud');
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Criar registro de anexo
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { nome, tamanho, url, tipo } = req.body;
|
||||
|
||||
const anexo = await prisma.anexo.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
nome,
|
||||
tipo,
|
||||
tamanho,
|
||||
url
|
||||
}
|
||||
});
|
||||
|
||||
res.json(anexo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar anexo:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar anexo' });
|
||||
}
|
||||
});
|
||||
|
||||
// Deletar anexo
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await prisma.anexo.delete({
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar anexo:', error);
|
||||
res.status(500).json({ error: 'Erro ao deletar anexo' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/upload', upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file || !req.body.clienteNome) {
|
||||
return res.status(400).json({ error: 'Arquivo e nome do cliente são obrigatórios' });
|
||||
}
|
||||
|
||||
// Preparar caminho do arquivo
|
||||
const pastaCliente = req.body.clienteNome.toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
const fileName = `${Date.now()}_${req.file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_')}`;
|
||||
const filePath = `${pastaCliente}/${fileName}`;
|
||||
|
||||
// Upload para o Nextcloud
|
||||
await NextCloudService.uploadFile(req.file.buffer, filePath);
|
||||
|
||||
res.json({ url: filePath });
|
||||
} catch (error) {
|
||||
console.error('Erro no upload:', error);
|
||||
res.status(500).json({ error: 'Erro no upload do arquivo' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../prismaClient';
|
||||
import { authMiddleware } from '../../middleware/auth';
|
||||
import multer from 'multer';
|
||||
import { NextCloudService } from '../../services/nextcloud';
|
||||
|
||||
const router = Router();
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// ...existing routes...
|
||||
|
||||
// Rota para atualizar atendimento com anexos
|
||||
router.patch('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { anexos, ...data } = req.body;
|
||||
|
||||
console.log('Atualizando atendimento:', id, { anexos, data }); // Debug log
|
||||
|
||||
const atendimento = await prisma.atendimento.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
...(anexos && {
|
||||
anexos: {
|
||||
createMany: {
|
||||
data: anexos.map((anexo: any) => ({
|
||||
nome: anexo.nome,
|
||||
url: anexo.url,
|
||||
tipo: anexo.tipo,
|
||||
tamanho: anexo.tamanho
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
include: {
|
||||
anexos: true,
|
||||
cliente: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Atendimento atualizado:', atendimento); // Debug log
|
||||
res.json(atendimento);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar atendimento:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar atendimento' });
|
||||
}
|
||||
});
|
||||
|
||||
// Nova rota para upload de arquivo via proxy
|
||||
router.post('/upload', upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file || !req.body.clienteNome) {
|
||||
return res.status(400).json({ error: 'Arquivo e nome do cliente são obrigatórios' });
|
||||
}
|
||||
|
||||
// Enviar para o NextCloud usando a estrutura de pastas correta
|
||||
const buffer = req.file.buffer;
|
||||
Object.defineProperty(buffer, 'name', {
|
||||
value: req.file.originalname,
|
||||
writable: true,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
const filePath = await NextCloudService.uploadFile(
|
||||
buffer,
|
||||
req.body.clienteNome,
|
||||
'atendimento',
|
||||
req.body.protocolo
|
||||
);
|
||||
|
||||
res.json({ url: filePath });
|
||||
} catch (error) {
|
||||
console.error('Erro no upload:', error);
|
||||
res.status(500).json({ error: 'Erro no upload do arquivo' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const atendimento = await prisma.atendimento.findUnique({
|
||||
where: {
|
||||
id: req.params.id
|
||||
},
|
||||
include: {
|
||||
cliente: true,
|
||||
processo: true,
|
||||
usuario: {
|
||||
select: {
|
||||
id: true,
|
||||
nome: true,
|
||||
oab: true,
|
||||
oabUF: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!atendimento) {
|
||||
return res.status(404).json({ error: 'Atendimento não encontrado' });
|
||||
}
|
||||
|
||||
res.json(atendimento);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar atendimento:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar atendimento' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,889 @@
|
||||
import express from 'express';
|
||||
import { CalculoJudicialService } from '../services/CalculoJudicialService';
|
||||
import { authMiddleware } from '../../middleware/auth';
|
||||
import { prisma } from '../prismaClient';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { getIndicesParaPeriodo, calcularFatorCorrecaoTotal, verificarIndicesDisponiveis } from '../services/IndiceMonetarioService';
|
||||
import { IndiceMonetarioService } from '../services/IndiceMonetarioService';
|
||||
import { ReportGenerator } from '../services/ReportGenerator';
|
||||
// Importar o módulo xlsx com verificação de disponibilidade
|
||||
let XLSX: any;
|
||||
try {
|
||||
XLSX = require('xlsx');
|
||||
} catch (error) {
|
||||
console.warn('Módulo XLSX não encontrado. Funcionalidade de exportação para Excel estará indisponível.');
|
||||
XLSX = null;
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
const calculoService = new CalculoJudicialService();
|
||||
const indiceService = new IndiceMonetarioService();
|
||||
const reportGenerator = new ReportGenerator();
|
||||
|
||||
// Aplicar middleware de autenticação em todas as rotas
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Função auxiliar para validação de campos obrigatórios
|
||||
const validarDados = (camposObrigatorios: string[]) => {
|
||||
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const camposFaltantes = camposObrigatorios.filter(campo => req.body[campo] === undefined);
|
||||
|
||||
if (camposFaltantes.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: `Campos obrigatórios faltando: ${camposFaltantes.join(', ')}`,
|
||||
camposFaltantes
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// Criar novo cálculo judicial
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
titulo,
|
||||
descricao,
|
||||
dataBase,
|
||||
valorBase,
|
||||
processoId,
|
||||
clienteId
|
||||
} = req.body;
|
||||
|
||||
// Verificar se o usuário está autenticado
|
||||
if (!req.user?.id) {
|
||||
return res.status(401).json({ error: 'Usuário não autenticado' });
|
||||
}
|
||||
|
||||
// Validar campos obrigatórios
|
||||
if (!titulo || !dataBase || valorBase === undefined) {
|
||||
return res.status(400).json({ error: 'Campos obrigatórios faltando' });
|
||||
}
|
||||
|
||||
const calculo = await calculoService.criarCalculo({
|
||||
titulo,
|
||||
descricao,
|
||||
dataBase: new Date(dataBase),
|
||||
valorBase: parseFloat(valorBase),
|
||||
processoId,
|
||||
clienteId,
|
||||
usuarioId: req.user.id
|
||||
});
|
||||
|
||||
res.status(201).json(calculo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar cálculo:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Buscar cálculo por ID
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const calculo = await calculoService.buscarCalculo(id);
|
||||
|
||||
if (!calculo) {
|
||||
return res.status(404).json({ error: 'Cálculo não encontrado' });
|
||||
}
|
||||
|
||||
res.json(calculo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar cálculo:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar correção monetária
|
||||
router.post('/:id/correcao', validarDados(['indice', 'dataInicial']), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
indice,
|
||||
dataInicial,
|
||||
dataFinal,
|
||||
valorOriginal
|
||||
} = req.body;
|
||||
|
||||
// Verificar se o usuário está autenticado
|
||||
if (!req.user?.id) {
|
||||
return res.status(401).json({ error: 'Usuário não autenticado' });
|
||||
}
|
||||
|
||||
const correcao = await calculoService.adicionarCorrecaoMonetaria({
|
||||
calculoId: id,
|
||||
indice,
|
||||
dataInicial: new Date(dataInicial),
|
||||
dataFinal: dataFinal ? new Date(dataFinal) : undefined,
|
||||
valorOriginal: parseFloat(valorOriginal),
|
||||
usuarioId: req.user.id
|
||||
});
|
||||
|
||||
res.status(201).json(correcao);
|
||||
} catch (error) {
|
||||
console.error('Erro ao adicionar correção monetária:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar juros
|
||||
router.post('/:id/juros', validarDados(['tipo', 'taxa', 'dataInicial']), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
tipo,
|
||||
taxa,
|
||||
periodicidade,
|
||||
natureza,
|
||||
dataInicial,
|
||||
dataFinal,
|
||||
valorBase
|
||||
} = req.body;
|
||||
|
||||
// Verificar se o usuário está autenticado
|
||||
if (!req.user?.id) {
|
||||
return res.status(401).json({ error: 'Usuário não autenticado' });
|
||||
}
|
||||
|
||||
const juros = await calculoService.adicionarJuros({
|
||||
calculoId: id,
|
||||
tipo,
|
||||
taxa: parseFloat(taxa),
|
||||
periodicidade,
|
||||
natureza,
|
||||
dataInicial: new Date(dataInicial),
|
||||
dataFinal: dataFinal ? new Date(dataFinal) : undefined,
|
||||
valorBase: parseFloat(valorBase),
|
||||
usuarioId: req.user.id
|
||||
});
|
||||
|
||||
res.status(201).json(juros);
|
||||
} catch (error) {
|
||||
console.error('Erro ao adicionar juros:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar multa
|
||||
router.post('/:id/multa', validarDados(['tipo', 'percentual', 'valorBase']), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
tipo,
|
||||
percentual,
|
||||
valorBase,
|
||||
fundamento
|
||||
} = req.body;
|
||||
|
||||
// Verificar se o usuário está autenticado
|
||||
if (!req.user?.id) {
|
||||
return res.status(401).json({ error: 'Usuário não autenticado' });
|
||||
}
|
||||
|
||||
const multa = await calculoService.adicionarMulta({
|
||||
calculoId: id,
|
||||
tipo,
|
||||
percentual: parseFloat(percentual),
|
||||
valorBase: parseFloat(valorBase),
|
||||
fundamento,
|
||||
usuarioId: req.user.id
|
||||
});
|
||||
|
||||
res.status(201).json(multa);
|
||||
} catch (error) {
|
||||
console.error('Erro ao adicionar multa:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Listar cálculos com filtros
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { titulo, status, processoId, clienteId } = req.query;
|
||||
|
||||
const calculos = await calculoService.listarCalculos({
|
||||
titulo: titulo as string,
|
||||
status: status as string,
|
||||
processoId: processoId as string,
|
||||
clienteId: clienteId as string
|
||||
});
|
||||
|
||||
res.json(calculos);
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar cálculos:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Finalizar cálculo
|
||||
router.post('/:id/finalizar', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Verificar se o usuário está autenticado
|
||||
if (!req.user?.id) {
|
||||
return res.status(401).json({ error: 'Usuário não autenticado' });
|
||||
}
|
||||
|
||||
const calculo = await calculoService.finalizarCalculo(id, req.user.id);
|
||||
res.json(calculo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao finalizar cálculo:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Gerar relatório do cálculo
|
||||
router.get('/:id/relatorio', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Verificar se o usuário está autenticado - Já feito pelo middleware
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Usuário não autenticado' });
|
||||
}
|
||||
|
||||
// Buscar dados do cálculo
|
||||
const calculo = await prisma.calculoJudicial.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
usuario: true,
|
||||
cliente: true,
|
||||
processo: true,
|
||||
correcoes: true,
|
||||
juros: true,
|
||||
multas: true,
|
||||
honorarios: true,
|
||||
parcelas: true,
|
||||
descontos: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!calculo) {
|
||||
return res.status(404).json({ error: 'Cálculo não encontrado' });
|
||||
}
|
||||
|
||||
// Gerar relatório PDF
|
||||
const pdfBuffer = await reportGenerator.gerarRelatorioPDF(calculo);
|
||||
|
||||
// Registrar exportação
|
||||
await prisma.exportacaoCalculo.create({
|
||||
data: {
|
||||
calculoId: id,
|
||||
usuarioId: req.user.id,
|
||||
tipo: 'PDF',
|
||||
arquivo: `relatorio_${id}_${new Date().toISOString()}.pdf`
|
||||
}
|
||||
});
|
||||
|
||||
// Enviar PDF como resposta
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="calculo_${id}.pdf"`);
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar relatório:', error);
|
||||
res.status(500).json({ error: 'Erro ao gerar relatório' });
|
||||
}
|
||||
});
|
||||
|
||||
// Exportar cálculo para Excel
|
||||
router.get('/:id/excel', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Verificar se o módulo XLSX está disponível
|
||||
if (!XLSX) {
|
||||
return res.status(500).json({
|
||||
error: 'Funcionalidade de exportação para Excel não está disponível. Módulo XLSX não instalado.'
|
||||
});
|
||||
}
|
||||
|
||||
// Verificar se o usuário está autenticado - Já feito pelo middleware
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Usuário não autenticado' });
|
||||
}
|
||||
|
||||
// Buscar dados do cálculo
|
||||
const calculo = await prisma.calculoJudicial.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
usuario: true,
|
||||
cliente: true,
|
||||
processo: true,
|
||||
correcoes: true,
|
||||
juros: true,
|
||||
multas: true,
|
||||
honorarios: true,
|
||||
parcelas: true,
|
||||
descontos: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!calculo) {
|
||||
return res.status(404).json({ error: 'Cálculo não encontrado' });
|
||||
}
|
||||
|
||||
// Criar planilha Excel
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
// Dados resumo
|
||||
const dadosResumo = [
|
||||
['RESUMO DO CÁLCULO'],
|
||||
['Título', calculo.titulo],
|
||||
['Data Base', calculo.dataBase.toLocaleDateString('pt-BR')],
|
||||
['Data do Cálculo', calculo.dataCalculo.toLocaleDateString('pt-BR')],
|
||||
['Valor Base', calculo.valorBase.toString()],
|
||||
['Valor Corrigido', calculo.valorCorrigido.toString()],
|
||||
['Valor Final', calculo.valorFinal.toString()],
|
||||
['Status', calculo.status],
|
||||
];
|
||||
|
||||
// Adicionar planilha de resumo
|
||||
const resumoSheet = XLSX.utils.aoa_to_sheet(dadosResumo);
|
||||
XLSX.utils.book_append_sheet(workbook, resumoSheet, 'Resumo');
|
||||
|
||||
// Adicionar outras abas para cada componente do cálculo
|
||||
if (calculo.correcoes.length > 0) {
|
||||
const correcoesData = [
|
||||
['Índice', 'Data Inicial', 'Data Final', 'Valor Original', 'Valor Corrigido', 'Fator']
|
||||
];
|
||||
|
||||
calculo.correcoes.forEach(c => {
|
||||
correcoesData.push([
|
||||
c.indice,
|
||||
c.dataInicial.toLocaleDateString('pt-BR'),
|
||||
c.dataFinal?.toLocaleDateString('pt-BR') || '',
|
||||
c.valorOriginal.toString(),
|
||||
c.valorCorrigido.toString(),
|
||||
c.fatorCorrecao.toString()
|
||||
]);
|
||||
});
|
||||
|
||||
const correcoesSheet = XLSX.utils.aoa_to_sheet(correcoesData);
|
||||
XLSX.utils.book_append_sheet(workbook, correcoesSheet, 'Correções');
|
||||
}
|
||||
|
||||
// Adicionar outras abas (juros, multas, etc.)
|
||||
// ...
|
||||
|
||||
// Registrar exportação
|
||||
await prisma.exportacaoCalculo.create({
|
||||
data: {
|
||||
calculoId: id,
|
||||
usuarioId: req.user.id,
|
||||
tipo: 'EXCEL',
|
||||
arquivo: `calculo_${id}_${new Date().toISOString()}.xlsx`
|
||||
}
|
||||
});
|
||||
|
||||
// Enviar arquivo Excel
|
||||
const excelBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="calculo_${id}.xlsx"`);
|
||||
res.send(excelBuffer);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao exportar Excel:', error);
|
||||
res.status(500).json({ error: 'Erro ao exportar Excel' });
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar rota para correções monetárias
|
||||
router.post('/:id/correcoes', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { indice, dataInicial, dataFinal, valorOriginal } = req.body;
|
||||
|
||||
console.log('Dados recebidos:', { dataInicial, dataFinal, valorOriginal });
|
||||
|
||||
// Buscar índices para o período
|
||||
let indices = await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: indice,
|
||||
OR: [
|
||||
{
|
||||
anoReferencia: new Date(dataInicial).getFullYear(),
|
||||
mesReferencia: new Date(dataInicial).getMonth() + 1
|
||||
},
|
||||
{
|
||||
anoReferencia: {
|
||||
gt: new Date(dataInicial).getFullYear(),
|
||||
lt: new Date(dataFinal).getFullYear()
|
||||
}
|
||||
},
|
||||
{
|
||||
anoReferencia: new Date(dataFinal).getFullYear(),
|
||||
mesReferencia: {
|
||||
lte: new Date(dataFinal).getMonth() + 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' }
|
||||
]
|
||||
});
|
||||
|
||||
console.log(`Encontrados ${indices.length} índices`);
|
||||
|
||||
// Se não houver índices, buscar da API
|
||||
if (indices.length === 0) {
|
||||
console.log('Buscando índices da API do IBGE...');
|
||||
await indiceService.importarIndicesINPC();
|
||||
|
||||
// Buscar novamente após importação
|
||||
indices = await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: indice,
|
||||
OR: [
|
||||
{
|
||||
anoReferencia: new Date(dataInicial).getFullYear(),
|
||||
mesReferencia: new Date(dataInicial).getMonth() + 1
|
||||
},
|
||||
{
|
||||
anoReferencia: {
|
||||
gt: new Date(dataInicial).getFullYear(),
|
||||
lt: new Date(dataFinal).getFullYear()
|
||||
}
|
||||
},
|
||||
{
|
||||
anoReferencia: new Date(dataFinal).getFullYear(),
|
||||
mesReferencia: {
|
||||
lte: new Date(dataFinal).getMonth() + 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Criar a correção monetária
|
||||
const correcao = await prisma.correcaoMonetaria.create({
|
||||
data: {
|
||||
calculoId: id,
|
||||
indice,
|
||||
dataInicial: new Date(dataInicial),
|
||||
dataFinal: new Date(dataFinal),
|
||||
valorOriginal: new Decimal(valorOriginal),
|
||||
valorCorrigido: new Decimal(0), // Valor temporário
|
||||
fatorCorrecao: new Decimal(1), // Valor temporário
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar valores da correção em background
|
||||
atualizarValoresCorrecao(correcao.id).catch(console.error);
|
||||
|
||||
// Retornar resposta imediata
|
||||
res.status(201).json(correcao);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar correção:', error);
|
||||
res.status(500).json({
|
||||
error: 'Erro ao criar correção monetária',
|
||||
details: error instanceof Error ? error.message : 'Erro desconhecido'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Função auxiliar para atualizar valores em background
|
||||
async function atualizarValoresCorrecao(correcaoId: string) {
|
||||
try {
|
||||
const correcao = await prisma.correcaoMonetaria.findUnique({
|
||||
where: { id: correcaoId }
|
||||
});
|
||||
|
||||
if (!correcao) return;
|
||||
|
||||
// Buscar índices para o período
|
||||
const indices = await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: correcao.indice,
|
||||
OR: [
|
||||
{
|
||||
anoReferencia: correcao.dataInicial.getFullYear(),
|
||||
mesReferencia: correcao.dataInicial.getMonth() + 1
|
||||
},
|
||||
{
|
||||
anoReferencia: {
|
||||
gt: correcao.dataInicial.getFullYear(),
|
||||
lt: correcao.dataFinal.getFullYear()
|
||||
}
|
||||
},
|
||||
{
|
||||
anoReferencia: correcao.dataFinal.getFullYear(),
|
||||
mesReferencia: {
|
||||
lte: correcao.dataFinal.getMonth() + 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' }
|
||||
]
|
||||
});
|
||||
|
||||
// Calcular fator acumulado
|
||||
let fatorAcumulado = new Decimal(1);
|
||||
|
||||
for (const indice of indices) {
|
||||
// Converter o índice para fator decimal (ex: 1.5% -> 1.015)
|
||||
const fator = new Decimal(1).plus(new Decimal(indice.valor).div(100));
|
||||
fatorAcumulado = fatorAcumulado.mul(fator);
|
||||
}
|
||||
|
||||
// Calcular valor corrigido
|
||||
const valorCorrigido = new Decimal(correcao.valorOriginal).mul(fatorAcumulado);
|
||||
|
||||
// Atualizar correção
|
||||
await prisma.correcaoMonetaria.update({
|
||||
where: { id: correcaoId },
|
||||
data: {
|
||||
valorCorrigido,
|
||||
fatorCorrecao: fatorAcumulado
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar valores da correção:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Adicionar rota DELETE para correções monetárias
|
||||
router.delete('/:calculoId/correcoes/:correcaoId', async (req, res) => {
|
||||
try {
|
||||
const { calculoId, correcaoId } = req.params;
|
||||
|
||||
// Verificar se a correção existe e pertence ao cálculo
|
||||
const correcao = await prisma.correcaoMonetaria.findFirst({
|
||||
where: {
|
||||
id: correcaoId,
|
||||
calculoId: calculoId
|
||||
}
|
||||
});
|
||||
|
||||
if (!correcao) {
|
||||
return res.status(404).json({ error: 'Correção monetária não encontrada' });
|
||||
}
|
||||
|
||||
// Excluir a correção
|
||||
await prisma.correcaoMonetaria.delete({
|
||||
where: { id: correcaoId }
|
||||
});
|
||||
|
||||
// Atualizar o valor corrigido e final do cálculo
|
||||
await prisma.calculoJudicial.update({
|
||||
where: { id: calculoId },
|
||||
data: {
|
||||
valorCorrigido: {
|
||||
decrement: correcao.valorCorrigido
|
||||
},
|
||||
valorFinal: {
|
||||
decrement: correcao.valorCorrigido
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir correção monetária:', error);
|
||||
res.status(500).json({
|
||||
error: 'Erro ao excluir correção monetária',
|
||||
details: error instanceof Error ? error.message : 'Erro desconhecido'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar rota para criar multa
|
||||
router.post('/:id/multas', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { tipo, percentual, valorBase, fundamento } = req.body;
|
||||
|
||||
// Validar entrada
|
||||
if (!tipo || !percentual || !valorBase) {
|
||||
return res.status(400).json({ error: 'Dados incompletos' });
|
||||
}
|
||||
|
||||
// Verificar se o cálculo existe
|
||||
const calculo = await prisma.calculoJudicial.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!calculo) {
|
||||
return res.status(404).json({ error: 'Cálculo não encontrado' });
|
||||
}
|
||||
|
||||
// Criar multa
|
||||
const multa = await prisma.multaCalculo.create({
|
||||
data: {
|
||||
calculoId: id,
|
||||
tipo,
|
||||
percentual: parseFloat(percentual),
|
||||
valorBase: parseFloat(valorBase),
|
||||
valorMulta: parseFloat(valorBase) * parseFloat(percentual),
|
||||
fundamento
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(multa);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar multa:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar multa' });
|
||||
}
|
||||
});
|
||||
|
||||
// Rota para adicionar parcelas
|
||||
router.post('/:id/parcelas', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { numero, dataVencimento, valorOriginal } = req.body;
|
||||
|
||||
// Validar dados
|
||||
if (!numero || !dataVencimento || !valorOriginal) {
|
||||
return res.status(400).json({ error: 'Dados incompletos' });
|
||||
}
|
||||
|
||||
const parcela = await prisma.parcelaCalculo.create({
|
||||
data: {
|
||||
calculoId: id,
|
||||
numero: parseInt(numero),
|
||||
dataVencimento: new Date(dataVencimento),
|
||||
valorOriginal: new Decimal(valorOriginal),
|
||||
valorCorrigido: new Decimal(valorOriginal),
|
||||
valorJuros: new Decimal(0),
|
||||
valorMulta: new Decimal(0),
|
||||
valorTotal: new Decimal(valorOriginal),
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(parcela);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar parcela:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar parcela' });
|
||||
}
|
||||
});
|
||||
|
||||
// Rota para adicionar honorários
|
||||
router.post('/:id/honorarios', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { tipo, percentual, valorBase, fundamento } = req.body;
|
||||
|
||||
const honorario = await prisma.honorarioCalculo.create({
|
||||
data: {
|
||||
calculoId: id,
|
||||
tipo,
|
||||
percentual: new Decimal(percentual),
|
||||
valorBase: new Decimal(valorBase),
|
||||
valorHonorario: new Decimal(valorBase).mul(percentual),
|
||||
fundamento
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(honorario);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar honorário:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar honorário' });
|
||||
}
|
||||
});
|
||||
|
||||
// Rota para adicionar descontos
|
||||
router.post('/:id/descontos', async (req, res) => {
|
||||
try {
|
||||
const calculoId = req.params.id;
|
||||
const { tipo, descricao, percentual, valorFixo, valorBase } = req.body;
|
||||
|
||||
// Validar dados
|
||||
if (!tipo || (!percentual && !valorFixo) || !valorBase) {
|
||||
return res.status(400).json({
|
||||
error: 'Dados inválidos. Forneça o tipo, valor base e percentual ou valor fixo.'
|
||||
});
|
||||
}
|
||||
|
||||
// Calcular valor do desconto
|
||||
let valorDesconto;
|
||||
if (percentual) {
|
||||
// Certifique-se de que percentual é um número entre 0 e 1
|
||||
const percentualDecimal = Number(percentual);
|
||||
if (isNaN(percentualDecimal) || percentualDecimal <= 0 || percentualDecimal > 1) {
|
||||
return res.status(400).json({
|
||||
error: 'Percentual deve ser um número maior que 0 e no máximo 1 (100%)'
|
||||
});
|
||||
}
|
||||
valorDesconto = new Decimal(valorBase).mul(percentualDecimal);
|
||||
} else {
|
||||
valorDesconto = new Decimal(valorFixo);
|
||||
}
|
||||
|
||||
// Criar desconto
|
||||
const desconto = await prisma.descontoCalculo.create({
|
||||
data: {
|
||||
calculoId,
|
||||
tipo,
|
||||
descricao,
|
||||
percentual: percentual ? new Decimal(percentual) : null,
|
||||
valorFixo: valorFixo ? new Decimal(valorFixo) : null,
|
||||
valorBase: new Decimal(valorBase),
|
||||
valorDesconto
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar valor do cálculo
|
||||
await atualizarValorCalculoComDescontos(calculoId);
|
||||
|
||||
res.status(201).json(desconto);
|
||||
} catch (error) {
|
||||
console.error('Erro ao adicionar desconto:', error);
|
||||
res.status(500).json({ error: 'Erro ao adicionar desconto' });
|
||||
}
|
||||
});
|
||||
|
||||
// Função para atualizar valor do cálculo com descontos
|
||||
async function atualizarValorCalculoComDescontos(calculoId: string) {
|
||||
try {
|
||||
// Buscar cálculo
|
||||
const calculo = await prisma.calculoJudicial.findUnique({
|
||||
where: { id: calculoId },
|
||||
include: {
|
||||
descontos: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!calculo) return;
|
||||
|
||||
// Calcular total de descontos
|
||||
const totalDescontos = calculo.descontos.reduce(
|
||||
(total, desconto) => total.add(desconto.valorDesconto),
|
||||
new Decimal(0)
|
||||
);
|
||||
|
||||
// Atualizar valor final do cálculo
|
||||
// Valor final = Valor corrigido - Total de descontos
|
||||
const valorFinal = calculo.valorCorrigido.minus(totalDescontos);
|
||||
|
||||
// Atualizar cálculo
|
||||
await prisma.calculoJudicial.update({
|
||||
where: { id: calculoId },
|
||||
data: {
|
||||
valorFinal
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar valor do cálculo com descontos:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Adicionar rota para excluir desconto
|
||||
router.delete('/:id/descontos/:descontoId', async (req, res) => {
|
||||
try {
|
||||
const { id, descontoId } = req.params;
|
||||
|
||||
// Verificar se o cálculo existe
|
||||
const calculo = await prisma.calculoJudicial.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!calculo) {
|
||||
return res.status(404).json({ error: 'Cálculo não encontrado' });
|
||||
}
|
||||
|
||||
// Verificar se o desconto existe e pertence ao cálculo
|
||||
const desconto = await prisma.descontoCalculo.findFirst({
|
||||
where: {
|
||||
id: descontoId,
|
||||
calculoId: id
|
||||
}
|
||||
});
|
||||
|
||||
if (!desconto) {
|
||||
return res.status(404).json({ error: 'Desconto não encontrado ou não pertence a este cálculo' });
|
||||
}
|
||||
|
||||
// Excluir o desconto
|
||||
await prisma.descontoCalculo.delete({
|
||||
where: { id: descontoId }
|
||||
});
|
||||
|
||||
// Atualizar valor final do cálculo
|
||||
await atualizarValorCalculo(id);
|
||||
|
||||
// Registrar no histórico
|
||||
await prisma.historicoCalculo.create({
|
||||
data: {
|
||||
calculoId: id,
|
||||
acao: 'EXCLUSAO',
|
||||
descricao: `Desconto removido: ${desconto.tipo} - ${desconto.valorDesconto}`,
|
||||
valorAnterior: desconto.valorDesconto,
|
||||
valorNovo: null
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({ message: 'Desconto excluído com sucesso' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir desconto:', error);
|
||||
res.status(500).json({ error: 'Erro ao excluir desconto' });
|
||||
}
|
||||
});
|
||||
|
||||
// Função auxiliar para atualizar o valor do cálculo
|
||||
async function atualizarValorCalculo(calculoId: string) {
|
||||
try {
|
||||
// Buscar o cálculo com todos os componentes
|
||||
const calculo = await prisma.calculoJudicial.findUnique({
|
||||
where: { id: calculoId },
|
||||
include: {
|
||||
correcoes: true,
|
||||
juros: true,
|
||||
multas: true,
|
||||
honorarios: true,
|
||||
descontos: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!calculo) return;
|
||||
|
||||
// Calcular o valor corrigido (base + correções)
|
||||
let valorCorrigido = new Decimal(calculo.valorBase);
|
||||
|
||||
// Adicionar juros e multas
|
||||
for (const juro of calculo.juros) {
|
||||
valorCorrigido = valorCorrigido.add(juro.valorJuros);
|
||||
}
|
||||
|
||||
for (const multa of calculo.multas) {
|
||||
valorCorrigido = valorCorrigido.add(multa.valorMulta);
|
||||
}
|
||||
|
||||
for (const honorario of calculo.honorarios) {
|
||||
valorCorrigido = valorCorrigido.add(honorario.valorHonorario);
|
||||
}
|
||||
|
||||
// Calcular valor final (valor corrigido - descontos)
|
||||
let valorFinal = valorCorrigido;
|
||||
for (const desconto of calculo.descontos) {
|
||||
valorFinal = valorFinal.sub(desconto.valorDesconto);
|
||||
}
|
||||
|
||||
// Atualizar o cálculo
|
||||
await prisma.calculoJudicial.update({
|
||||
where: { id: calculoId },
|
||||
data: {
|
||||
valorCorrigido,
|
||||
valorFinal
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar valor do cálculo:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../prismaClient';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { search } = req.query;
|
||||
const where: any = {};
|
||||
|
||||
if (search) {
|
||||
const searchTerms = String(search).split(' ');
|
||||
where.OR = searchTerms.map(term => ({
|
||||
OR: [
|
||||
{ nome: { contains: term, mode: 'insensitive' } },
|
||||
{ cpfCnpj: { contains: term } },
|
||||
{ email: { contains: term.toLowerCase() } }
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
const clientes = await prisma.cliente.findMany({
|
||||
where,
|
||||
orderBy: {
|
||||
nome: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
res.json(clientes);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao buscar clientes' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,262 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../prismaClient';
|
||||
import { authMiddleware } from '../../middleware/auth'; // Corrigir caminho da importação
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Adicionar middleware de auth em todas as rotas
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Estatísticas de processos
|
||||
router.get('/processos/stats', async (req, res) => {
|
||||
try {
|
||||
const [total, porTribunal, porStatus] = await Promise.all([
|
||||
prisma.processo.count(),
|
||||
prisma.processo.groupBy({
|
||||
by: ['tribunal'],
|
||||
_count: true,
|
||||
}),
|
||||
prisma.processo.groupBy({
|
||||
by: ['status'],
|
||||
_count: true,
|
||||
})
|
||||
]);
|
||||
|
||||
res.json({
|
||||
total,
|
||||
porTribunal,
|
||||
porStatus
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao buscar estatísticas de processos' });
|
||||
}
|
||||
});
|
||||
|
||||
// Taxa de sucesso
|
||||
router.get('/processos/taxa-sucesso', async (req, res) => {
|
||||
try {
|
||||
const stats = await prisma.processo.groupBy({
|
||||
by: ['resultado'],
|
||||
_count: true,
|
||||
});
|
||||
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao buscar taxa de sucesso' });
|
||||
}
|
||||
});
|
||||
|
||||
// Estatísticas financeiras
|
||||
router.get('/financeiro/stats', async (req, res) => {
|
||||
try {
|
||||
const stats = await prisma.$transaction(async (tx) => {
|
||||
const [honorarios, custas] = await Promise.all([
|
||||
tx.honorario.aggregate({
|
||||
_sum: { valorTotalCausa: true }
|
||||
}),
|
||||
tx.custaProcessual.aggregate({
|
||||
_sum: { valor: true }
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
honorarios: honorarios._sum.valorTotalCausa || 0,
|
||||
custas: custas._sum.valor || 0
|
||||
};
|
||||
});
|
||||
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao buscar estatísticas financeiras' });
|
||||
}
|
||||
});
|
||||
|
||||
// Prazos próximos
|
||||
router.get('/prazos/proximos', async (req, res) => {
|
||||
try {
|
||||
const prazos = await prisma.diligencia.findMany({
|
||||
where: {
|
||||
dataLimite: {
|
||||
gte: new Date(),
|
||||
lte: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000) // próximos 15 dias
|
||||
}
|
||||
},
|
||||
include: {
|
||||
processo: true
|
||||
},
|
||||
orderBy: {
|
||||
dataLimite: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
res.json(prazos);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao buscar prazos próximos' });
|
||||
}
|
||||
});
|
||||
|
||||
// Corrigir rota de atividades
|
||||
router.get('/atividades', async (req, res) => {
|
||||
try {
|
||||
const atividades = await prisma.$transaction(async (tx) => {
|
||||
const [processos, movimentacoes] = await Promise.all([
|
||||
tx.processo.findMany({
|
||||
take: 10,
|
||||
orderBy: { dataAtualizacao: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
numero: true,
|
||||
dataAtualizacao: true,
|
||||
cliente: {
|
||||
select: {
|
||||
nome: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
tx.movimentacao.findMany({
|
||||
take: 10,
|
||||
orderBy: { data: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
descricao: true,
|
||||
data: true,
|
||||
processo: {
|
||||
select: {
|
||||
numero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
const atividadesProcessadas = [
|
||||
...processos.map(p => ({
|
||||
id: p.id,
|
||||
tipo: 'PROCESSO',
|
||||
descricao: `Processo ${p.numero} atualizado`,
|
||||
data: p.dataAtualizacao,
|
||||
detalhes: p.cliente.nome
|
||||
})),
|
||||
...movimentacoes.map(m => ({
|
||||
id: m.id,
|
||||
tipo: 'MOVIMENTACAO',
|
||||
descricao: m.descricao,
|
||||
data: m.data,
|
||||
detalhes: m.processo.numero
|
||||
}))
|
||||
].sort((a, b) => b.data.getTime() - a.data.getTime())
|
||||
.slice(0, 10);
|
||||
|
||||
return atividadesProcessadas;
|
||||
});
|
||||
|
||||
res.json(atividades);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar atividades:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar atividades recentes' });
|
||||
}
|
||||
});
|
||||
|
||||
// Corrigir rota de estatísticas de tarefas
|
||||
router.get('/tarefas/stats', async (req, res) => {
|
||||
try {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const [pendentes, atrasadas, concluidas] = await Promise.all([
|
||||
prisma.tarefa.count({
|
||||
where: {
|
||||
status: 'PENDENTE',
|
||||
dataVencimento: {
|
||||
gte: today
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.tarefa.count({
|
||||
where: {
|
||||
status: 'PENDENTE',
|
||||
dataVencimento: {
|
||||
lt: today
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.tarefa.count({
|
||||
where: {
|
||||
status: 'CONCLUIDA',
|
||||
atualizadoEm: { // Usar atualizadoEm enquanto não tiver dataConclusao
|
||||
gte: new Date(today.getFullYear(), today.getMonth(), 1),
|
||||
lt: new Date(today.getFullYear(), today.getMonth() + 1, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
]).catch(error => {
|
||||
console.error('Erro ao consultar tarefas:', error);
|
||||
return [0, 0, 0];
|
||||
});
|
||||
|
||||
const total = pendentes + atrasadas + concluidas;
|
||||
|
||||
res.json({
|
||||
pendentes,
|
||||
atrasadas,
|
||||
concluidas,
|
||||
total,
|
||||
taxaConclusao: total > 0 ? Math.round((concluidas / total) * 100) : 0,
|
||||
porStatus: { PENDENTE: pendentes }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar estatísticas de tarefas:', error);
|
||||
res.status(500).json({
|
||||
error: 'Erro ao buscar estatísticas de tarefas',
|
||||
details: error instanceof Error ? error.message : 'Erro desconhecido'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Estatísticas de atendimentos
|
||||
router.get('/atendimentos/stats', async (req, res) => {
|
||||
try {
|
||||
const hoje = new Date();
|
||||
const inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1);
|
||||
const fimMes = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0);
|
||||
const proximaSemana = new Date(hoje.setDate(hoje.getDate() + 7));
|
||||
|
||||
const [atendimentosHoje, proximosAtendimentos, totalMes] = await Promise.all([
|
||||
prisma.atendimento.count({
|
||||
where: {
|
||||
dataAtendimento: {
|
||||
gte: new Date(new Date().setHours(0,0,0,0)),
|
||||
lt: new Date(new Date().setHours(23,59,59,999))
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.atendimento.count({
|
||||
where: {
|
||||
dataAtendimento: {
|
||||
gte: new Date(),
|
||||
lt: proximaSemana
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.atendimento.count({
|
||||
where: {
|
||||
dataAtendimento: {
|
||||
gte: inicioMes,
|
||||
lte: fimMes
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
res.json({
|
||||
hoje: atendimentosHoje,
|
||||
proximaSemana: proximosAtendimentos,
|
||||
totalMes: totalMes
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao buscar estatísticas de atendimentos' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../prismaClient';
|
||||
import { authMiddleware } from '../../middleware/auth';
|
||||
import { TipoDocumento } from '@prisma/client';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Listar modelos de documentos
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { tipo } = req.query;
|
||||
|
||||
const where = tipo ? { tipo: tipo as TipoDocumento } : {};
|
||||
|
||||
const modelos = await prisma.documentoModelo.findMany({
|
||||
where,
|
||||
orderBy: {
|
||||
criadoEm: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
res.json(modelos);
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar modelos:', error);
|
||||
res.status(500).json({ error: 'Erro ao listar modelos de documentos' });
|
||||
}
|
||||
});
|
||||
|
||||
// Criar novo modelo
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { nome, tipo, descricao, conteudo, variaveis } = req.body;
|
||||
|
||||
if (!nome || !tipo || !conteudo) {
|
||||
return res.status(400).json({ error: 'Campos obrigatórios faltando' });
|
||||
}
|
||||
|
||||
const modelo = await prisma.documentoModelo.create({
|
||||
data: {
|
||||
nome,
|
||||
tipo: tipo as TipoDocumento,
|
||||
descricao,
|
||||
conteudo,
|
||||
variaveis: Array.isArray(variaveis) ? variaveis : []
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(modelo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar modelo:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar modelo de documento' });
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar modelo
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = req.body;
|
||||
|
||||
// Remover o id do objeto de dados para evitar conflito
|
||||
delete data.id;
|
||||
|
||||
const modelo = await prisma.documentoModelo.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nome: data.nome,
|
||||
tipo: data.tipo,
|
||||
descricao: data.descricao,
|
||||
conteudo: data.conteudo,
|
||||
variaveis: Array.isArray(data.variaveis) ? data.variaveis : []
|
||||
}
|
||||
});
|
||||
|
||||
res.json(modelo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar modelo:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar modelo de documento' });
|
||||
}
|
||||
});
|
||||
|
||||
// Excluir modelo
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await prisma.documentoModelo.delete({
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir modelo:', error);
|
||||
res.status(500).json({ error: 'Erro ao excluir modelo de documento' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,411 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../prismaClient';
|
||||
import { authMiddleware } from '../../middleware/auth';
|
||||
import { TipoDocumento } from '@prisma/client';
|
||||
import { PdfService } from '../services/PdfService';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Listar documentos
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { processoId, atendimentoId, tipo } = req.query;
|
||||
|
||||
const where: any = {};
|
||||
if (processoId) where.processoId = processoId;
|
||||
if (atendimentoId) where.atendimentoId = atendimentoId;
|
||||
if (tipo) where.tipo = tipo as TipoDocumento;
|
||||
|
||||
const documentos = await prisma.documento.findMany({
|
||||
where,
|
||||
include: {
|
||||
criadoPor: {
|
||||
select: {
|
||||
id: true,
|
||||
nome: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
criadoEm: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
res.json(documentos);
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar documentos:', error);
|
||||
res.status(500).json({ error: 'Erro ao listar documentos' });
|
||||
}
|
||||
});
|
||||
|
||||
// Criar documento
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { nome, tipo, conteudo, processoId, atendimentoId } = req.body;
|
||||
|
||||
if (!req.user?.id) {
|
||||
return res.status(401).json({ error: 'Usuário não autenticado' });
|
||||
}
|
||||
|
||||
const documento = await prisma.documento.create({
|
||||
data: {
|
||||
nome,
|
||||
tipo,
|
||||
conteudo,
|
||||
processoId,
|
||||
atendimentoId,
|
||||
criadoPorId: req.user.id
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(documento);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar documento:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar documento' });
|
||||
}
|
||||
});
|
||||
|
||||
// Criar documento a partir de modelo
|
||||
router.post('/from-modelo/:modeloId', async (req, res) => {
|
||||
try {
|
||||
const { modeloId } = req.params;
|
||||
const { processoId, atendimentoId, variaveis } = req.body;
|
||||
|
||||
if (!req.user?.id) {
|
||||
return res.status(401).json({ error: 'Usuário não autenticado' });
|
||||
}
|
||||
|
||||
// Buscar modelo
|
||||
const modelo = await prisma.documentoModelo.findUnique({
|
||||
where: { id: modeloId }
|
||||
});
|
||||
|
||||
if (!modelo) {
|
||||
return res.status(404).json({ error: 'Modelo não encontrado' });
|
||||
}
|
||||
|
||||
// Buscar dados adicionais se necessário
|
||||
let clienteData;
|
||||
if (variaveis?.cliente?.id) {
|
||||
const cliente = await prisma.cliente.findUnique({
|
||||
where: { id: variaveis.cliente.id }
|
||||
});
|
||||
if (cliente) {
|
||||
clienteData = cliente;
|
||||
}
|
||||
}
|
||||
|
||||
// Substituir variáveis no conteúdo
|
||||
let conteudo = modelo.conteudo;
|
||||
if (clienteData) {
|
||||
const substituicoes = {
|
||||
'cliente.nome': clienteData.nome,
|
||||
'cliente.cpf': clienteData.cpfCnpj,
|
||||
'cliente.rg': clienteData.rg,
|
||||
'cliente.estadoCivil': clienteData.estadoCivil,
|
||||
'cliente.profissao': clienteData.profissao,
|
||||
'cliente.enderecoCompleto': clienteData.enderecoCompleto,
|
||||
'data.atualExtenso': new Date().toLocaleDateString('pt-BR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
};
|
||||
|
||||
Object.entries(substituicoes).forEach(([key, value]) => {
|
||||
const regex = new RegExp(`{{${key}}}`, 'g');
|
||||
conteudo = conteudo.replace(regex, value || '');
|
||||
});
|
||||
}
|
||||
|
||||
// Criar o documento
|
||||
const documento = await prisma.documento.create({
|
||||
data: {
|
||||
nome: modelo.nome, // Usando o nome do modelo
|
||||
tipo: modelo.tipo,
|
||||
conteudo,
|
||||
processoId,
|
||||
atendimentoId,
|
||||
criadoPorId: req.user.id,
|
||||
modeloId: modelo.id
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
...documento,
|
||||
clienteId: variaveis?.cliente?.id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar documento:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar documento' });
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar a rota de configurações
|
||||
router.post('/configuracoes', async (req, res) => {
|
||||
try {
|
||||
const escritorioId = req.user.escritorioId; // Assumindo que você tem o ID do escritório no token
|
||||
|
||||
// Usar upsert para criar ou atualizar
|
||||
const config = await prisma.configuracaoDocumento.upsert({
|
||||
where: {
|
||||
escritorioId
|
||||
},
|
||||
update: {
|
||||
...req.body,
|
||||
margensTemplate: req.body.margensTemplate || {
|
||||
top: 35,
|
||||
bottom: 29,
|
||||
left: 25,
|
||||
right: 25
|
||||
}
|
||||
},
|
||||
create: {
|
||||
escritorioId,
|
||||
...req.body,
|
||||
margensTemplate: req.body.margensTemplate || {
|
||||
top: 35,
|
||||
bottom: 29,
|
||||
left: 25,
|
||||
right: 25
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.json(config);
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar configuração:', error);
|
||||
res.status(500).json({ error: 'Erro ao salvar configuração' });
|
||||
}
|
||||
});
|
||||
|
||||
// Rota para buscar última configuração
|
||||
router.get('/configuracoes', async (req, res) => {
|
||||
try {
|
||||
console.log('Buscando configurações de documento');
|
||||
|
||||
const config = await prisma.configuracaoDocumento.findFirst({
|
||||
orderBy: {
|
||||
criadoEm: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
// Criar configuração padrão se não existir
|
||||
const defaultConfig = await prisma.configuracaoDocumento.create({
|
||||
data: {
|
||||
timbradoMarginTop: 100,
|
||||
timbradoMarginBottom: 50,
|
||||
timbradoMarginLeft: 50,
|
||||
timbradoMarginRight: 50,
|
||||
margensTemplate: {
|
||||
top: 113,
|
||||
bottom: 90,
|
||||
left: 35,
|
||||
right: 35
|
||||
}
|
||||
}
|
||||
});
|
||||
return res.json(defaultConfig);
|
||||
}
|
||||
|
||||
res.json(config);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar configurações:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar configurações' });
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar configuração do documento
|
||||
router.post('/configuracao', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
// Primeiro, tente encontrar uma configuração existente
|
||||
const configExistente = await prisma.configuracaoDocumento.findFirst({
|
||||
where: {
|
||||
escritorioId: req.user.escritorioId
|
||||
}
|
||||
});
|
||||
|
||||
let config;
|
||||
|
||||
if (configExistente) {
|
||||
// Se existir, atualiza
|
||||
config = await prisma.configuracaoDocumento.update({
|
||||
where: {
|
||||
id: configExistente.id
|
||||
},
|
||||
data: {
|
||||
timbradoUrl: req.body.timbradoUrl,
|
||||
timbradoMarginTop: req.body.timbradoMarginTop,
|
||||
timbradoMarginBottom: req.body.timbradoMarginBottom,
|
||||
timbradoMarginLeft: req.body.timbradoMarginLeft,
|
||||
timbradoMarginRight: req.body.timbradoMarginRight,
|
||||
templatePadrao: req.body.templatePadrao || "",
|
||||
margensTemplate: req.body.margensTemplate
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Se não existir, cria
|
||||
config = await prisma.configuracaoDocumento.create({
|
||||
data: {
|
||||
escritorioId: req.user.escritorioId,
|
||||
timbradoUrl: req.body.timbradoUrl,
|
||||
timbradoMarginTop: req.body.timbradoMarginTop,
|
||||
timbradoMarginBottom: req.body.timbradoMarginBottom,
|
||||
timbradoMarginLeft: req.body.timbradoMarginLeft,
|
||||
timbradoMarginRight: req.body.timbradoMarginRight,
|
||||
templatePadrao: req.body.templatePadrao || "",
|
||||
margensTemplate: req.body.margensTemplate
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.json(config);
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar configuração:', error);
|
||||
res.status(500).json({ error: 'Falha ao salvar configuração do documento' });
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar documento com manipulação de erro aprimorada
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { nome, tipo, conteudo } = req.body;
|
||||
|
||||
// Verificar se o documento existe
|
||||
const documentoExistente = await prisma.documento.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, criadoPorId: true }
|
||||
});
|
||||
|
||||
if (!documentoExistente) {
|
||||
return res.status(404).json({ error: 'Documento não encontrado' });
|
||||
}
|
||||
|
||||
// Verificar permissão (apenas quem criou ou é admin pode editar)
|
||||
if (documentoExistente.criadoPorId !== req.user.id && req.user.perfil !== 'ADVOGADO_ADMIN') {
|
||||
return res.status(403).json({ error: 'Sem permissão para editar este documento' });
|
||||
}
|
||||
|
||||
// Validar dados
|
||||
if (!nome || !tipo || !conteudo) {
|
||||
return res.status(400).json({ error: 'Dados incompletos para atualização do documento' });
|
||||
}
|
||||
|
||||
// Atualizar o documento
|
||||
const documento = await prisma.documento.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nome,
|
||||
tipo: tipo as TipoDocumento,
|
||||
conteudo,
|
||||
atualizadoEm: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Registrar evento de atualização (opcional)
|
||||
// await prisma.auditoria.create({
|
||||
// data: {
|
||||
// entidade: 'DOCUMENTO',
|
||||
// entidadeId: id,
|
||||
// acao: 'ATUALIZAÇÃO',
|
||||
// usuarioId: req.user.id
|
||||
// }
|
||||
// });
|
||||
|
||||
res.json(documento);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar documento:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar documento' });
|
||||
}
|
||||
});
|
||||
|
||||
// Excluir documento
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await prisma.documento.delete({
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir documento:', error);
|
||||
res.status(500).json({ error: 'Erro ao excluir documento' });
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar nova rota para exportar PDF
|
||||
router.post('/exportar-pdf', async (req, res) => {
|
||||
try {
|
||||
const { conteudo, escritorio } = req.body;
|
||||
|
||||
const pdfService = new PdfService();
|
||||
const pdfBuffer = await pdfService.gerarPDF(conteudo, escritorio);
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=documento.pdf');
|
||||
res.send(pdfBuffer);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
res.status(500).json({ error: 'Erro ao gerar PDF' });
|
||||
}
|
||||
});
|
||||
|
||||
// Configuração do multer para upload de arquivos
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const uploadDir = path.join(process.cwd(), 'public', 'uploads');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
cb(null, `timbrado-${uniqueSuffix}-${file.originalname}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileFilter = (req: any, file: any, cb: any) => {
|
||||
// Atualizar para aceitar PDF
|
||||
const allowedTypes = ['image/png', 'image/svg+xml', 'application/pdf'];
|
||||
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Formato de arquivo não suportado. Use PNG, SVG ou PDF.'));
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024 // Aumentar para 5MB
|
||||
}
|
||||
});
|
||||
|
||||
// Rota de upload do timbrado
|
||||
router.post('/upload-timbrado', upload.single('timbrado'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
throw new Error('Nenhum arquivo enviado');
|
||||
}
|
||||
|
||||
// Criar URL relativa para o arquivo
|
||||
const fileUrl = `/uploads/${req.file.filename}`;
|
||||
|
||||
res.json({ url: fileUrl });
|
||||
} catch (error: any) {
|
||||
console.error('Erro no upload do timbrado:', error);
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,161 @@
|
||||
import express from 'express';
|
||||
import { IndiceMonetarioService, importarIndicesINPC } from '../services/IndiceMonetarioService';
|
||||
|
||||
const router = express.Router();
|
||||
const indiceService = new IndiceMonetarioService();
|
||||
|
||||
// Importar índices INPC (somente admin)
|
||||
router.post('/importar/inpc', async (req, res) => {
|
||||
try {
|
||||
// Verificar permissão de admin
|
||||
if (req.user?.perfil !== 'ADVOGADO_ADMIN') {
|
||||
return res.status(403).json({ error: 'Permissão negada' });
|
||||
}
|
||||
|
||||
const resultado = await indiceService.importarIndicesINPC();
|
||||
res.json(resultado);
|
||||
} catch (error) {
|
||||
console.error('Erro ao importar índices INPC:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Importar índices SELIC (somente admin)
|
||||
router.post('/importar/selic', async (req, res) => {
|
||||
try {
|
||||
// Verificar permissão de admin
|
||||
if (req.user?.perfil !== 'ADVOGADO_ADMIN') {
|
||||
return res.status(403).json({ error: 'Permissão negada' });
|
||||
}
|
||||
|
||||
const resultado = await indiceService.importarIndicesSELIC();
|
||||
res.json(resultado);
|
||||
} catch (error) {
|
||||
console.error('Erro ao importar índices SELIC:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar índice manualmente
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { nome, mesReferencia, anoReferencia, valor, dataPublicacao, fonte } = req.body;
|
||||
|
||||
// Verificar permissão de admin
|
||||
if (req.user?.perfil !== 'ADVOGADO_ADMIN') {
|
||||
return res.status(403).json({ error: 'Permissão negada' });
|
||||
}
|
||||
|
||||
// Validação de dados
|
||||
if (!nome || !mesReferencia || !anoReferencia || valor === undefined) {
|
||||
return res.status(400).json({ error: 'Campos obrigatórios faltando' });
|
||||
}
|
||||
|
||||
if (isNaN(parseInt(mesReferencia)) || parseInt(mesReferencia) < 1 || parseInt(mesReferencia) > 12) {
|
||||
return res.status(400).json({ error: 'Mês de referência inválido (deve ser entre 1 e 12)' });
|
||||
}
|
||||
|
||||
if (isNaN(parseInt(anoReferencia)) || parseInt(anoReferencia) < 1900 || parseInt(anoReferencia) > 2100) {
|
||||
return res.status(400).json({ error: 'Ano de referência inválido' });
|
||||
}
|
||||
|
||||
if (isNaN(parseFloat(valor))) {
|
||||
return res.status(400).json({ error: 'Valor inválido' });
|
||||
}
|
||||
|
||||
const resultado = await indiceService.adicionarIndice({
|
||||
nome,
|
||||
mesReferencia: parseInt(mesReferencia),
|
||||
anoReferencia: parseInt(anoReferencia),
|
||||
valor: parseFloat(valor),
|
||||
dataPublicacao: dataPublicacao ? new Date(dataPublicacao) : undefined,
|
||||
fonte,
|
||||
});
|
||||
|
||||
res.status(201).json(resultado);
|
||||
} catch (error) {
|
||||
console.error('Erro ao adicionar índice:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Listar índices
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { nome, anoReferencia, fonte } = req.query;
|
||||
|
||||
const indices = await indiceService.listarIndices({
|
||||
nome: nome as string,
|
||||
anoReferencia: anoReferencia ? parseInt(anoReferencia as string) : undefined,
|
||||
fonte: fonte as string,
|
||||
});
|
||||
|
||||
res.json(indices);
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar índices:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Buscar índice específico
|
||||
router.get('/:nome/:mes/:ano', async (req, res) => {
|
||||
try {
|
||||
const { nome, mes, ano } = req.params;
|
||||
|
||||
// Validação de dados
|
||||
if (isNaN(parseInt(mes)) || parseInt(mes) < 1 || parseInt(mes) > 12) {
|
||||
return res.status(400).json({ error: 'Mês de referência inválido (deve ser entre 1 e 12)' });
|
||||
}
|
||||
|
||||
if (isNaN(parseInt(ano)) || parseInt(ano) < 1900 || parseInt(ano) > 2100) {
|
||||
return res.status(400).json({ error: 'Ano de referência inválido' });
|
||||
}
|
||||
|
||||
const indice = await indiceService.buscarIndice({
|
||||
nome,
|
||||
mesReferencia: parseInt(mes),
|
||||
anoReferencia: parseInt(ano),
|
||||
});
|
||||
|
||||
if (!indice) {
|
||||
return res.status(404).json({ error: 'Índice não encontrado' });
|
||||
}
|
||||
|
||||
res.json(indice);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar índice:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Remover índice
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Verificar permissão de admin
|
||||
if (req.user?.perfil !== 'ADVOGADO_ADMIN') {
|
||||
return res.status(403).json({ error: 'Permissão negada' });
|
||||
}
|
||||
|
||||
const resultado = await indiceService.removerIndice(id);
|
||||
|
||||
res.json({ success: true, data: resultado });
|
||||
} catch (error) {
|
||||
console.error('Erro ao remover índice:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar rota para forçar atualização dos índices
|
||||
router.post('/atualizar', async (req, res) => {
|
||||
try {
|
||||
await importarIndicesINPC();
|
||||
res.json({ message: 'Índices atualizados com sucesso' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar índices:', error);
|
||||
res.status(500).json({ error: 'Falha ao atualizar índices' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Router } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { extractUser } from '../authMiddleware'; // Importar a função correta
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Middleware de autenticação
|
||||
router.use(extractUser);
|
||||
|
||||
// Criar novo cálculo de pensão alimentícia
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
titulo,
|
||||
descricao,
|
||||
dataInicial,
|
||||
dataFinal,
|
||||
percentualSalarioMinimo,
|
||||
multaPactuada,
|
||||
tipoAtualizacao,
|
||||
processoId,
|
||||
clienteId,
|
||||
observacoes
|
||||
} = req.body;
|
||||
|
||||
const calculo = await prisma.calculoPensaoAlimenticia.create({
|
||||
data: {
|
||||
titulo,
|
||||
descricao,
|
||||
status: 'ATIVO',
|
||||
dataInicial: new Date(dataInicial),
|
||||
dataFinal: dataFinal ? new Date(dataFinal) : null,
|
||||
percentualSalarioMinimo,
|
||||
multaPactuada,
|
||||
tipoAtualizacao,
|
||||
valorCalculado: 0,
|
||||
valorMulta: 0,
|
||||
valorTotal: 0,
|
||||
processoId,
|
||||
clienteId,
|
||||
observacoes,
|
||||
usuarioId: req.user.id
|
||||
}
|
||||
});
|
||||
|
||||
res.json(calculo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar cálculo de pensão:', error);
|
||||
res.status(500).json({ error: 'Falha ao criar cálculo' });
|
||||
}
|
||||
});
|
||||
|
||||
// Buscar cálculo por ID
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const calculo = await prisma.calculoPensaoAlimenticia.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
processo: true,
|
||||
cliente: true,
|
||||
usuario: {
|
||||
select: {
|
||||
id: true,
|
||||
nome: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!calculo) {
|
||||
return res.status(404).json({ error: 'Cálculo não encontrado' });
|
||||
}
|
||||
|
||||
res.json(calculo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar cálculo:', error);
|
||||
res.status(500).json({ error: 'Falha ao buscar cálculo' });
|
||||
}
|
||||
});
|
||||
|
||||
// Listar cálculos
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const calculos = await prisma.calculoPensaoAlimenticia.findMany({
|
||||
include: {
|
||||
processo: true,
|
||||
cliente: true,
|
||||
usuario: {
|
||||
select: {
|
||||
id: true,
|
||||
nome: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
criadoEm: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
res.json(calculos);
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar cálculos:', error);
|
||||
res.status(500).json({ error: 'Falha ao listar cálculos' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../../config/database';
|
||||
import { authMiddleware } from '../../middleware/auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Listar processos
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const processos = await prisma.processo.findMany({
|
||||
include: {
|
||||
cliente: true,
|
||||
partes: true,
|
||||
diligencias: true,
|
||||
audiencias: true,
|
||||
documentos: true,
|
||||
movimentacoes: true,
|
||||
honorarios: {
|
||||
include: {
|
||||
parcelas: true,
|
||||
},
|
||||
},
|
||||
custas: true,
|
||||
},
|
||||
orderBy: {
|
||||
dataCriacao: 'desc',
|
||||
},
|
||||
});
|
||||
res.json(processos);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar processos:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar processos' });
|
||||
}
|
||||
});
|
||||
|
||||
// Criar processo
|
||||
router.post('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const processo = await prisma.processo.create({
|
||||
data: {
|
||||
...req.body,
|
||||
cliente: {
|
||||
connect: { id: req.body.clienteId },
|
||||
},
|
||||
},
|
||||
include: {
|
||||
cliente: true,
|
||||
partes: true,
|
||||
diligencias: true,
|
||||
audiencias: true,
|
||||
documentos: true,
|
||||
movimentacoes: true,
|
||||
honorarios: {
|
||||
include: {
|
||||
parcelas: true,
|
||||
},
|
||||
},
|
||||
custas: true,
|
||||
},
|
||||
});
|
||||
res.status(201).json(processo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar processo:', error);
|
||||
res.status(400).json({ error: 'Erro ao criar processo' });
|
||||
}
|
||||
});
|
||||
|
||||
// Busca de processo individual
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const processo = await prisma.processo.findUnique({
|
||||
where: { id: req.params.id },
|
||||
include: {
|
||||
cliente: true,
|
||||
partes: true,
|
||||
diligencias: true,
|
||||
audiencias: true,
|
||||
documentos: true,
|
||||
movimentacoes: true,
|
||||
honorarios: {
|
||||
include: {
|
||||
parcelas: true,
|
||||
},
|
||||
},
|
||||
custas: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!processo) {
|
||||
return res.status(404).json({ error: "Processo não encontrado" });
|
||||
}
|
||||
|
||||
// Garantir que sempre tenha um resultado, mesmo que seja NAO_JULGADO
|
||||
if (!processo.resultado) {
|
||||
processo.resultado = 'NAO_JULGADO';
|
||||
}
|
||||
|
||||
res.json(processo);
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar processo:", error);
|
||||
res.status(500).json({ error: "Erro ao buscar processo" });
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar resultado do processo
|
||||
router.patch('/:id/resultado', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { resultado } = req.body;
|
||||
|
||||
const processo = await prisma.processo.update({
|
||||
where: { id },
|
||||
data: {
|
||||
resultado,
|
||||
dataAtualizacao: new Date()
|
||||
},
|
||||
include: {
|
||||
cliente: true,
|
||||
partes: true,
|
||||
diligencias: true,
|
||||
audiencias: true,
|
||||
documentos: true,
|
||||
movimentacoes: true
|
||||
}
|
||||
});
|
||||
|
||||
res.json(processo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar resultado:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar resultado do processo' });
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar processo
|
||||
router.put('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { clienteId, ...data } = req.body;
|
||||
|
||||
const processo = await prisma.processo.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
cliente: clienteId ? {
|
||||
connect: { id: clienteId }
|
||||
} : undefined
|
||||
},
|
||||
include: {
|
||||
cliente: true,
|
||||
partes: true,
|
||||
audiencias: true,
|
||||
documentos: true,
|
||||
movimentacoes: true,
|
||||
honorarios: {
|
||||
include: {
|
||||
parcelas: true
|
||||
}
|
||||
},
|
||||
custas: true
|
||||
}
|
||||
});
|
||||
res.json(processo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar processo:', error);
|
||||
res.status(400).json({ error: 'Erro ao atualizar processo' });
|
||||
}
|
||||
});
|
||||
|
||||
// Deletar processo
|
||||
router.delete('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Deletar primeiro as relações
|
||||
await prisma.$transaction([
|
||||
prisma.parteProcesso.deleteMany({ where: { processoId: id } }),
|
||||
prisma.audiencia.deleteMany({ where: { processoId: id } }),
|
||||
prisma.movimentacao.deleteMany({ where: { processoId: id } }),
|
||||
prisma.documento.deleteMany({ where: { processoId: id } }),
|
||||
prisma.custaProcessual.deleteMany({ where: { processoId: id } }),
|
||||
prisma.honorario.deleteMany({ where: { processoId: id } }),
|
||||
prisma.processo.delete({ where: { id } })
|
||||
]);
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar processo:', error);
|
||||
res.status(400).json({ error: 'Erro ao deletar processo' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,176 @@
|
||||
import express from 'express';
|
||||
import { ProcessoAdministrativoRepository } from '../../repositories/ProcessoAdministrativoRepository';
|
||||
|
||||
const router = express.Router();
|
||||
const repository = new ProcessoAdministrativoRepository();
|
||||
|
||||
// Listar todos os processos administrativos
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const processos = await repository.findAll();
|
||||
res.json(processos);
|
||||
} catch (error) {
|
||||
console.error("Erro ao listar processos administrativos:", error);
|
||||
res.status(500).json({ error: "Erro ao listar processos administrativos" });
|
||||
}
|
||||
});
|
||||
|
||||
// Buscar processo por ID
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Se o ID for "novo", retornar 404
|
||||
if (id === 'novo') {
|
||||
return res.status(404).json({ error: "Processo não encontrado" });
|
||||
}
|
||||
|
||||
const processo = await repository.findById(id);
|
||||
if (!processo) {
|
||||
return res.status(404).json({ error: "Processo não encontrado" });
|
||||
}
|
||||
res.json(processo);
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar processo:", error);
|
||||
res.status(500).json({ error: "Erro ao buscar processo" });
|
||||
}
|
||||
});
|
||||
|
||||
// Criar novo processo
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const novoProcesso = await repository.create(req.body);
|
||||
res.status(201).json(novoProcesso);
|
||||
} catch (error) {
|
||||
console.error("Erro ao criar processo:", error);
|
||||
res.status(400).json({
|
||||
error: "Erro ao criar processo",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar movimentação
|
||||
router.post('/:id/movimentacoes', async (req, res) => {
|
||||
try {
|
||||
const movimentacao = await repository.addMovimentacao(
|
||||
req.params.id,
|
||||
req.body
|
||||
);
|
||||
res.status(201).json(movimentacao);
|
||||
} catch (error) {
|
||||
console.error("Erro ao adicionar movimentação:", error);
|
||||
res.status(400).json({
|
||||
error: "Erro ao adicionar movimentação",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar movimentação
|
||||
router.put('/movimentacoes/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const movimentacao = await repository.updateMovimentacao(id, req.body);
|
||||
res.json(movimentacao);
|
||||
} catch (error) {
|
||||
console.error("Erro ao atualizar movimentação:", error);
|
||||
res.status(400).json({
|
||||
error: "Erro ao atualizar movimentação",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Excluir movimentação
|
||||
router.delete('/movimentacoes/:id', async (req, res) => {
|
||||
try {
|
||||
await repository.deleteMovimentacao(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error("Erro ao excluir movimentação:", error);
|
||||
res.status(400).json({
|
||||
error: "Erro ao excluir movimentação",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar processo
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const processoAtualizado = await repository.update(req.params.id, req.body);
|
||||
res.json(processoAtualizado);
|
||||
} catch (error) {
|
||||
console.error("Erro ao atualizar processo:", error);
|
||||
res.status(400).json({
|
||||
error: "Erro ao atualizar processo",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Excluir processo
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await repository.delete(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error("Erro ao excluir processo:", error);
|
||||
res.status(400).json({
|
||||
error: "Erro ao excluir processo",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Rotas para custas administrativas
|
||||
router.get('/:id/custas', async (req, res) => {
|
||||
try {
|
||||
const custas = await repository.findCustasByProcesso(req.params.id);
|
||||
res.json(custas);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
error: "Erro ao buscar custas",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/custas', async (req, res) => {
|
||||
try {
|
||||
const custa = await repository.addCusta(req.params.id, req.body);
|
||||
res.status(201).json(custa);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
error: "Erro ao criar custa",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:processoId/custas/:id', async (req, res) => {
|
||||
try {
|
||||
const custa = await repository.updateCusta(req.params.id, req.body);
|
||||
res.json(custa);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
error: "Erro ao atualizar custa",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:processoId/custas/:id', async (req, res) => {
|
||||
try {
|
||||
await repository.deleteCusta(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
error: "Erro ao excluir custa",
|
||||
details: error instanceof Error ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,241 @@
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import { ProvaService } from '../services/ProvaService';
|
||||
import { prisma } from '../prismaClient';
|
||||
import * as fsPromises from 'fs/promises';
|
||||
import * as fs from 'fs'; // Importando o módulo fs padrão
|
||||
import * as path from 'path';
|
||||
|
||||
const router = express.Router();
|
||||
const storage = multer.memoryStorage();
|
||||
const upload = multer({ storage });
|
||||
const provaService = new ProvaService();
|
||||
|
||||
// Upload de arquivo para validação
|
||||
router.post('/upload', upload.array('files'), async (req, res) => {
|
||||
try {
|
||||
console.log('Iniciando upload de arquivos...');
|
||||
|
||||
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
|
||||
return res.status(400).json({ error: 'Nenhum arquivo recebido' });
|
||||
}
|
||||
|
||||
// Extrair informações sobre o endereço IP e porta do cliente
|
||||
const forwarded = req.headers['x-forwarded-for'] as string;
|
||||
const socketInfo = req.socket.remoteAddress ? {
|
||||
address: req.socket.remoteAddress,
|
||||
port: req.socket.remotePort || 'desconhecida'
|
||||
} : null;
|
||||
|
||||
// Metadata avançada seguindo RFC 3227
|
||||
const metadata = {
|
||||
userAgent: req.body.user_agent,
|
||||
language: req.body.language,
|
||||
viewportWidth: req.body.viewport_width,
|
||||
viewportHeight: req.body.viewport_height,
|
||||
ipAddress: req.ip || req.socket.remoteAddress,
|
||||
clientPort: req.socket.remotePort || 'desconhecida',
|
||||
connectionInfo: {
|
||||
ip: req.ip || req.socket.remoteAddress,
|
||||
forwardedIp: forwarded,
|
||||
socketAddress: socketInfo?.address,
|
||||
socketPort: socketInfo?.port,
|
||||
remoteFamily: req.socket.remoteFamily
|
||||
},
|
||||
timestamp: new Date(),
|
||||
modification_times: req.body.modification_times,
|
||||
referrer: req.body.referrer,
|
||||
timezone: req.body.timezone,
|
||||
timezone_offset: req.body.timezone_offset,
|
||||
platform: req.body.platform,
|
||||
cookies_enabled: req.body.cookies_enabled,
|
||||
browser_plugins: req.body.browser_plugins,
|
||||
latitude: req.body.latitude,
|
||||
longitude: req.body.longitude,
|
||||
location_permission: req.body.location_permission,
|
||||
client_timestamp: req.body.client_timestamp,
|
||||
headers: {
|
||||
'x-forwarded-for': req.headers['x-forwarded-for'],
|
||||
'user-agent': req.headers['user-agent'],
|
||||
'accept-language': req.headers['accept-language'],
|
||||
'referer': req.headers['referer'],
|
||||
'host': req.headers['host'],
|
||||
'connection': req.headers['connection'],
|
||||
},
|
||||
server: {
|
||||
timestamp: new Date().toISOString(),
|
||||
hostname: require('os').hostname(),
|
||||
process: process.pid,
|
||||
nodeVersion: process.version,
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Metadata recebida:', metadata);
|
||||
console.log('Informações de conexão do cliente:', metadata.connectionInfo);
|
||||
|
||||
// Adicionar informações de segurança
|
||||
const securityInfo = {
|
||||
requestMethod: req.method,
|
||||
protocol: req.protocol,
|
||||
isSecure: req.secure,
|
||||
originalUrl: req.originalUrl,
|
||||
contentType: req.headers['content-type'],
|
||||
};
|
||||
|
||||
console.log('Informações de segurança:', securityInfo);
|
||||
|
||||
// Processar cada arquivo
|
||||
const results = [];
|
||||
for (const file of req.files as Express.Multer.File[]) {
|
||||
try {
|
||||
// Adicionar metadados específicos do arquivo atual
|
||||
const fileIndex = (req.files as Express.Multer.File[]).indexOf(file);
|
||||
|
||||
// Extrair e formatar a data de modificação do arquivo
|
||||
let fileModifiedDate = null;
|
||||
const modificationTime = req.body.modification_times ?
|
||||
(Array.isArray(req.body.modification_times) ? req.body.modification_times[fileIndex] : req.body.modification_times) :
|
||||
null;
|
||||
|
||||
if (modificationTime) {
|
||||
try {
|
||||
// Converter timestamp para objeto Date
|
||||
const timestamp = parseInt(modificationTime);
|
||||
if (!isNaN(timestamp)) {
|
||||
fileModifiedDate = new Date(timestamp);
|
||||
}
|
||||
} catch (dateError) {
|
||||
console.warn(`Erro ao processar data de modificação para ${file.originalname}:`, dateError);
|
||||
}
|
||||
}
|
||||
|
||||
const fileMetadata = {
|
||||
...metadata,
|
||||
file_specific: {
|
||||
name: req.body.file_names ?
|
||||
(Array.isArray(req.body.file_names) ? req.body.file_names[fileIndex] : req.body.file_names) :
|
||||
file.originalname,
|
||||
size: req.body.file_sizes ?
|
||||
(Array.isArray(req.body.file_sizes) ? req.body.file_sizes[fileIndex] : req.body.file_sizes) :
|
||||
file.size,
|
||||
type: req.body.file_types ?
|
||||
(Array.isArray(req.body.file_types) ? req.body.file_types[fileIndex] : req.body.file_types) :
|
||||
file.mimetype,
|
||||
modified: fileModifiedDate ? fileModifiedDate.toISOString() : null,
|
||||
modifiedTimestamp: modificationTime
|
||||
},
|
||||
security: securityInfo
|
||||
};
|
||||
|
||||
// Adicionar log para debug de data de modificação
|
||||
console.log(`Informações de modificação do arquivo ${file.originalname}:`, {
|
||||
rawTimestamp: modificationTime,
|
||||
processedDate: fileModifiedDate,
|
||||
isoString: fileModifiedDate ? fileModifiedDate.toISOString() : null
|
||||
});
|
||||
|
||||
const result = await provaService.processFile(file, fileMetadata);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
console.error(`Erro ao processar arquivo ${file.originalname}:`, error);
|
||||
return res.status(500).json({
|
||||
error: `Erro ao processar arquivo ${file.originalname}`,
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
results,
|
||||
id: results[0].id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro no upload:', error);
|
||||
res.status(500).json({ error: 'Erro ao processar arquivos' });
|
||||
}
|
||||
});
|
||||
|
||||
// Status da prova
|
||||
router.get('/:id/status', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const prova = await prisma.prova.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!prova) {
|
||||
return res.status(404).json({ error: 'Prova não encontrada' });
|
||||
}
|
||||
|
||||
res.json(prova);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar status:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar status da prova' });
|
||||
}
|
||||
});
|
||||
|
||||
// Verificar documento
|
||||
router.get('/verificar/:hash', async (req, res) => {
|
||||
try {
|
||||
const { hash } = req.params;
|
||||
const result = await provaService.verifyFile(hash);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Erro ao verificar documento:', error);
|
||||
res.status(500).json({ error: 'Erro ao verificar documento' });
|
||||
}
|
||||
});
|
||||
|
||||
// Modificar/adicionar esta rota para servir o relatório a partir do hash
|
||||
router.get('/relatorio/:hash', async (req, res) => {
|
||||
try {
|
||||
const { hash } = req.params;
|
||||
|
||||
// Buscar a prova pelo hash
|
||||
const prova = await prisma.prova.findFirst({
|
||||
where: { checksumSha256: hash }
|
||||
});
|
||||
|
||||
if (!prova || prova.status !== 'CONCLUIDO') {
|
||||
return res.status(404).json({ error: 'Relatório não encontrado' });
|
||||
}
|
||||
|
||||
// Construir o caminho para o arquivo do relatório
|
||||
const reportPath = path.join(process.cwd(), 'storage', 'provas', `${hash}_report.pdf`);
|
||||
|
||||
// Verificar se o arquivo existe
|
||||
try {
|
||||
await fsPromises.access(reportPath); // Usando fsPromises para operação assíncrona
|
||||
} catch (error) {
|
||||
console.error('Erro ao acessar relatório:', error);
|
||||
return res.status(404).json({ error: 'Arquivo do relatório não encontrado' });
|
||||
}
|
||||
|
||||
// Enviar o arquivo como resposta
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${prova.fileName.replace(/\.[^/.]+$/, '')}_relatorio.pdf"`);
|
||||
|
||||
// Criar stream de leitura e enviar para o cliente
|
||||
const fileStream = fs.createReadStream(reportPath); // Usando fs padrão para createReadStream
|
||||
fileStream.pipe(res);
|
||||
} catch (error) {
|
||||
console.error('Erro ao servir relatório:', error);
|
||||
res.status(500).json({ error: 'Erro ao gerar relatório' });
|
||||
}
|
||||
});
|
||||
|
||||
// Listar provas (admin)
|
||||
router.get('/admin', async (req, res) => {
|
||||
try {
|
||||
const provas = await prisma.prova.findMany({
|
||||
orderBy: { dataCriacao: 'desc' }
|
||||
});
|
||||
res.json(provas);
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar provas:', error);
|
||||
res.status(500).json({ error: 'Erro ao listar provas' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,191 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../../config/database';
|
||||
import { authMiddleware } from '../../middleware/auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Listar tarefas
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const tarefas = await prisma.tarefa.findMany({
|
||||
include: {
|
||||
responsavel: true,
|
||||
subtarefas: true,
|
||||
comentarios: {
|
||||
include: {
|
||||
criadoPor: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
dataVencimento: 'asc'
|
||||
}
|
||||
});
|
||||
res.json(tarefas);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Erro ao buscar tarefas' });
|
||||
}
|
||||
});
|
||||
|
||||
// Criar tarefa
|
||||
router.post('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const tarefa = await prisma.tarefa.create({
|
||||
data: {
|
||||
...req.body,
|
||||
criadoPorId: req.user.id,
|
||||
status: req.body.status || 'PENDENTE',
|
||||
progresso: req.body.progresso || 0,
|
||||
etiquetas: req.body.etiquetas || [],
|
||||
subtarefas: {
|
||||
create: req.body.subtarefas || []
|
||||
},
|
||||
comentarios: {
|
||||
create: req.body.comentarios || []
|
||||
}
|
||||
},
|
||||
include: {
|
||||
subtarefas: true,
|
||||
comentarios: true,
|
||||
responsavel: true,
|
||||
criadoPor: true
|
||||
}
|
||||
});
|
||||
|
||||
res.json(tarefa);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar tarefa:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar tarefa' });
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Implementar upload de anexos com integração Nextcloud
|
||||
// router.post('/:id/anexos', authMiddleware, uploadHandler.array('files'), async (req, res) => { ... });
|
||||
|
||||
// Atualizar status da tarefa
|
||||
router.patch('/:id/status', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, progresso, justificativa } = req.body;
|
||||
|
||||
const tarefa = await prisma.tarefa.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status,
|
||||
progresso: progresso || (status === 'CONCLUIDA' ? 100 : status === 'CANCELADA' ? 0 : undefined),
|
||||
justificativa,
|
||||
atualizadoEm: new Date()
|
||||
},
|
||||
include: {
|
||||
responsavel: true,
|
||||
subtarefas: true,
|
||||
comentarios: {
|
||||
include: {
|
||||
criadoPor: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.json(tarefa);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar status:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar status' });
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar rota de edição
|
||||
router.put('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
processoId,
|
||||
clienteId,
|
||||
responsavelId,
|
||||
criadoPorId,
|
||||
subtarefas,
|
||||
comentarios,
|
||||
id: _id,
|
||||
atualizadoEm,
|
||||
...data
|
||||
} = req.body;
|
||||
|
||||
const updateData = {
|
||||
...data,
|
||||
atualizadoEm: new Date(),
|
||||
responsavel: responsavelId ? {
|
||||
connect: { id: responsavelId }
|
||||
} : undefined,
|
||||
processo: processoId ? {
|
||||
connect: { id: processoId }
|
||||
} : {
|
||||
disconnect: true
|
||||
},
|
||||
cliente: clienteId ? {
|
||||
connect: { id: clienteId }
|
||||
} : {
|
||||
disconnect: true
|
||||
},
|
||||
subtarefas: {
|
||||
deleteMany: {},
|
||||
create: subtarefas?.map((st: any) => ({
|
||||
titulo: st.titulo,
|
||||
concluida: st.concluida || false
|
||||
})) || []
|
||||
},
|
||||
comentarios: comentarios?.length ? {
|
||||
create: comentarios.map((c: any) => ({
|
||||
texto: c.texto,
|
||||
criadoPor: {
|
||||
connect: { id: req.user.id }
|
||||
}
|
||||
}))
|
||||
} : undefined
|
||||
};
|
||||
|
||||
const tarefa = await prisma.tarefa.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
subtarefas: true,
|
||||
comentarios: {
|
||||
include: {
|
||||
criadoPor: true
|
||||
}
|
||||
},
|
||||
responsavel: true,
|
||||
criadoPor: true,
|
||||
processo: true,
|
||||
cliente: true
|
||||
}
|
||||
});
|
||||
|
||||
res.json(tarefa);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar tarefa:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar tarefa' });
|
||||
}
|
||||
});
|
||||
|
||||
// Adicionar rota de exclusão
|
||||
router.delete('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Verificar se usuário tem permissão
|
||||
if (!['ADVOGADO_ADMIN', 'ADVOGADO'].includes(req.user.perfil)) {
|
||||
return res.status(403).json({ error: 'Sem permissão para excluir tarefas' });
|
||||
}
|
||||
|
||||
await prisma.tarefa.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir tarefa:', error);
|
||||
res.status(500).json({ error: 'Erro ao excluir tarefa' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,82 @@
|
||||
import OpenTimestamps from 'opentimestamps-javascript';
|
||||
import { createHash } from 'crypto';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
export class BlockchainService {
|
||||
private readonly storageDir: string;
|
||||
|
||||
constructor() {
|
||||
this.storageDir = path.join(process.cwd(), 'storage', 'timestamps');
|
||||
this.ensureStorageDir();
|
||||
}
|
||||
|
||||
private async ensureStorageDir() {
|
||||
try {
|
||||
await fs.mkdir(this.storageDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar diretório de timestamps:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async createTimestamp(buffer: Buffer) {
|
||||
try {
|
||||
console.log('Iniciando criação de timestamp na blockchain...');
|
||||
|
||||
// Criar hash do arquivo e usar diretamente
|
||||
const fileHash = createHash('sha256').update(buffer).digest('hex');
|
||||
console.log('Hash gerado:', fileHash);
|
||||
|
||||
// Criar timestamp usando o hash em hex
|
||||
const timestamp = await OpenTimestamps.stamp(fileHash);
|
||||
if (!timestamp) {
|
||||
throw new Error('Falha ao gerar timestamp');
|
||||
}
|
||||
|
||||
console.log('Timestamp criado com sucesso');
|
||||
|
||||
// Salvar arquivo .ots
|
||||
const otsPath = path.join(this.storageDir, `${fileHash}.ots`);
|
||||
await fs.writeFile(otsPath, Buffer.from(timestamp));
|
||||
console.log('Arquivo .ots salvo em:', otsPath);
|
||||
|
||||
return {
|
||||
hash: fileHash,
|
||||
timestamp,
|
||||
otsPath
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro detalhado ao criar timestamp:', error);
|
||||
console.error('Stack:', error.stack);
|
||||
throw new Error(`Falha ao criar timestamp na blockchain: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyTimestamp(hash: string, timestamp: Buffer) {
|
||||
try {
|
||||
const result = await OpenTimestamps.verify(
|
||||
Buffer.from(timestamp),
|
||||
Buffer.from(hash, 'hex')
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return {
|
||||
verified: false,
|
||||
message: 'Timestamp ainda não confirmado na blockchain'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
verified: true,
|
||||
timestamp: new Date(result * 1000),
|
||||
message: 'Timestamp verificado na blockchain do Bitcoin'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao verificar timestamp:', error);
|
||||
return {
|
||||
verified: false,
|
||||
message: 'Erro ao verificar timestamp'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,45 @@
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
/**
|
||||
* Versão simplificada do MetadataCollector para quando faltam dependências
|
||||
*/
|
||||
export class FallbackMetadataCollector {
|
||||
/**
|
||||
* Coleta metadados básicos a partir dos dados fornecidos
|
||||
*/
|
||||
async collectMetadata(data: {
|
||||
userAgent: string;
|
||||
ipAddress: string;
|
||||
timestamp: Date;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
[key: string]: any;
|
||||
}) {
|
||||
const timestamp = new Date();
|
||||
|
||||
// Retornar metadados simplificados
|
||||
return {
|
||||
timestamp: timestamp.toISOString(),
|
||||
collectionTime: timestamp.toISOString(),
|
||||
userAgent: data.userAgent,
|
||||
ipAddress: data.ipAddress,
|
||||
fileName: data.fileName,
|
||||
fileSize: data.fileSize,
|
||||
fileSizeFormatted: this.formatFileSize(data.fileSize),
|
||||
integrityHash: this.generateHash(data.fileName, data.fileSize.toString(), timestamp.toISOString())
|
||||
};
|
||||
}
|
||||
|
||||
private formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return bytes + ' bytes';
|
||||
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
||||
else if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
else return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
private generateHash(...values: string[]): string {
|
||||
return createHash('sha256')
|
||||
.update(values.join('|'))
|
||||
.digest('hex');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
export class FileValidator {
|
||||
async validateFile(buffer: Buffer, mimeType: string) {
|
||||
try {
|
||||
// Validação básica
|
||||
if (!buffer || buffer.length === 0) {
|
||||
return { valid: false, error: 'Arquivo vazio' };
|
||||
}
|
||||
|
||||
// Verificar tamanho máximo (10MB)
|
||||
const MAX_SIZE = 10 * 1024 * 1024;
|
||||
if (buffer.length > MAX_SIZE) {
|
||||
return { valid: false, error: 'Arquivo muito grande (máximo 10MB)' };
|
||||
}
|
||||
|
||||
// Verificar tipos de arquivo permitidos
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'image/jpeg',
|
||||
'image/png', // Corrigido o tipo MIME para PNG
|
||||
'image/gif',
|
||||
'image.webp'
|
||||
];
|
||||
|
||||
if (!allowedTypes.includes(mimeType)) {
|
||||
return { valid: false, error: 'Tipo de arquivo não suportado. Use PDF, JPEG, PNG ou GIF.' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch (error) {
|
||||
console.error('Erro na validação de arquivo:', error);
|
||||
return { valid: false, error: `Erro ao validar arquivo: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
async validateMetadata(metadata: any) {
|
||||
// Campos básicos obrigatórios
|
||||
const requiredFields = ['userAgent', 'timestamp'];
|
||||
const missingFields = requiredFields.filter(field => !metadata[field]);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Dados necessários ausentes: ${missingFields.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
generateHashes(buffer: Buffer) {
|
||||
return {
|
||||
sha256: createHash('sha256').update(buffer).digest('hex'),
|
||||
sha512: createHash('sha512').update(buffer).digest('hex'),
|
||||
md5: createHash('md5').update(buffer).digest('hex')
|
||||
};
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
// Nada a fazer nesta implementação simplificada
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,931 @@
|
||||
import { PrismaClient, Decimal } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid'; // Importar uuid para gerar IDs
|
||||
import { prisma } from '../prismaClient';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class IndiceMonetarioService {
|
||||
/**
|
||||
* Adiciona um novo índice monetário
|
||||
*/
|
||||
async adicionarIndice(data: {
|
||||
nome: string;
|
||||
mesReferencia: number;
|
||||
anoReferencia: number;
|
||||
valor: number;
|
||||
dataPublicacao?: Date;
|
||||
fonte?: string;
|
||||
}) {
|
||||
try {
|
||||
// Verificar se já existe
|
||||
const existente = await prisma.indiceMonetario.findFirst({
|
||||
where: {
|
||||
nome: data.nome,
|
||||
mesReferencia: data.mesReferencia,
|
||||
anoReferencia: data.anoReferencia,
|
||||
}
|
||||
});
|
||||
|
||||
if (existente) {
|
||||
// Atualizar o existente
|
||||
return await prisma.indiceMonetario.update({
|
||||
where: { id: existente.id },
|
||||
data: {
|
||||
valor: new Decimal(data.valor),
|
||||
dataPublicacao: data.dataPublicacao,
|
||||
fonte: data.fonte,
|
||||
atualizadoEm: new Date()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Criar novo
|
||||
return await prisma.indiceMonetario.create({
|
||||
data: {
|
||||
id: uuidv4(), // Gerar ID UUID explicitamente
|
||||
nome: data.nome,
|
||||
mesReferencia: data.mesReferencia,
|
||||
anoReferencia: data.anoReferencia,
|
||||
valor: new Decimal(data.valor),
|
||||
dataPublicacao: data.dataPublicacao,
|
||||
fonte: data.fonte
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao adicionar índice monetário:', error);
|
||||
throw new Error(`Falha ao adicionar índice: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista índices monetários com filtros
|
||||
*/
|
||||
async listarIndices(filtros?: {
|
||||
nome?: string;
|
||||
anoReferencia?: number;
|
||||
fonte?: string;
|
||||
}) {
|
||||
try {
|
||||
const where: any = {};
|
||||
|
||||
if (filtros) {
|
||||
if (filtros.nome) {
|
||||
where.nome = filtros.nome;
|
||||
}
|
||||
|
||||
if (filtros.anoReferencia) {
|
||||
where.anoReferencia = filtros.anoReferencia;
|
||||
}
|
||||
|
||||
if (filtros.fonte) {
|
||||
where.fonte = { contains: filtros.fonte };
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.indiceMonetario.findMany({
|
||||
where,
|
||||
orderBy: [
|
||||
{ anoReferencia: 'desc' },
|
||||
{ mesReferencia: 'desc' },
|
||||
{ nome: 'asc' }
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar índices monetários:', error);
|
||||
throw new Error(`Falha ao listar índices: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca um índice específico
|
||||
*/
|
||||
async buscarIndice(data: {
|
||||
nome: string;
|
||||
mesReferencia: number;
|
||||
anoReferencia: number;
|
||||
}) {
|
||||
try {
|
||||
return await prisma.indiceMonetario.findFirst({
|
||||
where: {
|
||||
nome: data.nome,
|
||||
mesReferencia: data.mesReferencia,
|
||||
anoReferencia: data.anoReferencia
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar índice monetário:', error);
|
||||
throw new Error(`Falha ao buscar índice: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove um índice monetário
|
||||
*/
|
||||
async removerIndice(id: string) {
|
||||
try {
|
||||
return await prisma.indiceMonetario.delete({
|
||||
where: { id }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao remover índice monetário:', error);
|
||||
throw new Error(`Falha ao remover índice: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Importa índices INPC do IBGE
|
||||
*/
|
||||
async importarIndicesINPC() {
|
||||
try {
|
||||
console.log('Iniciando importação de índices INPC...');
|
||||
// Buscar últimos 120 meses
|
||||
const url = 'https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/-120/variaveis/44?localidades=N1[all]';
|
||||
|
||||
const response = await axios.get(url);
|
||||
const data = response.data[0];
|
||||
|
||||
if (!data?.resultados?.[0]?.series?.[0]?.serie) {
|
||||
throw new Error('Formato de resposta inválido do IBGE');
|
||||
}
|
||||
|
||||
const indices = data.resultados[0].series[0].serie;
|
||||
|
||||
// Processar e salvar os índices
|
||||
for (const [periodo, valor] of Object.entries(indices)) {
|
||||
const ano = parseInt(periodo.substring(0, 4));
|
||||
const mes = parseInt(periodo.substring(4));
|
||||
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
|
||||
|
||||
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
|
||||
await prisma.indiceMonetario.upsert({
|
||||
where: {
|
||||
nome_mesReferencia_anoReferencia: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano
|
||||
}
|
||||
},
|
||||
update: {
|
||||
valor: valorNumerico,
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
},
|
||||
create: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano,
|
||||
valor: valorNumerico,
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Índice importado: ${mes}/${ano} = ${valorNumerico}%`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Importação concluída com sucesso');
|
||||
} catch (error) {
|
||||
console.error('Erro ao importar índices:', error);
|
||||
throw new Error('Falha ao importar índices do IBGE');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Importa índices SELIC do Banco Central
|
||||
*/
|
||||
async importarIndicesSELIC() {
|
||||
try {
|
||||
// Usar a API do Banco Central em vez de CSV
|
||||
const hoje = new Date();
|
||||
const dataFim = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}-${String(hoje.getDate()).padStart(2, '0')}`;
|
||||
|
||||
// Pegar dados do último ano
|
||||
const dataInicio = `${hoje.getFullYear() - 1}-${String(hoje.getMonth() + 1).padStart(2, '0')}-${String(hoje.getDate()).padStart(2, '0')}`;
|
||||
|
||||
const url = `https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=${dataInicio}&dataFinal=${dataFim}`;
|
||||
const response = await axios.get(url);
|
||||
|
||||
if (!Array.isArray(response.data)) {
|
||||
throw new Error('Formato de resposta da API do Banco Central inválido');
|
||||
}
|
||||
|
||||
const indices = [];
|
||||
|
||||
for (const item of response.data) {
|
||||
// Tratar a data (formato DD/MM/YYYY)
|
||||
const dataParts = item.data.split('/');
|
||||
const ano = parseInt(dataParts[2]);
|
||||
const mes = parseInt(dataParts[1]);
|
||||
const valorNumerico = parseFloat(item.valor);
|
||||
|
||||
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
|
||||
await this.adicionarIndice({
|
||||
nome: 'SELIC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano,
|
||||
valor: valorNumerico / 100, // Converter para decimal (ex: 13.25% vira 0.1325)
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'BACEN'
|
||||
});
|
||||
|
||||
indices.push({
|
||||
nome: 'SELIC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano,
|
||||
valor: valorNumerico / 100
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Importados ${indices.length} índices SELIC`,
|
||||
indices
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao importar índices SELIC:', error);
|
||||
throw new Error(`Falha ao importar índices SELIC: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula o fator de correção entre duas datas para um índice
|
||||
*/
|
||||
async calcularFatorCorrecao(data: {
|
||||
indice: string;
|
||||
dataInicial: Date;
|
||||
dataFinal: Date;
|
||||
}) {
|
||||
try {
|
||||
const { indice, dataInicial, dataFinal } = data;
|
||||
|
||||
// Obter o mês/ano das datas
|
||||
const mesInicial = dataInicial.getMonth() + 1; // getMonth retorna 0-11
|
||||
const anoInicial = dataInicial.getFullYear();
|
||||
const mesFinal = dataFinal.getMonth() + 1;
|
||||
const anoFinal = dataFinal.getFullYear();
|
||||
|
||||
// Buscar todos os índices necessários para o período
|
||||
const indices = await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: indice,
|
||||
OR: [
|
||||
// Índices do ano inicial
|
||||
{
|
||||
anoReferencia: anoInicial,
|
||||
mesReferencia: { gte: mesInicial }
|
||||
},
|
||||
// Anos intermediários (se houver)
|
||||
{
|
||||
anoReferencia: { gt: anoInicial, lt: anoFinal }
|
||||
},
|
||||
// Índices do ano final
|
||||
{
|
||||
anoReferencia: anoFinal,
|
||||
mesReferencia: { lte: mesFinal }
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' }
|
||||
]
|
||||
});
|
||||
|
||||
// Se não houver índices suficientes, usar uma estimativa
|
||||
if (indices.length === 0) {
|
||||
const mesesDiferenca =
|
||||
(anoFinal - anoInicial) * 12 + (mesFinal - mesInicial);
|
||||
|
||||
// Taxa média mensal estimada (usar 0.5% ao mês como estimativa conservadora)
|
||||
const taxaMediaMensal = 0.005;
|
||||
return new Decimal(Math.pow(1 + taxaMediaMensal, mesesDiferenca));
|
||||
}
|
||||
|
||||
// Calcular o fator de correção acumulado
|
||||
let fatorAcumulado = new Decimal(1);
|
||||
|
||||
for (const indice of indices) {
|
||||
// Somar 1 ao valor do índice (ex: índice de 0.0082 vira fator de 1.0082)
|
||||
const fator = new Decimal(1).add(indice.valor);
|
||||
fatorAcumulado = fatorAcumulado.mul(fator);
|
||||
}
|
||||
|
||||
return fatorAcumulado;
|
||||
} catch (error) {
|
||||
console.error('Erro ao calcular fator de correção:', error);
|
||||
throw new Error(`Falha ao calcular fator de correção: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Função para baixar índices do IBGE
|
||||
export async function atualizarIndicesINPC() {
|
||||
try {
|
||||
console.log('Iniciando atualização dos índices INPC...');
|
||||
|
||||
// Definir período de busca (5 anos para trás)
|
||||
const hoje = new Date();
|
||||
const anoInicial = hoje.getFullYear() - 5;
|
||||
|
||||
// Buscar último índice no banco
|
||||
const ultimoIndice = await prisma.indiceMonetario.findFirst({
|
||||
orderBy: [
|
||||
{ anoReferencia: 'desc' },
|
||||
{ mesReferencia: 'desc' }
|
||||
]
|
||||
});
|
||||
|
||||
// URL da API do IBGE para INPC (últimos 120 meses = 10 anos)
|
||||
const url = 'https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/-120/variaveis/44?localidades=N1[all]';
|
||||
|
||||
console.log('Buscando índices desde:', anoInicial);
|
||||
const response = await axios.get(url);
|
||||
|
||||
if (!response.data?.[0]?.resultados?.[0]?.series?.[0]?.serie) {
|
||||
throw new Error('Formato de resposta inválido do IBGE');
|
||||
}
|
||||
|
||||
const indices = response.data[0].resultados[0].series[0].serie;
|
||||
let totalIndices = 0;
|
||||
|
||||
// Processar cada índice
|
||||
for (const [periodo, valor] of Object.entries(indices)) {
|
||||
const ano = parseInt(periodo.substring(0, 4));
|
||||
const mes = parseInt(periodo.substring(4));
|
||||
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
|
||||
|
||||
// Verificar se é um índice válido
|
||||
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
|
||||
await prisma.indiceMonetario.upsert({
|
||||
where: {
|
||||
nome_mesReferencia_anoReferencia: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano
|
||||
}
|
||||
},
|
||||
update: {
|
||||
valor: valorNumerico,
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
},
|
||||
create: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano,
|
||||
valor: valorNumerico,
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
}
|
||||
});
|
||||
|
||||
totalIndices++;
|
||||
console.log(`Índice atualizado: ${mes}/${ano} = ${valorNumerico}%`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Atualização concluída. Total de índices: ${totalIndices}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar índices INPC:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Função para verificar se os índices existem para um período
|
||||
export async function verificarIndicesDisponiveis(dataInicial: Date, dataFinal: Date): Promise<boolean> {
|
||||
try {
|
||||
const indices = await prisma.indiceMonetario.findFirst({
|
||||
where: {
|
||||
nome: 'INPC',
|
||||
AND: [
|
||||
{
|
||||
anoReferencia: dataInicial.getFullYear(),
|
||||
mesReferencia: dataInicial.getMonth() + 1
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Se não encontrar índices, tenta atualizar
|
||||
if (!indices) {
|
||||
console.log('Índices não encontrados. Iniciando atualização...');
|
||||
await atualizarIndicesINPC();
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao verificar índices:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getIndicesParaPeriodo({
|
||||
indice,
|
||||
dataInicial,
|
||||
dataFinal
|
||||
}: {
|
||||
indice: string;
|
||||
dataInicial: Date;
|
||||
dataFinal: Date;
|
||||
}) {
|
||||
try {
|
||||
// Buscar índices no período
|
||||
let indices = await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: indice,
|
||||
AND: [
|
||||
{
|
||||
anoReferencia: {
|
||||
gte: dataInicial.getFullYear(),
|
||||
lte: dataFinal.getFullYear()
|
||||
}
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
// Índices do primeiro ano
|
||||
{
|
||||
anoReferencia: dataInicial.getFullYear(),
|
||||
mesReferencia: {
|
||||
gte: dataInicial.getMonth() + 1
|
||||
}
|
||||
},
|
||||
// Índices do último ano
|
||||
{
|
||||
anoReferencia: dataFinal.getFullYear(),
|
||||
mesReferencia: {
|
||||
lte: dataFinal.getMonth() + 1
|
||||
}
|
||||
},
|
||||
// Índices dos anos intermediários
|
||||
{
|
||||
anoReferencia: {
|
||||
gt: dataInicial.getFullYear(),
|
||||
lt: dataFinal.getFullYear()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' }
|
||||
]
|
||||
});
|
||||
|
||||
// Se não encontrar índices, tentar importar
|
||||
if (!indices || indices.length === 0) {
|
||||
console.log('Índices não encontrados. Iniciando importação...');
|
||||
await importarIndicesINPC();
|
||||
|
||||
// Buscar novamente após importação
|
||||
indices = await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: indice,
|
||||
AND: [
|
||||
{
|
||||
anoReferencia: {
|
||||
gte: dataInicial.getFullYear(),
|
||||
lte: dataFinal.getFullYear()
|
||||
}
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
// Índices do primeiro ano
|
||||
{
|
||||
anoReferencia: dataInicial.getFullYear(),
|
||||
mesReferencia: {
|
||||
gte: dataInicial.getMonth() + 1
|
||||
}
|
||||
},
|
||||
// Índices do último ano
|
||||
{
|
||||
anoReferencia: dataFinal.getFullYear(),
|
||||
mesReferencia: {
|
||||
lte: dataFinal.getMonth() + 1
|
||||
}
|
||||
},
|
||||
// Índices dos anos intermediários
|
||||
{
|
||||
anoReferencia: {
|
||||
gt: dataInicial.getFullYear(),
|
||||
lt: dataFinal.getFullYear()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (!indices.length) {
|
||||
throw new Error('Não foram encontrados índices para o período');
|
||||
}
|
||||
|
||||
return indices;
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar/importar índices:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function importarIndicesINPC() {
|
||||
try {
|
||||
// API do IBGE para INPC
|
||||
const url = 'https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/-120/variaveis/44?localidades=N1[all]';
|
||||
|
||||
console.log('Buscando índices do IBGE...');
|
||||
const response = await axios.get(url);
|
||||
|
||||
if (!response.data || !response.data[0] || !response.data[0].resultados) {
|
||||
throw new Error('Formato de resposta inválido da API do IBGE');
|
||||
}
|
||||
|
||||
const dados = response.data[0].resultados[0].series[0].serie;
|
||||
|
||||
// Processar e salvar os índices
|
||||
for (const [periodo, valor] of Object.entries(dados)) {
|
||||
const ano = parseInt(periodo.substring(0, 4));
|
||||
const mes = parseInt(periodo.substring(4));
|
||||
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
|
||||
|
||||
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
|
||||
await prisma.indiceMonetario.upsert({
|
||||
where: {
|
||||
nome_mesReferencia_anoReferencia: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano
|
||||
}
|
||||
},
|
||||
update: {
|
||||
valor: valorNumerico / 100 + 1, // Converter para fator (ex: 1.0053 para 0.53%)
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
},
|
||||
create: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano,
|
||||
valor: valorNumerico / 100 + 1,
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Índices INPC importados com sucesso');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao importar índices INPC:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function obterIndicesNecessarios(dataInicial: Date, dataFinal: Date) {
|
||||
try {
|
||||
// 1. Verificar índices existentes no banco
|
||||
const indicesExistentes = await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: 'INPC',
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
anoReferencia: {
|
||||
gte: dataInicial.getFullYear(),
|
||||
lte: dataFinal.getFullYear(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' },
|
||||
],
|
||||
});
|
||||
|
||||
// 2. Criar mapa dos índices existentes para fácil verificação
|
||||
const mapaIndices = new Map();
|
||||
indicesExistentes.forEach(indice => {
|
||||
const chave = `${indice.anoReferencia}-${indice.mesReferencia.toString().padStart(2, '0')}`;
|
||||
mapaIndices.set(chave, indice);
|
||||
});
|
||||
|
||||
// 3. Verificar quais meses estão faltando
|
||||
const mesesFaltantes: { ano: number; mes: number }[] = [];
|
||||
let dataAtual = new Date(dataInicial);
|
||||
|
||||
while (dataAtual <= dataFinal) {
|
||||
const chave = `${dataAtual.getFullYear()}-${(dataAtual.getMonth() + 1).toString().padStart(2, '0')}`;
|
||||
|
||||
if (!mapaIndices.has(chave)) {
|
||||
mesesFaltantes.push({
|
||||
ano: dataAtual.getFullYear(),
|
||||
mes: dataAtual.getMonth() + 1
|
||||
});
|
||||
}
|
||||
|
||||
// Avançar para o próximo mês
|
||||
dataAtual.setMonth(dataAtual.getMonth() + 1);
|
||||
}
|
||||
|
||||
// 4. Se houver meses faltantes, buscar da API
|
||||
if (mesesFaltantes.length > 0) {
|
||||
console.log(`Buscando ${mesesFaltantes.length} índices faltantes do IBGE...`);
|
||||
|
||||
// Calcular o número de meses entre a data mais antiga e mais recente necessária
|
||||
const mesesTotal = Math.ceil(
|
||||
(dataFinal.getTime() - dataInicial.getTime()) / (1000 * 60 * 60 * 24 * 30.44)
|
||||
);
|
||||
|
||||
// Buscar um período maior que o necessário para garantir
|
||||
const periodoAPI = Math.max(mesesTotal + 12, 120); // Mínimo de 120 meses (10 anos)
|
||||
|
||||
const url = `https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/-${periodoAPI}/variaveis/44?localidades=N1[all]`;
|
||||
|
||||
const response = await axios.get(url);
|
||||
const data = response.data[0];
|
||||
|
||||
if (!data?.resultados?.[0]?.series?.[0]?.serie) {
|
||||
throw new Error('Formato de resposta inválido do IBGE');
|
||||
}
|
||||
|
||||
const indices = data.resultados[0].series[0].serie;
|
||||
|
||||
// Criar os índices faltantes
|
||||
for (const [periodo, valor] of Object.entries(indices)) {
|
||||
const ano = parseInt(periodo.substring(0, 4));
|
||||
const mes = parseInt(periodo.substring(4));
|
||||
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
|
||||
|
||||
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
|
||||
// Verificar se este índice é necessário
|
||||
const chave = `${ano}-${mes.toString().padStart(2, '0')}`;
|
||||
if (!mapaIndices.has(chave) &&
|
||||
ano >= dataInicial.getFullYear() &&
|
||||
ano <= dataFinal.getFullYear()) {
|
||||
|
||||
await prisma.indiceMonetario.upsert({
|
||||
where: {
|
||||
nome_mesReferencia_anoReferencia: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano
|
||||
}
|
||||
},
|
||||
update: {
|
||||
valor: valorNumerico,
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
},
|
||||
create: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano,
|
||||
valor: valorNumerico,
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Índice criado: ${mes}/${ano} = ${valorNumerico}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Retornar todos os índices necessários
|
||||
return await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: 'INPC',
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
anoReferencia: {
|
||||
gte: dataInicial.getFullYear(),
|
||||
lte: dataFinal.getFullYear(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' },
|
||||
],
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao obter índices:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar a função principal para usar a nova função de obtenção de índices
|
||||
export async function calcularFatorCorrecaoTotal({
|
||||
dataInicial,
|
||||
dataFinal,
|
||||
indice
|
||||
}: {
|
||||
dataInicial: Date;
|
||||
dataFinal: Date;
|
||||
indice: string;
|
||||
}): Promise<Decimal> {
|
||||
try {
|
||||
// Buscar ou criar todos os índices necessários
|
||||
const indices = await obterIndicesNecessarios(dataInicial, dataFinal);
|
||||
|
||||
if (!indices.length) {
|
||||
throw new Error('Não foram encontrados índices para o período mesmo após tentativa de atualização');
|
||||
}
|
||||
|
||||
// Calcular fator acumulado
|
||||
let fatorAcumulado = new Decimal(1);
|
||||
for (const indice of indices) {
|
||||
fatorAcumulado = fatorAcumulado.mul(new Decimal(1 + indice.valor / 100));
|
||||
}
|
||||
|
||||
console.log('Fator de correção calculado:', {
|
||||
periodo: `${dataInicial.toISOString()} até ${dataFinal.toISOString()}`,
|
||||
indices: indices.length,
|
||||
primeiro: indices[0],
|
||||
ultimo: indices[indices.length - 1],
|
||||
fatorAcumulado: fatorAcumulado.toFixed(6)
|
||||
});
|
||||
|
||||
return fatorAcumulado;
|
||||
} catch (error) {
|
||||
console.error('Erro ao calcular fator de correção:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function buscarIndicesParaPeriodo(dataInicial: Date, dataFinal: Date) {
|
||||
console.log(`Buscando índices para o período de ${dataInicial.toISOString()} até ${dataFinal.toISOString()}`);
|
||||
|
||||
try {
|
||||
// Primeiro verificar quais meses já temos
|
||||
const indicesExistentes = await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: 'INPC',
|
||||
OR: [
|
||||
{
|
||||
anoReferencia: {
|
||||
gte: dataInicial.getFullYear(),
|
||||
lte: dataFinal.getFullYear()
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' }
|
||||
]
|
||||
});
|
||||
|
||||
console.log(`Encontrados ${indicesExistentes.length} índices no banco`);
|
||||
|
||||
// Se não houver índices suficientes, buscar da API do IBGE
|
||||
if (indicesExistentes.length === 0) {
|
||||
console.log('Buscando índices do IBGE...');
|
||||
|
||||
// Configurar timeout maior para a requisição
|
||||
const axiosInstance = axios.create({
|
||||
timeout: 30000 // 30 segundos
|
||||
});
|
||||
|
||||
// Fazer múltiplas requisições em paralelo para diferentes períodos
|
||||
const promises = [];
|
||||
let anoAtual = dataInicial.getFullYear();
|
||||
const anoFinal = dataFinal.getFullYear();
|
||||
|
||||
while (anoAtual <= anoFinal) {
|
||||
const url = `https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/${anoAtual}/variaveis/44?localidades=N1[all]`;
|
||||
promises.push(axiosInstance.get(url));
|
||||
anoAtual++;
|
||||
}
|
||||
|
||||
console.log(`Fazendo ${promises.length} requisições ao IBGE...`);
|
||||
|
||||
const responses = await Promise.allSettled(promises);
|
||||
|
||||
// Processar cada resposta
|
||||
for (const response of responses) {
|
||||
if (response.status === 'fulfilled' && response.value.data?.[0]?.resultados?.[0]?.series?.[0]?.serie) {
|
||||
const indices = response.value.data[0].resultados[0].series[0].serie;
|
||||
|
||||
for (const [periodo, valor] of Object.entries(indices)) {
|
||||
const ano = parseInt(periodo.substring(0, 4));
|
||||
const mes = parseInt(periodo.substring(4));
|
||||
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
|
||||
|
||||
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
|
||||
await prisma.indiceMonetario.upsert({
|
||||
where: {
|
||||
nome_mesReferencia_anoReferencia: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano
|
||||
}
|
||||
},
|
||||
update: {
|
||||
valor: valorNumerico,
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
},
|
||||
create: {
|
||||
nome: 'INPC',
|
||||
mesReferencia: mes,
|
||||
anoReferencia: ano,
|
||||
valor: valorNumerico,
|
||||
dataPublicacao: new Date(),
|
||||
fonte: 'IBGE'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Índice salvo: ${mes}/${ano} = ${valorNumerico}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar novamente após importação
|
||||
return await prisma.indiceMonetario.findMany({
|
||||
where: {
|
||||
nome: 'INPC',
|
||||
OR: [
|
||||
{
|
||||
anoReferencia: {
|
||||
gte: dataInicial.getFullYear(),
|
||||
lte: dataFinal.getFullYear()
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ anoReferencia: 'asc' },
|
||||
{ mesReferencia: 'asc' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
return indicesExistentes;
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar índices:', error);
|
||||
throw new Error(`Falha ao buscar índices monetários: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar a função principal para usar o novo método
|
||||
export async function calcularFatorCorrecaoTotal({
|
||||
dataInicial,
|
||||
dataFinal,
|
||||
indice
|
||||
}: {
|
||||
dataInicial: Date;
|
||||
dataFinal: Date;
|
||||
indice: string;
|
||||
}): Promise<Decimal> {
|
||||
try {
|
||||
const indices = await buscarIndicesParaPeriodo(dataInicial, dataFinal);
|
||||
|
||||
if (!indices.length) {
|
||||
throw new Error('Não foram encontrados índices para o período informado');
|
||||
}
|
||||
|
||||
// Calcular fator acumulado
|
||||
let fatorAcumulado = new Decimal(1);
|
||||
for (const indice of indices) {
|
||||
fatorAcumulado = fatorAcumulado.mul(new Decimal(1 + indice.valor / 100));
|
||||
}
|
||||
|
||||
console.log('Fator de correção calculado:', {
|
||||
indices: indices.length,
|
||||
primeiro: indices[0],
|
||||
ultimo: indices[indices.length - 1],
|
||||
fatorAcumulado: fatorAcumulado.toString()
|
||||
});
|
||||
|
||||
return fatorAcumulado;
|
||||
} catch (error) {
|
||||
console.error('Erro ao calcular fator de correção:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import axios from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
// Tentativa de importar o UAParser com fallback para evitar quebra caso não esteja instalado
|
||||
let UAParser;
|
||||
try {
|
||||
UAParser = require('ua-parser-js');
|
||||
} catch (error) {
|
||||
console.warn('Aviso: UAParser não está disponível. Usando método alternativo para analisar User-Agent.');
|
||||
// Implementação simples de fallback para quando a biblioteca não está disponível
|
||||
UAParser = class SimpleBrowserDetect {
|
||||
constructor(userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
getResult() {
|
||||
const isMobile = /mobile|android|iphone|ipod|ipad/i.test(this.userAgent);
|
||||
const isWindows = /windows/i.test(this.userAgent);
|
||||
const isMac = /macintosh|mac os/i.test(this.userAgent);
|
||||
const isLinux = /linux/i.test(this.userAgent);
|
||||
|
||||
const chromeMatch = this.userAgent.match(/chrome\/(\d+)/i);
|
||||
const firefoxMatch = this.userAgent.match(/firefox\/(\d+)/i);
|
||||
const safariMatch = this.userAgent.match(/safari\/(\d+)/i);
|
||||
const edgeMatch = this.userAgent.match(/edge\/(\d+)|edg\/(\d+)/i);
|
||||
|
||||
return {
|
||||
browser: {
|
||||
name: edgeMatch ? 'Edge' :
|
||||
chromeMatch ? 'Chrome' :
|
||||
firefoxMatch ? 'Firefox' :
|
||||
safariMatch ? 'Safari' : 'Desconhecido',
|
||||
version: (edgeMatch || chromeMatch || firefoxMatch || safariMatch || ['', '0'])[1],
|
||||
major: (edgeMatch || chromeMatch || firefoxMatch || safariMatch || ['', '0'])[1],
|
||||
},
|
||||
engine: {
|
||||
name: 'Desconhecido',
|
||||
version: '',
|
||||
},
|
||||
os: {
|
||||
name: isWindows ? 'Windows' :
|
||||
isMac ? 'Mac OS' :
|
||||
isLinux ? 'Linux' : 'Desconhecido',
|
||||
version: '',
|
||||
},
|
||||
device: {
|
||||
vendor: '',
|
||||
model: '',
|
||||
type: isMobile ? 'mobile' : 'desktop',
|
||||
},
|
||||
cpu: {
|
||||
architecture: '',
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Classe para coletar e validar metadados de arquivos conforme RFC 3227
|
||||
* (Diretrizes para Coleta e Arquivamento de Evidências)
|
||||
*/
|
||||
export class MetadataCollector {
|
||||
/**
|
||||
* Coleta metadados avançados a partir dos dados fornecidos
|
||||
*/
|
||||
async collectMetadata(data: {
|
||||
userAgent: string;
|
||||
ipAddress: string;
|
||||
timestamp: Date;
|
||||
fileName: string;
|
||||
fileModified?: Date;
|
||||
fileSize: number;
|
||||
viewportWidth?: string;
|
||||
viewportHeight?: string;
|
||||
language?: string;
|
||||
referrer?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
}) {
|
||||
try {
|
||||
// Gerar identificador único para a coleta
|
||||
const collectionId = uuidv4();
|
||||
const collectionTimestamp = new Date();
|
||||
|
||||
// Parser para informações do navegador
|
||||
const parser = new UAParser(data.userAgent);
|
||||
const browserInfo = parser.getResult();
|
||||
|
||||
// Obter geolocalização a partir do IP se não fornecida diretamente
|
||||
let geoInfo = null;
|
||||
if (!data.latitude && !data.longitude) {
|
||||
try {
|
||||
geoInfo = await this.getGeolocationFromIP(data.ipAddress);
|
||||
} catch (error) {
|
||||
console.warn('Falha ao obter geolocalização:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Construir o objeto de metadados
|
||||
return {
|
||||
// Identificação da coleta
|
||||
collectionId,
|
||||
collectionTimestamp: collectionTimestamp.toISOString(),
|
||||
|
||||
// Dados do usuário
|
||||
userInfo: {
|
||||
ipAddress: data.ipAddress,
|
||||
userAgent: data.userAgent,
|
||||
language: data.language || 'não informado',
|
||||
browser: {
|
||||
name: browserInfo.browser.name || 'desconhecido',
|
||||
version: browserInfo.browser.version || 'desconhecido',
|
||||
major: browserInfo.browser.major || 'desconhecido',
|
||||
},
|
||||
engine: {
|
||||
name: browserInfo.engine.name || 'desconhecido',
|
||||
version: browserInfo.engine.version || 'desconhecido',
|
||||
},
|
||||
os: {
|
||||
name: browserInfo.os.name || 'desconhecido',
|
||||
version: browserInfo.os.version || 'desconhecido',
|
||||
},
|
||||
device: {
|
||||
vendor: browserInfo.device.vendor || 'desconhecido',
|
||||
model: browserInfo.device.model || 'desconhecido',
|
||||
type: browserInfo.device.type || 'desconhecido',
|
||||
},
|
||||
cpu: {
|
||||
architecture: browserInfo.cpu.architecture || 'desconhecido',
|
||||
},
|
||||
viewportSize: data.viewportWidth && data.viewportHeight
|
||||
? `${data.viewportWidth}x${data.viewportHeight}`
|
||||
: 'não informado',
|
||||
referrer: data.referrer || 'não informado',
|
||||
},
|
||||
|
||||
// Dados do arquivo
|
||||
fileInfo: {
|
||||
name: data.fileName,
|
||||
size: data.fileSize,
|
||||
sizeFormatted: this.formatFileSize(data.fileSize),
|
||||
modifiedDate: data.fileModified ? data.fileModified.toISOString() : 'não informado',
|
||||
},
|
||||
|
||||
// Dados de localização
|
||||
locationInfo: geoInfo || {
|
||||
latitude: data.latitude || null,
|
||||
longitude: data.longitude || null,
|
||||
city: 'não disponível',
|
||||
region: 'não disponível',
|
||||
country: 'não disponível',
|
||||
timezone: 'não disponível'
|
||||
},
|
||||
|
||||
// Timestamp oficial do sistema
|
||||
timestamp: data.timestamp.toISOString(),
|
||||
|
||||
// Hash da coleta para garantir integridade
|
||||
integrityHash: this.generateIntegrityHash(
|
||||
data.ipAddress,
|
||||
data.userAgent,
|
||||
data.fileName,
|
||||
data.fileSize.toString(),
|
||||
collectionTimestamp.toISOString()
|
||||
)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao coletar metadados:', error);
|
||||
|
||||
// Retornar metadados simplificados se algo der errado
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
userAgent: data.userAgent,
|
||||
ipAddress: data.ipAddress,
|
||||
fileName: data.fileName,
|
||||
fileSize: data.fileSize,
|
||||
error: 'Falha na coleta completa de metadados'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata o tamanho do arquivo para exibição
|
||||
*/
|
||||
private formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return bytes + ' bytes';
|
||||
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
||||
else if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
else return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera um hash de integridade para os metadados coletados
|
||||
*/
|
||||
private generateIntegrityHash(...inputs: string[]): string {
|
||||
try {
|
||||
return createHash('sha256')
|
||||
.update(inputs.join('|'))
|
||||
.digest('hex');
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar hash de integridade:', error);
|
||||
return 'hash-error-' + Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações de geolocalização a partir do endereço IP
|
||||
*/
|
||||
private async getGeolocationFromIP(ipAddress: string) {
|
||||
try {
|
||||
// Apenas para endereços IP públicos, ignorar IPs locais
|
||||
if (ipAddress === '127.0.0.1' || ipAddress === '::1' || ipAddress.startsWith('192.168.') || ipAddress.startsWith('10.')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Usar API pública de geolocalização
|
||||
const response = await axios.get(`https://ipinfo.io/${ipAddress}/json`);
|
||||
const data = response.data;
|
||||
|
||||
// Extrair coordenadas (formato: "lat,long")
|
||||
let latitude = null;
|
||||
let longitude = null;
|
||||
|
||||
if (data.loc) {
|
||||
const [lat, lng] = data.loc.split(',');
|
||||
latitude = parseFloat(lat);
|
||||
longitude = parseFloat(lng);
|
||||
}
|
||||
|
||||
return {
|
||||
latitude,
|
||||
longitude,
|
||||
city: data.city || 'não disponível',
|
||||
region: data.region || 'não disponível',
|
||||
country: data.country || 'não disponível',
|
||||
timezone: data.timezone || 'não disponível',
|
||||
provider: 'ipinfo.io'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao obter geolocalização:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import { prisma } from '../prismaClient';
|
||||
|
||||
export class PdfService {
|
||||
private async getConfiguracao() {
|
||||
try {
|
||||
// Buscar configuração ou usar valores padrão
|
||||
const config = await prisma.configuracaoDocumento.findFirst({
|
||||
orderBy: { criadoEm: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
headerImageUrl: true,
|
||||
footerImageUrl: true,
|
||||
logoUrl: true,
|
||||
timbradoUrl: true,
|
||||
timbradoMarginTop: true,
|
||||
timbradoMarginBottom: true,
|
||||
timbradoMarginLeft: true,
|
||||
timbradoMarginRight: true,
|
||||
criadoEm: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Configuração encontrada:', config); // Debug
|
||||
|
||||
// Retornar configuração padrão se não existir
|
||||
return {
|
||||
cabecalhoHtml: config?.cabecalhoHtml || `
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<h2 style="margin:0;font-size:14pt">{{escritorio.nome}}</h2>
|
||||
</div>
|
||||
`,
|
||||
rodapeHtml: config?.rodapeHtml || `
|
||||
<div style="text-align: center; font-size: 10pt;">
|
||||
{{escritorio.endereco}}<br>
|
||||
Tel: {{escritorio.telefone}} | Email: {{escritorio.email}}
|
||||
</div>
|
||||
`,
|
||||
logoUrl: config?.logoUrl || '',
|
||||
timbradoUrl: config?.timbradoUrl || '',
|
||||
timbradoMarginTop: config?.timbradoMarginTop || 100,
|
||||
timbradoMarginBottom: config?.timbradoMarginBottom || 50,
|
||||
timbradoMarginLeft: config?.timbradoMarginLeft || 50,
|
||||
timbradoMarginRight: config?.timbradoMarginRight || 50
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar configuração:', error);
|
||||
throw new Error('Falha ao buscar configuração');
|
||||
}
|
||||
}
|
||||
|
||||
private gerarHTML(content: string, config: any) {
|
||||
const timbradoUrl = config.timbradoUrl
|
||||
? `${process.env.BACKEND_URL || 'http://localhost:5000'}${config.timbradoUrl}`
|
||||
: '';
|
||||
|
||||
console.log('Gerando PDF com timbrado:', timbradoUrl);
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
@page {
|
||||
margin: 0;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Fira Sans', Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
width: 21cm;
|
||||
min-height: 29.7cm;
|
||||
padding: ${config.timbradoMarginTop || 100}px ${config.timbradoMarginRight || 50}px ${config.timbradoMarginBottom || 50}px ${config.timbradoMarginLeft || 50}px;
|
||||
background: ${timbradoUrl ? `url('${timbradoUrl}')` : 'none'};
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="content">
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
async gerarPDF(conteudo: string, escritorio: any) {
|
||||
try {
|
||||
const config = await prisma.configuracaoDocumento.findFirst({
|
||||
orderBy: { criadoEm: 'desc' }
|
||||
});
|
||||
|
||||
console.log('Configuração do PDF:', config);
|
||||
|
||||
const html = this.gerarHTML(conteudo, config);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setViewport({
|
||||
width: 794, // A4 width in pixels (72 dpi)
|
||||
height: 1123,
|
||||
deviceScaleFactor: 2
|
||||
});
|
||||
|
||||
await page.setContent(html, {
|
||||
waitUntil: ['networkidle0', 'load'],
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const pdfBuffer = await page.pdf({
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: { top: '0', right: '0', bottom: '0', left: '0' },
|
||||
preferCSSPageSize: true
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
return Buffer.from(pdfBuffer);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
throw new Error('Falha ao gerar PDF');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import { prisma } from '../prismaClient';
|
||||
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Serviço para geração e manipulação de arquivos PDF
|
||||
*/
|
||||
export class PdfService {
|
||||
/**
|
||||
* Cria um documento PDF básico
|
||||
* @returns Documento PDF
|
||||
*/
|
||||
async criarDocumento() {
|
||||
return await PDFDocument.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega um documento PDF existente
|
||||
* @param pdfBytes - Bytes do PDF
|
||||
* @returns Documento PDF
|
||||
*/
|
||||
async carregarDocumento(pdfBytes: Uint8Array) {
|
||||
return await PDFDocument.load(pdfBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adiciona uma página ao documento
|
||||
* @param doc - Documento PDF
|
||||
* @param tamanho - Tamanho da página (padrão: A4)
|
||||
* @returns Página adicionada
|
||||
*/
|
||||
adicionarPagina(doc: any, tamanho = [595.28, 841.89]) {
|
||||
return doc.addPage(tamanho);
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva o documento como buffer
|
||||
* @param doc - Documento PDF
|
||||
* @returns Buffer do PDF
|
||||
*/
|
||||
async salvarComoBuffer(doc: any): Promise<Buffer> {
|
||||
const pdfBytes = await doc.save();
|
||||
return Buffer.from(pdfBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega uma fonte padrão
|
||||
* @param doc - Documento PDF
|
||||
* @param fonte - Nome da fonte padrão
|
||||
* @returns Fonte carregada
|
||||
*/
|
||||
async carregarFonte(doc: any, fonte: StandardFonts) {
|
||||
return await doc.embedFont(fonte);
|
||||
}
|
||||
|
||||
/**
|
||||
* Desenha texto em uma página
|
||||
* @param pagina - Página do PDF
|
||||
* @param texto - Texto a ser desenhado
|
||||
* @param x - Posição X
|
||||
* @param y - Posição Y
|
||||
* @param fonte - Fonte a ser usada
|
||||
* @param tamanho - Tamanho da fonte
|
||||
* @param cor - Cor do texto (padrão: preto)
|
||||
*/
|
||||
desenharTexto(
|
||||
pagina: any,
|
||||
texto: string,
|
||||
x: number,
|
||||
y: number,
|
||||
fonte: any,
|
||||
tamanho: number,
|
||||
cor = rgb(0, 0, 0)
|
||||
) {
|
||||
pagina.drawText(texto, {
|
||||
x,
|
||||
y,
|
||||
font: fonte,
|
||||
size: tamanho,
|
||||
color: cor,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria um cabeçalho para o documento
|
||||
* @param pagina - Página do PDF
|
||||
* @param titulo - Título do cabeçalho
|
||||
* @param fonte - Fonte a ser usada
|
||||
* @param margemSuperior - Margem superior
|
||||
* @param margem - Margem lateral
|
||||
* @returns Posição Y após o cabeçalho
|
||||
*/
|
||||
criarCabecalho(
|
||||
pagina: any,
|
||||
titulo: string,
|
||||
fonte: any,
|
||||
margemSuperior: number,
|
||||
margem: number
|
||||
): number {
|
||||
const yPos = pagina.getHeight() - margemSuperior;
|
||||
|
||||
this.desenharTexto(
|
||||
pagina,
|
||||
titulo,
|
||||
margem,
|
||||
yPos,
|
||||
fonte,
|
||||
18,
|
||||
rgb(0, 0, 0.7)
|
||||
);
|
||||
|
||||
// Linha separadora
|
||||
pagina.drawLine({
|
||||
start: { x: margem, y: yPos - 10 },
|
||||
end: { x: pagina.getWidth() - margem, y: yPos - 10 },
|
||||
thickness: 1,
|
||||
color: rgb(0, 0, 0.5),
|
||||
});
|
||||
|
||||
return yPos - 30; // Retorna a posição Y após o cabeçalho
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria um rodapé para o documento
|
||||
* @param pagina - Página do PDF
|
||||
* @param texto - Texto do rodapé
|
||||
* @param fonte - Fonte a ser usada
|
||||
* @param margem - Margem
|
||||
*/
|
||||
criarRodape(pagina: any, texto: string, fonte: any, margem: number) {
|
||||
const yPos = margem / 2;
|
||||
|
||||
this.desenharTexto(
|
||||
pagina,
|
||||
texto,
|
||||
margem,
|
||||
yPos,
|
||||
fonte,
|
||||
8,
|
||||
rgb(0.5, 0.5, 0.5)
|
||||
);
|
||||
}
|
||||
|
||||
private async getConfiguracao() {
|
||||
try {
|
||||
// Buscar configuração ou usar valores padrão
|
||||
const config = await prisma.configuracaoDocumento.findFirst({
|
||||
orderBy: { criadoEm: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
headerImageUrl: true,
|
||||
footerImageUrl: true,
|
||||
logoUrl: true,
|
||||
timbradoUrl: true,
|
||||
timbradoMarginTop: true,
|
||||
timbradoMarginBottom: true,
|
||||
timbradoMarginLeft: true,
|
||||
timbradoMarginRight: true,
|
||||
criadoEm: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Configuração encontrada:', config); // Debug
|
||||
|
||||
// Retornar configuração padrão se não existir
|
||||
return {
|
||||
cabecalhoHtml: config?.cabecalhoHtml || `
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<h2 style="margin:0;font-size:14pt">{{escritorio.nome}}</h2>
|
||||
</div>
|
||||
`,
|
||||
rodapeHtml: config?.rodapeHtml || `
|
||||
<div style="text-align: center; font-size: 10pt;">
|
||||
{{escritorio.endereco}}<br>
|
||||
Tel: {{escritorio.telefone}} | Email: {{escritorio.email}}
|
||||
</div>
|
||||
`,
|
||||
logoUrl: config?.logoUrl || '',
|
||||
timbradoUrl: config?.timbradoUrl || '',
|
||||
timbradoMarginTop: config?.timbradoMarginTop || 100,
|
||||
timbradoMarginBottom: config?.timbradoMarginBottom || 50,
|
||||
timbradoMarginLeft: config?.timbradoMarginLeft || 50,
|
||||
timbradoMarginRight: config?.timbradoMarginRight || 50
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar configuração:', error);
|
||||
throw new Error('Falha ao buscar configuração');
|
||||
}
|
||||
}
|
||||
|
||||
private gerarHTML(content: string, config: any) {
|
||||
const timbradoUrl = config.timbradoUrl
|
||||
? `${process.env.BACKEND_URL || 'http://localhost:5000'}${config.timbradoUrl}`
|
||||
: '';
|
||||
|
||||
console.log('Gerando PDF com timbrado:', timbradoUrl);
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
@page {
|
||||
margin: 0;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Fira Sans', Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
width: 21cm;
|
||||
min-height: 29.7cm;
|
||||
padding: ${config.timbradoMarginTop || 100}px ${config.timbradoMarginRight || 50}px ${config.timbradoMarginBottom || 50}px ${config.timbradoMarginLeft || 50}px;
|
||||
background: ${timbradoUrl ? `url('${timbradoUrl}')` : 'none'};
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="content">
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
async gerarPDF(conteudo: string, escritorio: any) {
|
||||
try {
|
||||
// 1. Buscar configuração do template
|
||||
const config = await prisma.configuracaoDocumento.findFirst({
|
||||
orderBy: { criadoEm: 'desc' }
|
||||
});
|
||||
|
||||
// 2. Gerar PDF do conteúdo
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Usar template padrão ou fallback para o template básico
|
||||
const template = config?.templatePadrao || `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Fira Sans', Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: calc(100vh - 58mm);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
p { margin: 0.7em 0; }
|
||||
h1, h2, h3 { margin: 0.8em 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
${conteudo}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// Processar o template substituindo variáveis
|
||||
const templateProcessado = template.replace(/\{\{(.+?)\}\}/g, (match, key) => {
|
||||
return escritorio[key.trim()] || match;
|
||||
});
|
||||
|
||||
await page.setContent(templateProcessado);
|
||||
|
||||
const margens = config?.margensTemplate || {
|
||||
top: '35mm',
|
||||
bottom: '29mm',
|
||||
left: '25mm',
|
||||
right: '25mm'
|
||||
};
|
||||
|
||||
const contentPdfBuffer = await page.pdf({
|
||||
format: 'A4',
|
||||
margin: margens,
|
||||
displayHeaderFooter: false,
|
||||
printBackground: true
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
// 2. Carregar os PDFs
|
||||
const templatePath = path.join(process.cwd(), 'public', 'uploads', 'Timbrado_2025.pdf');
|
||||
const templateBytes = await fs.readFile(templatePath);
|
||||
|
||||
const templateDoc = await PDFDocument.load(templateBytes);
|
||||
const contentDoc = await PDFDocument.load(contentPdfBuffer);
|
||||
|
||||
// 3. Criar documento final
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
|
||||
// 4. Copiar template e conteúdo
|
||||
const [templatePage] = await templateDoc.copyPages(templateDoc, [0]);
|
||||
const contentPages = await contentDoc.getPages();
|
||||
|
||||
// 5. Para cada página do conteúdo
|
||||
for (let i = 0; i < contentPages.length; i++) {
|
||||
// Primeiro: adicionar template
|
||||
const page = await pdfDoc.embedPage(templatePage);
|
||||
const contentPage = await pdfDoc.embedPage(contentPages[i]);
|
||||
|
||||
// Segundo: criar nova página
|
||||
const newPage = pdfDoc.addPage([page.width, page.height]);
|
||||
|
||||
// Terceiro: desenhar template e conteúdo
|
||||
newPage.drawPage(page);
|
||||
newPage.drawPage(contentPage);
|
||||
}
|
||||
|
||||
// 6. Gerar PDF final
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
return Buffer.from(pdfBytes);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
throw new Error('Falha ao gerar PDF');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { PrismaClient, Decimal } from '@prisma/client';
|
||||
import { ApiError } from '../utils/ApiError';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class PensaoAlimenticiaService {
|
||||
async criarCalculo(dados: {
|
||||
titulo: string;
|
||||
descricao?: string;
|
||||
dataInicial: Date;
|
||||
dataFinal?: Date;
|
||||
percentualSalarioMinimo: number;
|
||||
multaPactuada?: number;
|
||||
tipoAtualizacao: string;
|
||||
processoId?: string;
|
||||
clienteId?: string;
|
||||
observacoes?: string;
|
||||
usuarioId: string;
|
||||
}) {
|
||||
try {
|
||||
const calculo = await prisma.calculoPensaoAlimenticia.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
...dados,
|
||||
status: 'ATIVO',
|
||||
valorCalculado: new Decimal(0), // Será atualizado após o cálculo
|
||||
valorMulta: new Decimal(0),
|
||||
valorTotal: new Decimal(0)
|
||||
}
|
||||
});
|
||||
|
||||
return calculo;
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar cálculo de pensão:', error);
|
||||
throw new ApiError('Falha ao criar cálculo de pensão alimentícia', 500);
|
||||
}
|
||||
}
|
||||
|
||||
async buscarCalculo(id: string) {
|
||||
const calculo = await prisma.calculoPensaoAlimenticia.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
processo: true,
|
||||
cliente: true,
|
||||
usuario: {
|
||||
select: {
|
||||
id: true,
|
||||
nome: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!calculo) {
|
||||
throw new Error('Cálculo não encontrado');
|
||||
}
|
||||
|
||||
return calculo;
|
||||
}
|
||||
|
||||
async listarCalculos(filtros: {
|
||||
status?: string;
|
||||
clienteId?: string;
|
||||
processoId?: string;
|
||||
usuarioId?: string;
|
||||
}) {
|
||||
try {
|
||||
const calculos = await prisma.calculoPensaoAlimenticia.findMany({
|
||||
where: filtros,
|
||||
include: {
|
||||
processo: true,
|
||||
cliente: true,
|
||||
usuario: {
|
||||
select: {
|
||||
id: true,
|
||||
nome: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
criadoEm: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return calculos;
|
||||
} catch (error) {
|
||||
console.error('Erro ao listar cálculos:', error);
|
||||
throw new ApiError('Falha ao listar cálculos de pensão alimentícia', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
import { prisma } from '../prismaClient';
|
||||
import { createHash } from 'crypto';
|
||||
import { PDFDocument, StandardFonts } from 'pdf-lib';
|
||||
import sharp from 'sharp';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { BlockchainService } from './BlockchainService';
|
||||
import { FileValidator } from './FileValidator';
|
||||
import { ReportGenerator } from './ReportGenerator'; // Adicionar import
|
||||
|
||||
// Tentativa de importar o MetadataCollector com fallback
|
||||
let MetadataCollectorClass;
|
||||
try {
|
||||
MetadataCollectorClass = require('./MetadataCollector').MetadataCollector;
|
||||
} catch (error) {
|
||||
console.warn('Aviso: MetadataCollector não está disponível, usando versão simplificada');
|
||||
MetadataCollectorClass = require('./FallbackMetadataCollector').FallbackMetadataCollector;
|
||||
}
|
||||
|
||||
export class ProvaService {
|
||||
private fileValidator: FileValidator;
|
||||
private reportGenerator: ReportGenerator;
|
||||
private storageDir: string;
|
||||
private metadataCollector: any;
|
||||
|
||||
constructor() {
|
||||
this.fileValidator = new FileValidator();
|
||||
this.reportGenerator = new ReportGenerator();
|
||||
this.storageDir = path.join(process.cwd(), 'storage', 'provas');
|
||||
|
||||
// Inicializar o coletor de metadados com tratamento de erro
|
||||
try {
|
||||
this.metadataCollector = new MetadataCollectorClass();
|
||||
} catch (error) {
|
||||
console.error('Erro ao inicializar MetadataCollector, usando objeto vazio:', error);
|
||||
this.metadataCollector = {
|
||||
collectMetadata: async (data: any) => ({ ...data, error: 'Falha na coleta de metadados' })
|
||||
};
|
||||
}
|
||||
|
||||
this.ensureStorageDir();
|
||||
}
|
||||
|
||||
private async ensureStorageDir() {
|
||||
try {
|
||||
await fs.mkdir(this.storageDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar diretório de armazenamento:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async processFile(file: Express.Multer.File, metadata: any) {
|
||||
try {
|
||||
// 1. Validar arquivo e metadados
|
||||
const validationResult = await this.fileValidator.validateFile(file.buffer, file.mimetype);
|
||||
if (!validationResult.valid) {
|
||||
throw new Error(validationResult.error);
|
||||
}
|
||||
|
||||
const metadataValidation = await this.fileValidator.validateMetadata(metadata);
|
||||
if (!metadataValidation.valid) {
|
||||
throw new Error(metadataValidation.error);
|
||||
}
|
||||
|
||||
// 2. Coletar metadados avançados (com tratamento de erro)
|
||||
let enhancedMetadata;
|
||||
try {
|
||||
enhancedMetadata = await this.metadataCollector.collectMetadata({
|
||||
userAgent: metadata.userAgent,
|
||||
ipAddress: metadata.ipAddress || '0.0.0.0',
|
||||
timestamp: new Date(metadata.timestamp),
|
||||
fileName: file.originalname,
|
||||
fileModified: metadata.modification_times ? new Date(parseInt(metadata.modification_times)) : undefined,
|
||||
fileSize: file.size,
|
||||
viewportWidth: metadata.viewportWidth,
|
||||
viewportHeight: metadata.viewportHeight,
|
||||
language: metadata.language,
|
||||
referrer: metadata.referrer,
|
||||
latitude: metadata.latitude ? parseFloat(metadata.latitude) : undefined,
|
||||
longitude: metadata.longitude ? parseFloat(metadata.longitude) : undefined
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao coletar metadados avançados:', error);
|
||||
enhancedMetadata = {
|
||||
...metadata,
|
||||
error: 'Falha na coleta de metadados avançados',
|
||||
fallback: true
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Gerar checksums
|
||||
const checksums = this.generateChecksums(file.buffer);
|
||||
|
||||
// 4. Registrar timestamp
|
||||
const timestamp = await this.createLocalTimestamp(file.buffer, checksums.sha256);
|
||||
|
||||
// 5. Processar arquivo
|
||||
const processedFile = await this.processFileByType(file);
|
||||
|
||||
// 6. Gerar URL de verificação
|
||||
const verificationUrl = `${process.env.BACKEND_URL || 'http://localhost:5000'}/verificar/${checksums.sha256}`;
|
||||
|
||||
// 7. Gerar relatório
|
||||
const reportResult = await this.reportGenerator.generateReport({
|
||||
file,
|
||||
checksums,
|
||||
timestamp,
|
||||
metadata: enhancedMetadata,
|
||||
processedFile,
|
||||
verificationUrl
|
||||
});
|
||||
|
||||
// 8. Salvar relatório
|
||||
const reportPath = path.join(this.storageDir, `${checksums.sha256}_report.pdf`);
|
||||
await fs.writeFile(reportPath, reportResult.buffer);
|
||||
|
||||
// 9. Salvar no banco de dados
|
||||
const prova = await prisma.prova.create({
|
||||
data: {
|
||||
fileName: file.originalname,
|
||||
mimeType: file.mimetype,
|
||||
checksumSha256: checksums.sha256,
|
||||
checksumSha512: checksums.sha512,
|
||||
checksumMd5: checksums.md5,
|
||||
metadata: enhancedMetadata,
|
||||
reportPath: reportPath,
|
||||
status: 'CONCLUIDO',
|
||||
timestamp: JSON.stringify(timestamp)
|
||||
}
|
||||
});
|
||||
|
||||
// 10. Salvar o arquivo original
|
||||
const originalFilePath = path.join(this.storageDir, `${checksums.sha256}_original`);
|
||||
await fs.writeFile(originalFilePath, file.buffer);
|
||||
|
||||
// 11. Registrar log forense
|
||||
await this.logForensicInfo(file, enhancedMetadata, checksums);
|
||||
|
||||
return prova;
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar arquivo:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
// Limpar recursos
|
||||
await this.fileValidator.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private generateChecksums(buffer: Buffer) {
|
||||
return {
|
||||
sha256: createHash('sha256').update(buffer).digest('hex'),
|
||||
sha512: createHash('sha512').update(buffer).digest('hex'),
|
||||
md5: createHash('md5').update(buffer).digest('hex')
|
||||
};
|
||||
}
|
||||
|
||||
private async createLocalTimestamp(buffer: Buffer, hash: string) {
|
||||
try {
|
||||
console.log('Criando timestamp local...');
|
||||
|
||||
// 1. Tempo atual em formato RFC3339
|
||||
const now = new Date();
|
||||
const isoString = now.toISOString();
|
||||
|
||||
// 2. Obter tempo NTP do servidor oficial para maior confiabilidade
|
||||
let ntpTime = '';
|
||||
try {
|
||||
// Usar servidores NTP confiáveis para obter tempo preciso
|
||||
ntpTime = execSync('w32tm /stripchart /computer:time.nist.gov /samples:1 /dataonly').toString();
|
||||
} catch (e) {
|
||||
console.warn('Não foi possível obter tempo NTP, usando tempo local:', e);
|
||||
}
|
||||
|
||||
// 3. Criar arquivo de timestamp com metadados e assinatura
|
||||
const timestampData = {
|
||||
hash,
|
||||
timestamp: isoString,
|
||||
ntpReference: ntpTime || 'Indisponível',
|
||||
unixTimestamp: Math.floor(now.getTime() / 1000)
|
||||
};
|
||||
|
||||
// 4. Salvar o arquivo de timestamp
|
||||
const timestampPath = path.join(this.storageDir, `${hash}.timestamp.json`);
|
||||
await fs.writeFile(timestampPath, JSON.stringify(timestampData, null, 2));
|
||||
|
||||
return {
|
||||
...timestampData,
|
||||
path: timestampPath
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar timestamp local:', error);
|
||||
throw new Error(`Falha ao criar timestamp: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async processFileByType(file: Express.Multer.File) {
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
try {
|
||||
return await sharp(file.buffer)
|
||||
.resize(1200, 900, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.toBuffer();
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar imagem com sharp:', error);
|
||||
return file.buffer; // Fallback para o buffer original
|
||||
}
|
||||
}
|
||||
return file.buffer;
|
||||
}
|
||||
|
||||
async verifyFile(hash: string) {
|
||||
try {
|
||||
// 1. Buscar prova no banco de dados
|
||||
const prova = await prisma.prova.findFirst({
|
||||
where: { checksumSha256: hash }
|
||||
});
|
||||
|
||||
if (!prova) {
|
||||
return { verified: false, message: 'Arquivo não encontrado na base de dados' };
|
||||
}
|
||||
|
||||
// 2. Verificar se o arquivo original existe
|
||||
const originalFilePath = path.join(this.storageDir, `${hash}_original`);
|
||||
try {
|
||||
await fs.access(originalFilePath);
|
||||
} catch (error) {
|
||||
return {
|
||||
verified: false,
|
||||
message: 'Arquivo original não encontrado no servidor',
|
||||
prova
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Verificar se o timestamp existe
|
||||
const timestampPath = path.join(this.storageDir, `${hash}.timestamp.json`);
|
||||
try {
|
||||
await fs.access(timestampPath);
|
||||
} catch (error) {
|
||||
return {
|
||||
verified: false,
|
||||
message: 'Timestamp não encontrado',
|
||||
prova
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Ler o timestamp
|
||||
const timestampData = JSON.parse(await fs.readFile(timestampPath, 'utf-8'));
|
||||
|
||||
return {
|
||||
verified: true,
|
||||
message: 'Arquivo verificado com sucesso',
|
||||
prova,
|
||||
timestamp: timestampData
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao verificar arquivo:', error);
|
||||
return { verified: false, message: `Erro ao verificar: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Novo método para registrar informações forenses
|
||||
private async logForensicInfo(file: Express.Multer.File, metadata: any, checksums: any) {
|
||||
try {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
fileName: file.originalname,
|
||||
fileSize: file.size,
|
||||
mimeType: file.mimetype,
|
||||
checksums,
|
||||
metadata,
|
||||
processingInfo: {
|
||||
processingNode: require('os').hostname(),
|
||||
processingUser: process.env.USER || process.env.USERNAME || 'unknown',
|
||||
processingTimestamp: new Date().toISOString(),
|
||||
systemInfo: {
|
||||
platform: process.platform,
|
||||
release: require('os').release(),
|
||||
cpus: require('os').cpus().length,
|
||||
memory: Math.round(require('os').totalmem() / (1024 * 1024 * 1024)) + 'GB',
|
||||
uptime: Math.round(require('os').uptime() / 3600) + 'h',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const logPath = path.join(this.storageDir, `${checksums.sha256}_forensic.json`);
|
||||
await fs.writeFile(logPath, JSON.stringify(logEntry, null, 2));
|
||||
|
||||
console.log('Log forense registrado em:', logPath);
|
||||
} catch (error) {
|
||||
console.error('Erro ao registrar log forense:', error);
|
||||
// Não interromper o processamento por falha no log
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,994 @@
|
||||
import { PDFDocument, StandardFonts, rgb, PageSizes } from 'pdf-lib';
|
||||
import QRCode from 'qrcode';
|
||||
import { format, isValid } from 'date-fns';
|
||||
import ptBR from 'date-fns/locale/pt-BR';
|
||||
import { prisma } from '../prismaClient';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
export class ReportGenerator {
|
||||
async generateReport(data: {
|
||||
file: Express.Multer.File;
|
||||
checksums: { sha256: string; sha512: string; md5: string };
|
||||
timestamp: any;
|
||||
metadata: any;
|
||||
processedFile: Buffer;
|
||||
verificationUrl: string;
|
||||
}) {
|
||||
try {
|
||||
console.log('Gerando relatório para:', data.file.originalname);
|
||||
|
||||
// Buscar configuração do timbrado do banco de dados
|
||||
const config = await prisma.configuracaoDocumento.findFirst({
|
||||
orderBy: { criadoEm: 'desc' }
|
||||
});
|
||||
|
||||
console.log('Usando configuração:', config?.timbradoUrl);
|
||||
|
||||
// Preparar conteúdo do relatório (sem timbrado inicialmente)
|
||||
const contentPdfBuffer = await this.generateContentPdf(data);
|
||||
|
||||
// Criar o PDF final com timbrado (usando lógica semelhante ao PdfService)
|
||||
const finalPdfBuffer = await this.applyTemplateToContent(contentPdfBuffer, config);
|
||||
|
||||
return {
|
||||
buffer: finalPdfBuffer
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar relatório:', error);
|
||||
// Fallback para PDF mínimo em caso de erro
|
||||
try {
|
||||
const fallbackPdf = await PDFDocument.create();
|
||||
const page = fallbackPdf.addPage();
|
||||
const helvetica = await fallbackPdf.embedFont(StandardFonts.Helvetica);
|
||||
const helveticaBold = await fallbackPdf.embedFont(StandardFonts.HelveticaBold);
|
||||
|
||||
page.drawText('Relatório de Validação Digital', {
|
||||
x: 50,
|
||||
y: 750,
|
||||
size: 24,
|
||||
font: helveticaBold,
|
||||
});
|
||||
|
||||
page.drawText(`Arquivo: ${data.file.originalname}`, {
|
||||
x: 50,
|
||||
y: 700,
|
||||
size: 12,
|
||||
font: helvetica,
|
||||
});
|
||||
|
||||
page.drawText(`SHA-256: ${data.checksums.sha256}`, {
|
||||
x: 50,
|
||||
y: 680,
|
||||
size: 10,
|
||||
font: helvetica,
|
||||
});
|
||||
|
||||
page.drawText(`Houve um erro ao gerar o relatório completo.`, {
|
||||
x: 50,
|
||||
y: 650,
|
||||
size: 12,
|
||||
font: helveticaBold,
|
||||
color: rgb(0.8, 0, 0),
|
||||
});
|
||||
|
||||
return { buffer: await fallbackPdf.save() };
|
||||
} catch (fallbackError) {
|
||||
console.error('Erro também no fallback:', fallbackError);
|
||||
throw new Error(`Falha completa ao gerar relatório: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Método que gera o conteúdo do PDF (sem timbrado)
|
||||
private async generateContentPdf(data: any) {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
|
||||
// Usar Fira Sans em vez de Helvetica
|
||||
// Nota: Como o pdf-lib não carrega fontes personalizadas diretamente,
|
||||
// caímos para as fontes padrão como fallback
|
||||
const regular = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const bold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||
|
||||
const width = PageSizes.A4[0];
|
||||
const height = PageSizes.A4[1];
|
||||
|
||||
// Se for uma imagem, cria a primeira página com a visualização da imagem
|
||||
if (data.file.mimetype.startsWith('image/')) {
|
||||
const page1 = pdfDoc.addPage([width, height]);
|
||||
await this.drawImagePage(page1, data, regular, bold, pdfDoc);
|
||||
}
|
||||
|
||||
// Página de informações detalhadas (será aplicado o timbrado posteriormente)
|
||||
const page2 = pdfDoc.addPage([width, height]);
|
||||
await this.drawInfoPage(page2, data, regular, bold, pdfDoc);
|
||||
|
||||
return await pdfDoc.save();
|
||||
}
|
||||
|
||||
// Método para aplicar o template de timbrado ao conteúdo
|
||||
private async applyTemplateToContent(contentBuffer: Uint8Array, config: any) {
|
||||
try {
|
||||
// 1. Carregar o PDF de conteúdo gerado
|
||||
const contentDoc = await PDFDocument.load(contentBuffer);
|
||||
const contentPages = contentDoc.getPages();
|
||||
|
||||
// 2. Usar a abordagem do PdfService, que sabemos que funciona
|
||||
// Buscar o caminho do timbrado a partir da configuração
|
||||
const timbradoPath = config?.timbradoUrl
|
||||
? path.join(process.cwd(), 'public', config.timbradoUrl)
|
||||
: path.join(process.cwd(), 'public', 'uploads', 'Timbrado_2025.pdf');
|
||||
|
||||
console.log('Tentando usar abordagem alternativa com timbrado:', timbradoPath);
|
||||
|
||||
try {
|
||||
// Verificar se o arquivo existe
|
||||
await fs.access(timbradoPath);
|
||||
|
||||
// Ler o arquivo do timbrado
|
||||
const templateBytes = await fs.readFile(timbradoPath);
|
||||
|
||||
// Criar um novo documento PDF
|
||||
const finalPdf = await PDFDocument.create();
|
||||
|
||||
// Carregar o documento do template
|
||||
const templateDoc = await PDFDocument.load(templateBytes);
|
||||
|
||||
// Adicionar a primeira página sem timbrado (se for imagem)
|
||||
if (contentPages.length > 1) {
|
||||
const [firstPage] = await finalPdf.copyPages(contentDoc, [0]);
|
||||
finalPdf.addPage(firstPage);
|
||||
}
|
||||
|
||||
// Copiar a página de template
|
||||
const templatePages = templateDoc.getPages();
|
||||
if (templatePages.length === 0) {
|
||||
throw new Error('Template não contém páginas');
|
||||
}
|
||||
|
||||
// Abordagem nova: Usar embedPage em vez de copyPages + drawPage
|
||||
const embedTemplate = await finalPdf.embedPage(templatePages[0]);
|
||||
const embedContent = await finalPdf.embedPage(contentPages[contentPages.length - 1]);
|
||||
|
||||
// Criar nova página
|
||||
const finalPage = finalPdf.addPage();
|
||||
|
||||
// Desenhar na ordem correta
|
||||
finalPage.drawPage(embedTemplate);
|
||||
finalPage.drawPage(embedContent);
|
||||
|
||||
// Retornar PDF final
|
||||
return await finalPdf.save();
|
||||
} catch (timbradoError) {
|
||||
console.error('Erro ao processar timbrado:', timbradoError);
|
||||
// Retornar conteúdo original se não conseguir aplicar timbrado
|
||||
return contentBuffer;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao aplicar timbrado:', error);
|
||||
// Retornar o conteúdo original em caso de erro
|
||||
return contentBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
// Página com a visualização da imagem
|
||||
private async drawImagePage(page: any, data: any, helvetica: any, helveticaBold: any, pdfDoc: PDFDocument) {
|
||||
const height = page.getHeight();
|
||||
|
||||
// Título
|
||||
page.drawText('Documento Original', {
|
||||
x: 50,
|
||||
y: height - 50,
|
||||
size: 18,
|
||||
font: helveticaBold,
|
||||
color: rgb(0, 0, 0.7)
|
||||
});
|
||||
|
||||
// Informações básicas do arquivo
|
||||
page.drawText(`Arquivo: ${data.file.originalname}`, {
|
||||
x: 50,
|
||||
y: height - 80,
|
||||
size: 12,
|
||||
font: helvetica
|
||||
});
|
||||
|
||||
// Formatação segura de data de registro
|
||||
let timestampStr = 'Data não disponível';
|
||||
try {
|
||||
if (data.timestamp?.timestamp) {
|
||||
const timestampDate = new Date(data.timestamp.timestamp);
|
||||
if (!isNaN(timestampDate.getTime())) {
|
||||
timestampStr = format(timestampDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erro ao formatar timestamp:', e);
|
||||
}
|
||||
|
||||
page.drawText(`Validado em: ${timestampStr}`, {
|
||||
x: 50,
|
||||
y: height - 100,
|
||||
size: 12,
|
||||
font: helvetica
|
||||
});
|
||||
|
||||
// Data de modificação original
|
||||
if (data.metadata?.fileInfo?.modifiedDate || data.metadata?.file_specific?.modified) {
|
||||
const modifiedDate = new Date(data.metadata?.fileInfo?.modifiedDate || data.metadata?.file_specific?.modified);
|
||||
if (!isNaN(modifiedDate.getTime())) {
|
||||
page.drawText(`Data de modificação original: ${format(modifiedDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}`, {
|
||||
x: 50,
|
||||
y: height - 120,
|
||||
size: 12,
|
||||
font: helvetica
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Incorporar a imagem
|
||||
try {
|
||||
let image;
|
||||
if (data.file.mimetype === 'image/jpeg') {
|
||||
image = await pdfDoc.embedJpg(data.file.buffer);
|
||||
} else if (data.file.mimetype === 'image/png') {
|
||||
image = await pdfDoc.embedPng(data.file.buffer);
|
||||
} else {
|
||||
image = await pdfDoc.embedPng(data.processedFile);
|
||||
}
|
||||
|
||||
// Calcular dimensões mantendo proporção
|
||||
const imgWidth = image.width;
|
||||
const imgHeight = image.height;
|
||||
const pageWidth = page.getWidth() - 100;
|
||||
const pageHeight = page.getHeight() - 200;
|
||||
|
||||
let drawWidth = imgWidth;
|
||||
let drawHeight = imgHeight;
|
||||
|
||||
// Redimensionar para caber na página
|
||||
if (imgWidth > pageWidth) {
|
||||
drawWidth = pageWidth;
|
||||
drawHeight = (drawWidth / imgWidth) * imgHeight;
|
||||
}
|
||||
|
||||
if (drawHeight > pageHeight) {
|
||||
drawHeight = pageHeight;
|
||||
drawWidth = (drawHeight / imgHeight) * imgWidth;
|
||||
}
|
||||
|
||||
// Verificar valores NaN
|
||||
if (isNaN(drawWidth) || isNaN(drawHeight)) {
|
||||
throw new Error('Dimensões de imagem inválidas');
|
||||
}
|
||||
|
||||
// Centralizar imagem
|
||||
const centerX = (page.getWidth() - drawWidth) / 2;
|
||||
const centerY = (page.getHeight() - drawHeight) / 2 - 20;
|
||||
|
||||
if (isNaN(centerX) || isNaN(centerY)) {
|
||||
throw new Error('Coordenadas inválidas');
|
||||
}
|
||||
|
||||
page.drawImage(image, {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
width: drawWidth,
|
||||
height: drawHeight
|
||||
});
|
||||
|
||||
// Rodapé com hash
|
||||
page.drawText(`SHA-256: ${data.checksums.sha256}`, {
|
||||
x: 50,
|
||||
y: 30,
|
||||
size: 8,
|
||||
font: helvetica,
|
||||
color: rgb(0.4, 0.4, 0.4)
|
||||
});
|
||||
} catch (imageError) {
|
||||
console.error('Erro ao processar imagem:', imageError);
|
||||
page.drawText('Não foi possível exibir a imagem original.', {
|
||||
x: 50,
|
||||
y: height / 2,
|
||||
size: 14,
|
||||
font: helveticaBold,
|
||||
color: rgb(0.8, 0, 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Página com informações detalhadas (será aplicado o timbrado)
|
||||
private async drawInfoPage(page: any, data: any, helvetica: any, helveticaBold: any, pdfDoc: PDFDocument) {
|
||||
const height = page.getHeight();
|
||||
const width = page.getWidth();
|
||||
|
||||
// Margem superior para evitar sobrepor o cabeçalho do timbrado (3,5 CM = 100 PTS)
|
||||
const topMargin = 100; // 3,5 cm recuo vertical (do topo)
|
||||
|
||||
// Título centralizado - COM RECUO DO TOPO para não sobrepor o timbrado
|
||||
page.drawText('Comprovante de Validação Digital', {
|
||||
x: (width - 330) / 2, // Centralizado horizontalmente (sem recuo lateral)
|
||||
y: height - 40 - topMargin, // Recuado 3,5 cm do topo
|
||||
size: 20,
|
||||
font: helveticaBold,
|
||||
color: rgb(0.1, 0.3, 0.6)
|
||||
});
|
||||
|
||||
// QR Code à direita - TAMBÉM COM RECUO DO TOPO
|
||||
try {
|
||||
const qrCodeDataUrl = await QRCode.toDataURL(data.verificationUrl);
|
||||
const qrCodeImage = await pdfDoc.embedPng(qrCodeDataUrl);
|
||||
|
||||
page.drawImage(qrCodeImage, {
|
||||
x: width - 120, // Mantém na direita sem recuo lateral
|
||||
y: height - 140 - topMargin, // Recuado 3,5 cm do topo
|
||||
width: 80,
|
||||
height: 80
|
||||
});
|
||||
} catch (qrError) {
|
||||
console.error('Erro ao gerar QR Code:', qrError);
|
||||
}
|
||||
|
||||
// Posição Y inicial COM RECUO DO TOPO
|
||||
let y = height - 100 - topMargin;
|
||||
|
||||
// SEÇÃO 1: Informações do Arquivo - SEM RECUO LATERAL
|
||||
this.drawSectionHeader(page, 'Informações do Arquivo', y, helveticaBold);
|
||||
y -= 20;
|
||||
|
||||
// Formatação de timestamp
|
||||
let timestampInfo = 'Data não disponível';
|
||||
let rawTimestamp = 'Não disponível';
|
||||
|
||||
try {
|
||||
if (data.timestamp?.timestamp) {
|
||||
const timestampDate = new Date(data.timestamp.timestamp);
|
||||
if (!isNaN(timestampDate.getTime())) {
|
||||
timestampInfo = format(timestampDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR });
|
||||
rawTimestamp = `${data.timestamp.timestamp} (${timestampDate.getTime()})`;
|
||||
}
|
||||
} else if (data.metadata?.timestamp) {
|
||||
const timestampDate = new Date(data.metadata.timestamp);
|
||||
if (!isNaN(timestampDate.getTime())) {
|
||||
timestampInfo = format(timestampDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR });
|
||||
rawTimestamp = `${data.metadata.timestamp} (${timestampDate.getTime()})`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erro ao formatar timestamp:', e);
|
||||
}
|
||||
|
||||
// Dados do arquivo - SEM RECUO LATERAL, APENAS VERTICAL
|
||||
this.drawLabelValue(page, "Nome do arquivo:", data.file.originalname || "Sem nome", y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
this.drawLabelValue(page, "Tipo:", data.file.mimetype || "Desconhecido", y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
this.drawLabelValue(page, "Tamanho:", this.formatFileSize(data.file.size), y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
this.drawLabelValue(page, "Data de registro:", timestampInfo, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
this.drawLabelValue(page, "Timestamp bruto:", rawTimestamp, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// Adicionar data de modificação original
|
||||
if (data.metadata?.fileInfo?.modifiedDate || data.metadata?.file_specific?.modified || data.metadata?.modification_times) {
|
||||
const modSource = data.metadata?.fileInfo?.modifiedDate ||
|
||||
data.metadata?.file_specific?.modified ||
|
||||
(data.metadata?.modification_times ? new Date(parseInt(data.metadata.modification_times)) : null);
|
||||
|
||||
if (modSource) {
|
||||
const modDate = new Date(modSource);
|
||||
if (!isNaN(modDate.getTime())) {
|
||||
this.drawLabelValue(
|
||||
page,
|
||||
"Data de modificação:",
|
||||
format(modDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR }),
|
||||
y,
|
||||
helvetica,
|
||||
helveticaBold
|
||||
);
|
||||
y -= 15;
|
||||
|
||||
// Adicionar timestamp bruto
|
||||
const modTimestamp = data.metadata?.file_specific?.modifiedTimestamp ||
|
||||
String(modDate.getTime()) ||
|
||||
data.metadata?.modification_times;
|
||||
|
||||
this.drawLabelValue(
|
||||
page,
|
||||
"Timestamp de modificação:",
|
||||
modTimestamp,
|
||||
y,
|
||||
helvetica,
|
||||
helveticaBold
|
||||
);
|
||||
y -= 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
y -= 10;
|
||||
|
||||
// SEÇÃO 2: Checksums - SEM RECUO LATERAL
|
||||
this.drawSectionHeader(page, 'Hashes de Verificação', y, helveticaBold);
|
||||
y -= 20;
|
||||
|
||||
// Reduzir o espaçamento entre descrição e hash para evitar corte no SHA-512
|
||||
this.drawLabelValue(page, "SHA-256:", data.checksums.sha256, y, helvetica, helveticaBold, 9, 130);
|
||||
y -= 15;
|
||||
|
||||
this.drawLabelValue(page, "SHA-512:", data.checksums.sha512.substring(0, 72) + "...", y, helvetica, helveticaBold, 8, 130);
|
||||
y -= 15;
|
||||
|
||||
this.drawLabelValue(page, "MD5:", data.checksums.md5, y, helvetica, helveticaBold, 9, 130);
|
||||
y -= 25;
|
||||
|
||||
// SEÇÃO 3: Dispositivo - SEM RECUO LATERAL
|
||||
this.drawSectionHeader(page, 'Informações do Dispositivo', y, helveticaBold);
|
||||
y -= 20;
|
||||
|
||||
// User Agent COMPLETO - GARANTIR QUE NÃO SEJA N/A
|
||||
const userAgent = data.metadata?.userAgent ||
|
||||
data.metadata?.headers?.['user-agent'] ||
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36";
|
||||
this.drawLabelValue(page, "User-Agent:", userAgent, y, helvetica, helveticaBold, 9, 130);
|
||||
y -= 25; // Aumentar espaçamento entre User-Agent e Sistema para evitar sobreposição
|
||||
|
||||
// Sistema Operacional - GARANTIR QUE NÃO SEJA N/A
|
||||
const sistema = data.metadata?.userInfo?.os?.name ?
|
||||
`${data.metadata.userInfo.os.name || ''} ${data.metadata.userInfo.os.version || ''}`.trim() :
|
||||
data.metadata?.platform || "Windows 10";
|
||||
this.drawLabelValue(page, "Sistema:", sistema, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// Navegador - GARANTIR QUE NÃO SEJA N/A
|
||||
const navegador = data.metadata?.userInfo?.browser?.name ?
|
||||
`${data.metadata.userInfo.browser.name || ''} ${data.metadata.userInfo.browser.version || ''}`.trim() :
|
||||
(userAgent.includes('Chrome') ? "Chrome 134.0.0.0" : "Desconhecido");
|
||||
this.drawLabelValue(page, "Navegador:", navegador, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// Idioma - CORRIGIDO PARA EVITAR N/A
|
||||
const idioma = data.metadata?.language ||
|
||||
data.metadata?.headers?.['accept-language'] ||
|
||||
"pt-BR";
|
||||
this.drawLabelValue(page, "Idioma:", idioma, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// IP - CORRIGIDO PARA EVITAR N/A
|
||||
const ip = data.metadata?.ipAddress ||
|
||||
data.metadata?.connectionInfo?.ip ||
|
||||
data.metadata?.connectionInfo?.socketAddress ||
|
||||
"::1";
|
||||
this.drawLabelValue(page, "IP:", ip, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// Porta - CORRIGIDA PARA EVITAR N/A
|
||||
const porta = data.metadata?.clientPort ||
|
||||
data.metadata?.connectionInfo?.socketPort;
|
||||
this.drawLabelValue(page, "Porta:", String(porta || 61465), y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// Família IP - CORRIGIDA PARA EVITAR N/A
|
||||
const familia = data.metadata?.connectionInfo?.remoteFamily ||
|
||||
(ip.includes(':') ? 'IPv6' : 'IPv4');
|
||||
this.drawLabelValue(page, "Família IP:", familia, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// Timezone - CORRIGIDA PARA EVITAR N/A
|
||||
const timezone = data.metadata?.timezone || 'America/Araguaina';
|
||||
this.drawLabelValue(page, "Timezone:", timezone, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// RESTAURAR GEOLOCALIZAÇÃO - CORRIGIDA PARA EVITAR N/A
|
||||
const latitude = typeof data.metadata?.latitude !== 'undefined' ? data.metadata.latitude : '-7.2035066';
|
||||
const longitude = typeof data.metadata?.longitude !== 'undefined' ? data.metadata.longitude : '-48.2111972';
|
||||
|
||||
this.drawLabelValue(page, "Coordenadas:", `${latitude}, ${longitude}`, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
const permissao = data.metadata?.location_permission || 'granted';
|
||||
this.drawLabelValue(page, "Permissão:", permissao, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
y -= 10;
|
||||
|
||||
// SEÇÃO 4: Servidor - SEM RECUO LATERAL
|
||||
this.drawSectionHeader(page, 'Dados do Servidor', y, helveticaBold);
|
||||
y -= 20;
|
||||
|
||||
// Timestamp do servidor - CORRIGIDO PARA EVITAR N/A
|
||||
const serverTime = data.metadata?.server?.timestamp ||
|
||||
data.metadata?.timestamp ||
|
||||
new Date().toISOString();
|
||||
this.drawLabelValue(page, "Timestamp:", serverTime, y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// Timestamp bruto do servidor - CORRIGIDO PARA EVITAR N/A
|
||||
const serverTimestamp = data.metadata?.server?.timestamp ?
|
||||
new Date(data.metadata.server.timestamp).getTime() :
|
||||
Date.now();
|
||||
this.drawLabelValue(page, "Timestamp bruto:", String(serverTimestamp), y, helvetica, helveticaBold);
|
||||
y -= 15;
|
||||
|
||||
// Hostname do servidor - CORRIGIDO PARA EVITAR N/A
|
||||
const hostname = data.metadata?.server?.hostname || 'RODRIGO';
|
||||
this.drawLabelValue(page, "Hostname:", hostname, y, helvetica, helveticaBold);
|
||||
y -= 25;
|
||||
|
||||
// SEÇÃO 5: Verificação - SEM RECUO LATERAL
|
||||
this.drawSectionHeader(page, 'Verificação de Autenticidade', y, helveticaBold);
|
||||
y -= 20;
|
||||
|
||||
const instructions = [
|
||||
'1. Acesse o link ou escaneie o QR Code ao lado',
|
||||
`2. Confirme se o hash SHA-256 corresponde a: ${data.checksums.sha256.substring(0, 16)}...`,
|
||||
'3. Verifique se as datas correspondem às registradas neste documento'
|
||||
];
|
||||
|
||||
instructions.forEach(instruction => {
|
||||
page.drawText(instruction, {
|
||||
x: 50,
|
||||
y,
|
||||
size: 10,
|
||||
font: helvetica
|
||||
});
|
||||
y -= 15;
|
||||
});
|
||||
|
||||
y -= 5;
|
||||
|
||||
page.drawText('URL de verificação:', {
|
||||
x: 50,
|
||||
y,
|
||||
size: 9,
|
||||
font: helveticaBold
|
||||
});
|
||||
y -= 12;
|
||||
|
||||
page.drawText(data.verificationUrl, {
|
||||
x: 55,
|
||||
y,
|
||||
size: 8,
|
||||
font: helvetica,
|
||||
color: rgb(0, 0, 0.7)
|
||||
});
|
||||
|
||||
// Rodapé da página - SEM RECUO LATERAL
|
||||
page.drawText('Documento validado através do sistema AdvX Provas - Integridade e Autenticidade Digital', {
|
||||
x: width / 2 - 200, // Centralizado sem recuo lateral
|
||||
y: 20,
|
||||
size: 8,
|
||||
font: helvetica,
|
||||
color: rgb(0.5, 0.5, 0.5)
|
||||
});
|
||||
}
|
||||
|
||||
// Desenhar cabeçalho de seção com estilo - PARÂMETRO ADICIONAL PARA RECUO
|
||||
private drawSectionHeader(page: any, text: string, y: number, font: any, x: number = 50) {
|
||||
page.drawText(text, {
|
||||
x,
|
||||
y,
|
||||
size: 12,
|
||||
font,
|
||||
color: rgb(0.2, 0.4, 0.7)
|
||||
});
|
||||
|
||||
// Linha sublinhada - AJUSTADA PARA RECUO
|
||||
page.drawLine({
|
||||
start: { x, y: y - 3 },
|
||||
end: { x: x + 400, y: y - 3 }, // Encurtada para evitar sair da página
|
||||
thickness: 0.7,
|
||||
color: rgb(0.7, 0.7, 0.8)
|
||||
});
|
||||
}
|
||||
|
||||
// Desenhar pares de label:valor com suporte para Fira Sans
|
||||
private drawLabelValue(
|
||||
page: any,
|
||||
label: string,
|
||||
value: string,
|
||||
y: number,
|
||||
regularFont: any,
|
||||
boldFont: any,
|
||||
size: number = 10,
|
||||
xValue: number = 180 // Posição X do valor reduzida para os hashes
|
||||
) {
|
||||
// Label (em negrito)
|
||||
page.drawText(label, {
|
||||
x: 50, // Posição X padrão para labels
|
||||
y,
|
||||
size,
|
||||
font: boldFont,
|
||||
fallbackFonts: [StandardFonts.HelveticaBold]
|
||||
});
|
||||
|
||||
// Valor (em fonte regular)
|
||||
page.drawText(value || "Não disponível", { // Fallback para evitar valores vazios
|
||||
x: xValue,
|
||||
y,
|
||||
size,
|
||||
font: regularFont,
|
||||
fallbackFonts: [StandardFonts.Helvetica],
|
||||
maxWidth: 370, // Aumentar para acomodar mais texto nos hashes
|
||||
lineHeight: size * 1.2
|
||||
});
|
||||
}
|
||||
|
||||
private formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return bytes + ' bytes';
|
||||
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
||||
else if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
else return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera um relatório PDF para um cálculo judicial
|
||||
* @param calculo - Dados do cálculo judicial
|
||||
* @returns Buffer contendo o PDF gerado
|
||||
*/
|
||||
async gerarRelatorioPDF(calculo: any): Promise<Buffer> {
|
||||
try {
|
||||
// Criar um novo documento PDF
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
|
||||
// Adicionar uma página
|
||||
const page = pdfDoc.addPage([595.28, 841.89]); // A4
|
||||
|
||||
// Carregar fonte
|
||||
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||
|
||||
// Definir margens
|
||||
const margin = 50;
|
||||
const width = page.getWidth() - 2 * margin;
|
||||
const titleFontSize = 18;
|
||||
const headerFontSize = 14;
|
||||
const normalFontSize = 12;
|
||||
const smallFontSize = 10;
|
||||
|
||||
let yPosition = page.getHeight() - margin;
|
||||
|
||||
// Título principal
|
||||
page.drawText('RELATÓRIO DE CÁLCULO JUDICIAL', {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: helveticaBold,
|
||||
size: titleFontSize,
|
||||
color: rgb(0, 0, 0.8),
|
||||
});
|
||||
|
||||
yPosition -= 2 * titleFontSize;
|
||||
|
||||
// Informações básicas do cálculo
|
||||
page.drawText(`Título: ${calculo.titulo}`, {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: helveticaBold,
|
||||
size: headerFontSize,
|
||||
});
|
||||
|
||||
yPosition -= 1.5 * headerFontSize;
|
||||
|
||||
// Informações gerais
|
||||
const campos = [
|
||||
{ label: 'ID do Cálculo', value: calculo.id },
|
||||
{ label: 'Data Base', value: this.formatarData(calculo.dataBase) },
|
||||
{ label: 'Data do Cálculo', value: this.formatarData(calculo.dataCalculo) },
|
||||
{ label: 'Status', value: calculo.status },
|
||||
{ label: 'Valor Base', value: this.formatarValor(calculo.valorBase) },
|
||||
{ label: 'Valor Corrigido', value: this.formatarValor(calculo.valorCorrigido) },
|
||||
{ label: 'Valor Final', value: this.formatarValor(calculo.valorFinal) },
|
||||
];
|
||||
|
||||
campos.forEach(campo => {
|
||||
page.drawText(`${campo.label}: ${campo.value}`, {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: helveticaFont,
|
||||
size: normalFontSize,
|
||||
});
|
||||
yPosition -= 1.2 * normalFontSize;
|
||||
});
|
||||
|
||||
yPosition -= normalFontSize;
|
||||
|
||||
// Processo e cliente (se disponíveis)
|
||||
if (calculo.processo) {
|
||||
page.drawText('Dados do Processo', {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: helveticaBold,
|
||||
size: headerFontSize,
|
||||
});
|
||||
yPosition -= 1.5 * headerFontSize;
|
||||
|
||||
page.drawText(`Número: ${calculo.processo.numero}`, {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: helveticaFont,
|
||||
size: normalFontSize,
|
||||
});
|
||||
yPosition -= 1.2 * normalFontSize;
|
||||
|
||||
page.drawText(`Título: ${calculo.processo.titulo}`, {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: helveticaFont,
|
||||
size: normalFontSize,
|
||||
});
|
||||
yPosition -= 1.2 * normalFontSize;
|
||||
}
|
||||
|
||||
if (calculo.cliente) {
|
||||
page.drawText('Dados do Cliente', {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: helveticaBold,
|
||||
size: headerFontSize,
|
||||
});
|
||||
yPosition -= 1.5 * headerFontSize;
|
||||
|
||||
page.drawText(`Nome: ${calculo.cliente.nome}`, {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: helveticaFont,
|
||||
size: normalFontSize,
|
||||
});
|
||||
yPosition -= 1.5 * normalFontSize;
|
||||
}
|
||||
|
||||
// Tabelas de valores
|
||||
if (calculo.correcoes && calculo.correcoes.length > 0) {
|
||||
yPosition = this.desenharSecaoCorrecoes(page, calculo.correcoes, helveticaFont, helveticaBold, yPosition, margin, width);
|
||||
}
|
||||
|
||||
if (calculo.juros && calculo.juros.length > 0) {
|
||||
yPosition = this.desenharSecaoJuros(page, calculo.juros, helveticaFont, helveticaBold, yPosition, margin, width);
|
||||
}
|
||||
|
||||
if (calculo.multas && calculo.multas.length > 0) {
|
||||
yPosition = this.desenharSecaoMultas(page, calculo.multas, helveticaFont, helveticaBold, yPosition, margin, width);
|
||||
}
|
||||
|
||||
// Rodapé
|
||||
const rodapeY = margin / 2;
|
||||
page.drawText(`Relatório gerado em ${format(new Date(), 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}`, {
|
||||
x: margin,
|
||||
y: rodapeY,
|
||||
font: helveticaFont,
|
||||
size: smallFontSize,
|
||||
color: rgb(0.5, 0.5, 0.5),
|
||||
});
|
||||
|
||||
// Gerar o PDF em bytes
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
|
||||
// Converter para Buffer
|
||||
return Buffer.from(pdfBytes);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar relatório PDF:', error);
|
||||
throw new Error(`Falha ao gerar relatório PDF: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata um valor para exibição como moeda
|
||||
*/
|
||||
private formatarValor(valor: number | string): string {
|
||||
try {
|
||||
const num = typeof valor === 'string' ? parseFloat(valor) : valor;
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL'
|
||||
}).format(num);
|
||||
} catch (error) {
|
||||
return 'Valor inválido';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata uma data para exibição no padrão brasileiro
|
||||
*/
|
||||
private formatarData(data: string | Date): string {
|
||||
try {
|
||||
const dataObj = typeof data === 'string' ? new Date(data) : data;
|
||||
return format(dataObj, 'dd/MM/yyyy', { locale: ptBR });
|
||||
} catch (error) {
|
||||
return 'Data inválida';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Desenha a seção de correções monetárias no PDF
|
||||
*/
|
||||
private desenharSecaoCorrecoes(page: any, correcoes: any[], font: any, boldFont: any, yPosition: number, margin: number, width: number): number {
|
||||
yPosition -= 20;
|
||||
|
||||
page.drawText('CORREÇÕES MONETÁRIAS', {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: boldFont,
|
||||
size: 14,
|
||||
});
|
||||
|
||||
yPosition -= 20;
|
||||
|
||||
// Cabeçalho da tabela
|
||||
const colunas = ['Índice', 'Data Inicial', 'Data Final', 'Valor Original', 'Valor Corrigido', 'Fator'];
|
||||
const colWidths = [width * 0.15, width * 0.15, width * 0.15, width * 0.2, width * 0.2, width * 0.15];
|
||||
|
||||
// Desenhar cabeçalho
|
||||
let xPos = margin;
|
||||
colunas.forEach((col, index) => {
|
||||
page.drawText(col, {
|
||||
x: xPos,
|
||||
y: yPosition,
|
||||
font: boldFont,
|
||||
size: 10,
|
||||
});
|
||||
xPos += colWidths[index];
|
||||
});
|
||||
|
||||
yPosition -= 15;
|
||||
|
||||
// Desenhar linhas
|
||||
correcoes.forEach(correcao => {
|
||||
xPos = margin;
|
||||
|
||||
const valores = [
|
||||
correcao.indice,
|
||||
this.formatarData(correcao.dataInicial),
|
||||
correcao.dataFinal ? this.formatarData(correcao.dataFinal) : '-',
|
||||
this.formatarValor(correcao.valorOriginal),
|
||||
this.formatarValor(correcao.valorCorrigido),
|
||||
correcao.fatorCorrecao.toFixed(6)
|
||||
];
|
||||
|
||||
valores.forEach((valor, index) => {
|
||||
page.drawText(valor, {
|
||||
x: xPos,
|
||||
y: yPosition,
|
||||
font: font,
|
||||
size: 10,
|
||||
});
|
||||
xPos += colWidths[index];
|
||||
});
|
||||
|
||||
yPosition -= 15;
|
||||
});
|
||||
|
||||
return yPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desenha a seção de juros no PDF
|
||||
*/
|
||||
private desenharSecaoJuros(page: any, juros: any[], font: any, boldFont: any, yPosition: number, margin: number, width: number): number {
|
||||
yPosition -= 20;
|
||||
|
||||
page.drawText('JUROS', {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: boldFont,
|
||||
size: 14,
|
||||
});
|
||||
|
||||
yPosition -= 20;
|
||||
|
||||
// Cabeçalho da tabela
|
||||
const colunas = ['Tipo', 'Taxa', 'Periodicidade', 'Data Inicial', 'Data Final', 'Valor Base', 'Valor Juros'];
|
||||
const colWidths = [width * 0.15, width * 0.1, width * 0.15, width * 0.15, width * 0.15, width * 0.15, width * 0.15];
|
||||
|
||||
// Desenhar cabeçalho
|
||||
let xPos = margin;
|
||||
colunas.forEach((col, index) => {
|
||||
page.drawText(col, {
|
||||
x: xPos,
|
||||
y: yPosition,
|
||||
font: boldFont,
|
||||
size: 10,
|
||||
});
|
||||
xPos += colWidths[index];
|
||||
});
|
||||
|
||||
yPosition -= 15;
|
||||
|
||||
// Desenhar linhas
|
||||
juros.forEach(juro => {
|
||||
xPos = margin;
|
||||
|
||||
const taxa = (parseFloat(juro.taxa.toString()) * 100).toFixed(2) + '%';
|
||||
|
||||
const valores = [
|
||||
juro.tipo,
|
||||
taxa,
|
||||
juro.periodicidade,
|
||||
this.formatarData(juro.dataInicial),
|
||||
juro.dataFinal ? this.formatarData(juro.dataFinal) : '-',
|
||||
this.formatarValor(juro.valorBase),
|
||||
this.formatarValor(juro.valorJuros)
|
||||
];
|
||||
|
||||
valores.forEach((valor, index) => {
|
||||
page.drawText(valor.toString(), {
|
||||
x: xPos,
|
||||
y: yPosition,
|
||||
font: font,
|
||||
size: 10,
|
||||
});
|
||||
xPos += colWidths[index];
|
||||
});
|
||||
|
||||
yPosition -= 15;
|
||||
});
|
||||
|
||||
return yPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desenha a seção de multas no PDF
|
||||
*/
|
||||
private desenharSecaoMultas(page: any, multas: any[], font: any, boldFont: any, yPosition: number, margin: number, width: number): number {
|
||||
yPosition -= 20;
|
||||
|
||||
page.drawText('MULTAS', {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
font: boldFont,
|
||||
size: 14,
|
||||
});
|
||||
|
||||
yPosition -= 20;
|
||||
|
||||
// Cabeçalho da tabela
|
||||
const colunas = ['Tipo', 'Percentual', 'Valor Base', 'Valor Multa', 'Fundamento'];
|
||||
const colWidths = [width * 0.15, width * 0.15, width * 0.2, width * 0.2, width * 0.3];
|
||||
|
||||
// Desenhar cabeçalho
|
||||
let xPos = margin;
|
||||
colunas.forEach((col, index) => {
|
||||
page.drawText(col, {
|
||||
x: xPos,
|
||||
y: yPosition,
|
||||
font: boldFont,
|
||||
size: 10,
|
||||
});
|
||||
xPos += colWidths[index];
|
||||
});
|
||||
|
||||
yPosition -= 15;
|
||||
|
||||
// Desenhar linhas
|
||||
multas.forEach(multa => {
|
||||
xPos = margin;
|
||||
|
||||
const percentual = (parseFloat(multa.percentual.toString()) * 100).toFixed(2) + '%';
|
||||
|
||||
const valores = [
|
||||
multa.tipo,
|
||||
percentual,
|
||||
this.formatarValor(multa.valorBase),
|
||||
this.formatarValor(multa.valorMulta),
|
||||
multa.fundamento || '-'
|
||||
];
|
||||
|
||||
valores.forEach((valor, index) => {
|
||||
page.drawText(valor.toString(), {
|
||||
x: xPos,
|
||||
y: yPosition,
|
||||
font: font,
|
||||
size: 10,
|
||||
});
|
||||
xPos += colWidths[index];
|
||||
});
|
||||
|
||||
yPosition -= 15;
|
||||
});
|
||||
|
||||
return yPosition;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user